赞
踩
Shell 既是一个连接用户和 Linux 内核的程序,又是一门管理 Linux 系统的脚本语言,也就有了Shell编程(就像Python编程、Java编程)
Shell程序连接了用户和 Linux 内核,让用户能够更加高效、安全、低成本地使用 Linux 内核,这就是 Shell 的本质
当然,我们使用命令或者执行脚本还是在应用层,Shell程序(bash)负责解释我们的指令,即Shell本身是独立的一层,所有与内核的交互必须经过这里
别把bash和Linux终端命令行终端搞混了!
Shell针对的不是命令就是程序,都可以叫命令或程序…
不同于图形界面,Shell直接使用命令。但也是控制程序的运行,都需要查找程序在硬盘上的安装位置,然后将它们加载到内存运行;Load into Memory
我们运行一个命令,大部分情况下 Shell 都会去调用内核暴露出来的接口,这就是在使用内核,只是这个过程被 Shell 隐藏了起来
接口其实就是一个一个的函数,使用内核就是调用这些函数。这就是使用内核的全部内容了吗?嗯,是的!除了函数,你没有别的途径使用内核
OK,现在明确一个概念即可
Shell的作用是解释我们要执行的命令或者程序给内核
用户的任何操作都要通过Shell
Shell可以编写脚本程序(命令的集合)
Shell本身也是一个程序,就像OS
大家所说的 Shell 强大,并不是 Shell 本身功能丰富,而是它擅长使用和组织其他的程序
Shell 就是一扇门,这正是 Shell 的魅力所在
由于 Linux 服务器的大规模应用,需要一批专业的人才去管理
OPS 的主要工作就是搭建起运行环境,让程序员写的代码能够高效、稳定、安全地在服务器上运行,他们属于后勤部门。OPS 的要求并不比程序员低,优秀的 OPS 拥有架设服务器集群的能力,还会编程开发常用的工具
- 安装操作系统,例如 CentOS、Ubuntu 等。
- 部署代码运行环境,例如网站后台语言采用 PHP,就需要安装 Nginx、Apache、MySQL、PHP 运行时等。
- 及时修复漏洞,防止服务器被攻击,这包括 Linux 本身漏洞以及各个软件的漏洞。
- 根据项目需求升级软件,例如 PHP 7.0 在性能方面获得了重大突破,如果现在服务器压力比较大,就可以考虑将旧版的 PHP 5.x 升级到 PHP 7.0。
- 监控服务器压力,别让服务器宕机。例如淘宝双十一的时候就会瞬间涌入大量用户,导致部分服务器宕机,网页没法访问,甚至连支付宝都不能使用。
- 分析日志,及时发现代码或者环境的问题,通知相关人员修复
服务器一旦多了,人力工作都需要自动化起来,跑一段代码就能在成千上万台服务器上完成相同的工作,例如服务的监控、代码快速部署、服务启动停止、数据备份、日志分析等
Shell 脚本是实现 Linux 系统自动管理以及自动化运维所必备的工具
除了 Shell,能够用于 Linux 运维的脚本语言还有 Python 和 Perl,参考
当然,Shell编程只是Shell解释器的“副业”
不同的组织机构开发了不同的 Shell,它们各有所长,有的占用资源少,有的支持高级编程功能,有的兼容性好,有的重视用户体验
常见的 Shell 有 sh、bash、csh、tcsh、ash 等
bash shell 是 Linux 的默认 shell,也是接下来要学习的
即bash
就是Shell解释程序的一种,就是那扇门!
往后的日子我们说bash就代表Shell,
bash
这个程序放在/bin/bash
现代的 Linux 上,sh 已经被 bash 代替,
/bin/sh
往往是指向/bin/bash
的符号链接
# 输出 SHELL 环境变量
$ echo $SHELL
/bin/bash
现在的Linux大多都是直接进入图形界面,Shell客户端在这里被隐藏了,进入我们的终端就可以使用Shell
也就是说终端就是Shell的客户端,脚本中的语法命令按理说都可以单行在这里执行!
另一种方式是在启动时,CentOS会创建 6 个虚拟控制台,按下快捷键Ctrl + Alt + Fn(n=2,3,4,5,6)
可以从图形界面模式切换到控制台模式,按下Ctrl + Alt + F1
可以从控制台模式再切换回图形界面模式
也就是说,1 号控制台被图形桌面程序占用了
Shell命令分为两种,Shell 自带的命令称为内置命令,更多的命令是外部的应用程序
command [选项] [参数]
Shell命令的本质是什么?
Shell 内置命令的本质是一个自带的函数,执行内置命令就是调用这个自带的函数。因为函数代码在 Shell 启动时已经被加载到内存了,所以内置命令的执行速度很快。
Shell 外部命令的本质是一个应用程序,执行外部命令就是启动一个新的应用程序。因为要创建新的进程并加载应用程序的代码,所以外部命令的执行速度很慢
例如我们使用C语言编写一个累加的函数编译后放在
~/bin
目录,在终端执行[roy@localhost ~]$ getsum -s 1 -e 100
就能得到结果;因为Shell会去$PATH
指定的目录下寻找与getsum
同名的程序并启动运行如果你想知道更多请
Shell命令的选项和参数在本质上到底是什么?
请参考
不管是内置命令还是外部命令,它后面附带的数据最终都以参数的形式传递给了函数。实现一个命令的一项重要工作就是解析传递给函数的参数
命令提示符$
可以更改,输入命令时换行会出现
>
,也可以改
废话不多说写一个
#!/bin/bash
# Copyright (c) http://c.biancheng.net/shell/
echo "What is your name?"
read PERSON
echo "Hello, $PERSON"
#!
是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行echo 命令用于向标准输出文件(Standard Output,stdout,一般就是指显示器)输出文本
read 命令用来从标准输入文件(Standard Input,stdin,一般就是指键盘)读取用户输入的数据(Linux一切皆文件记得吗?键盘也是文件目录)
这个脚本叫test.sh,不加
.sh
也行,怎么都行!Shell解释的时候按人家的规则办,不行就给你报错!
执行脚本
[roy@localhost ~]$ cd demo #切换到 test.sh 所在的目录
[roy@localhost demo]$ chmod +x ./test.sh #给脚本添加执行权限
[roy@localhost demo]$ ./test.sh #执行脚本文件
Hello World ! #运行结果
当然,可以不写该死的
#!
这一行,直接在执行时指定只不过多了个bash找解释器的过程
[roy@localhost demo]$ bash test.sh
Hello World !
# $是Shell特殊变量
echo $$ #输出当前进程PID
source
source 是Shell 内置命令的一种,它会读取脚本文件中的代码,并依次执行所有语句
会强制执行脚本文件中的全部命令,而忽略脚本文件的权限
[roy@localhost demo]$ source ./test.sh #使用source
Hello World !
Shell的四种运行方式
- 交互式的登录 Shell;
- 交互式的非登录 Shell;
- 非交互式的登录 Shell;
- 非交互式的非登录 Shel
Shell配置文件的加载
无论是否是交互式,是否是登录式,Bash Shell 在启动时总要配置其运行环境,例如初始化环境变量、设置命令提示符、指定系统命令路径等。这个过程是通过加载一系列配置文件完成的,这些配置文件其实就是 Shell 脚本文件
如果是登录式的 Shell,首先会读取和执行
/etc/profiles
,这是所有用户的全局配置文件,接着会到用户主目录中寻找~/.bash_profile
、~/.bash_login
或者~/.profile
,它们都是用户个人的配置文件
如何编写自己的Shell配置文件
对于普通用户来说,也许
~/.bashrc
才是最重要的文件,因为不管是否登录都会加载该文件如果我们想增加自己的路径,可以将该路径放在 ~/.bashrc 文件中,例如:
vim ~/.bashrc
# 将主目录下的 addon 目录也设置为系统路径
PATH=$PATH:$HOME/addon
规则是:各路径使用冒号分割
将下面的代码添加到 ~/.bashrc 文件中,然后重新启动 Shell(终端),命令提示符就变成了
[c.biancheng.net]$
PS1="[c.biancheng.net]\$ "
在 Bash shell 中,每一个变量的值都是字符串,无论你给变量赋值时有没有使用引号,值都会以字符串的形式存储
variable=value
variable='value'
variable="value"
如果 value 包含了空白符,那么就必须使用引号包围起来
以双引号
" "
包围变量的值时,输出时会先解析里面的变量和命令,单引号就是纯文本;其他没有特别要求的字符串等最好都加上双引号,详情了解注意,赋值号
=
的周围不能有空格,这可能和你熟悉的大部分编程语言都不一样
使用一个定义过的变量,只要在变量名前面加美元符号$
即可
将命令的执行结果赋值给变量(命令替换)
variable=`command` # 这是个反引号,容易混淆,不推荐这种方式
variable=$(command)
# 将日志内容赋值给变量log
[roy@localhost demo]$ log=$(cat log.txt)
只读变量,不允许修改,使用readonly
命令即可
#!/bin/bash
myUrl="http://c.biancheng.net/shell/"
readonly myUrl
myUrl="http://c.biancheng.net/shell/"
删除变量
unset variable_name
# unset 命令不能删除只读变量。
作用域
有的变量只能在函数内部使用,这叫做局部变量(local variable);
有的变量可以在当前 Shell 进程中使用,这叫做全局变量(global variable);
而有的变量还可以在子进程中使用,这叫做环境变量(environment variable)
局部变量
Shell 函数中定义的变量默认也是全局变量,它和在函数外部定义变量拥有一样的效果,这个特性和 JavaScript 中的变量是类似的
要想变量的作用域仅限于函数内部,可以在定义时加上
local
命令
#!/bin/bash
#定义函数
function func(){
a=99
}
#调用函数
func
#输出函数内部的变量
echo $a
# 输出结果为99
全局变量
所谓全局变量,就是指变量在当前的整个 Shell 进程中都有效。每个 Shell 进程都有自己的作用域,彼此之间互不影响
在一个 Shell 进程中可以使用 source 命令执行多个 Shell 脚本文件,此时全局变量在这些脚本文件中都有效(这就叫全局)
怎么才能证明进程间相互独立呢?打开两个终端就行
环境变量
环境变量被创建时所处的 Shell 进程称为父进程,如果在父进程中再创建一个新的进程来执行 Shell 命令,那么这个新的进程被称作 Shell 子进程,它会继承父进程的环境变量为自己所用,所以说环境变量可从父进程传给子进程
使用
export
将全局变量导出为环境变量两个没有父子关系的 Shell 进程是不能传递环境变量的,并且环境变量只能向下传递而不能向上传递
总而言之,不同进程之间不能通信!(进程之间通信的问题好像是个难题!)
通过
exit
命令可以一层一层地退出 Shell
# 打开终端,就是父进程
[c.biancheng.net]$ a=22 #定义一个全局变量
[c.biancheng.net]$ echo $a #在当前Shell中输出a,成功
22
[c.biancheng.net]$ bash #进入Shell子进程(bash就是要搞进程执行脚本的)
[c.biancheng.net]$ echo $a #在子进程中输出a,失败
[c.biancheng.net]$ exit #退出Shell子进程,返回上一级Shell
exit
[c.biancheng.net]$ export a #将a导出为环境变量
[c.biancheng.net]$ bash #重新进入Shell子进程
[c.biancheng.net]$ echo $a #在子进程中再次输出a,成功
22
通过
export
导出的环境变量只对当前 Shell 进程以及所有的子进程有效,如果最顶层的父进程被关闭了,那么环境变量也就随之消失了如果我想让一个变量在所有 Shell 进程中都有效,不管它们之间是否存在父子关系,只有将变量写入 Shell 配置文件中才能达到这个目的!在每次Shell初始化配置环境的时候载入
定义 Shell 函数时不能带参数,但是在调用函数时却可以传递参数,这些传递进来的参数,在函数内部就也使用$n
的形式接收,例如,$1 表示第一个参数,$2 表示第二个参数
这种通过$n
的形式来接收的参数,在 Shell 中称为位置参数,咱们也称他们为“特殊变量”,因为违反变量命名规则嘛
传递参数的时候使用空格分隔
#!/bin/bash
#定义函数
function func(){
echo "Language: $1"
echo "URL: $2"
}
#调用函数
func C++ http://c.biancheng.net/cplus/
写这个东西的时候就按其他编程语言的方式写就行
如果参数个数太多,达到或者超过了 10 个,那么就得用
${n}
的形式来接收了,例如 ${10}、${23}
上面的位置参数也是特殊变量
变量 | 含义 |
---|---|
$0 | 当前脚本的文件名。 |
$n(n≥1) | 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 $1,第二个参数是 $2。 |
$# | 传递给脚本或函数的参数个数。 |
$* | 传递给脚本或函数的所有参数。 |
$@ | 传递给脚本或函数的所有参数。当被双引号" " 包含时,$@ 与 $* 稍有不同 |
$? | 上个命令的退出状态,或函数的返回值 |
$$ | 当前 Shell 进程 ID。对于 Shell 脚本,就是这些脚本所在的进程 ID |
当 $* 和 $@ 不被双引号
" "
包围时,它们之间没有任何区别,都是将接收到的每个参数看做一份数据,彼此之间以空格来分隔
"$*"
会将所有的参数从整体上看做一份数据,而不是把每个参数都看做一份数据。
"$@"
仍然将每个参数都看作一份数据,彼此之间是独立的
$?
中,退出状态是一个数字,一般情况下,大部分命令执行成功会返回 0,失败返回 1,下面是两个例子,分别获取退出状态和返回值
#!/bin/bash
if [ "$1" == 100 ]
then
exit 0 #参数正确,退出状态为0
else
exit 1 #参数错误,退出状态1
fi
[roy@localhost demo]$ bash ./test.sh 100 #作为一个新进程运行,不然会直接退出
[roy@localhost demo]$ echo $?
0
#!/bin/bash
#得到两个数相加的和
function add(){
return `expr $1 + $2`
}
add 23 50 #调用函数
echo $? #获取函数返回值
这里的返回方式有问题!
获取字符串长度
#!/bin/bash
str="http://c.biancheng.net/shell/"
echo ${#str}
字符串拼接
#!/bin/bash
name="Shell"
url="http://c.biancheng.net/shell/"
str1=$name$url #中间不能有空格
str2="$name $url" #如果被双引号包围,那么中间可以有空格
str3=$name": "$url #中间可以出现别的字符串
str4="$name: $url" #这样写也可以
str5="${name}Script: ${url}index.html" #这个时候需要给变量名加上大括号
双引号无敌?
字符串截取
从指定位置截取,两个参数:除了指定起始位置,还需要截取长度
url="c.biancheng.net"
echo ${url: 2: 9}
# 从右边开始计数
url="c.biancheng.net"
echo ${url: 0-13: 9}
从指定字符(子字符串)截取
# ${string#*chars} 使用 # 号截取右边字符
url="http://c.biancheng.net/index.html"
echo ${url#*:} # //c.biancheng.net/index.html
# 使用 % 截取左边字符,因为是左边字符,所以*在右侧
url="http://c.biancheng.net/index.html"
echo ${url%/*} #结果为 http://c.biancheng.net
echo ${url%%/*} #结果为 http:
格式 | 说明 |
---|---|
${string: start :length} | 从 string 字符串的左边第 start 个字符开始,向右截取 length 个字符。 |
${string: start} | 从 string 字符串的左边第 start 个字符开始截取,直到最后。 |
${string: 0-start :length} | 从 string 字符串的右边第 start 个字符开始,向右截取 length 个字符。 |
${string: 0-start} | 从 string 字符串的右边第 start 个字符开始截取,直到最后。 |
${string#*chars} | 从 string 字符串第一次出现 chars 的位置开始,截取 chars 右边的所有字符 |
${string##*chars} | 从 string 字符串最后一次出现 chars 的位置开始,截取 chars 右边的所有字符 |
${string%*chars} | 从 string 字符串第一次出现 chars 的位置开始,截取 chars 左边的所有字符 |
${string%%*chars} | 从 string 字符串最后一次出现 chars 的位置开始,截取 chars 左边的所有字符 |
Shell 并且没有限制数组的大小,理论上可以存放无限量的数据
获取数组中的元素要使用下标[]
,下标可以是一个整数,也可以是一个结果为整数的表达式
常用的 Bash Shell 只支持一维数组,不支持多维数组
array_name=(ele1 ele2 ele3 ... elen)
arr=(20 56 "http://c.biancheng.net/shell/")
Shell有很多让人“不适应”的地方,数组用小括号算一个吧
同样,赋值号
=
两边不能有空格Shell 是弱类型的,它并不要求所有数组元素的类型必须相同,类型都不要求相同!
ages=([3]=24 [5]=19 [10]=12)
给特定元素赋值,精吗?这里数组的长度就为3,不是11
# 获取数组元素的值需要使用大括号
n=${nums[2]}
# 得到数组的所有元素
${nums[*]}
${nums[@]}
# 获取数组长度呗!
${#array_name[@]}
${#array_name[*]}
# 如果某个元素是字符串,还可以通过指定下标的方式获得该元素的长度
${#arr[2]} # 类比一下 ${#string_name}
# 反正#就和长度有关知道吗?
规律就是:$后面只认变量,有其他和变量一起作用的东西包起来才认识,有不是和变量一起作用的包起来隔开才认识!
*
或者@
的作用就将元素扩展成列表,搞成列表就方便了?
数组拼接(合并)
# 先扩展成列表,再合并
array_new=(${array1[@]} ${array2[@]})
array_new=(${array1[*]} ${array2[*]})
${}
在手,天下我有
删除元素
unset array_name[index]
unset array_name # 删除整个数组
关联数组
关联数组也称为“键值对(key-value)”数组,键(key)也即字符串形式的数组下标,值(value)也即元素值
# 创建一个叫做 color 的关联数组
declare -A color
color["red"]="#ff0000"
color["green"]="#00ff00"
color["blue"]="#0000ff"
# 定义的时候赋值
declare -A color=(["red"]="#ff0000", ["green"]="#00ff00", ["blue"]="#0000ff")
这个
-A
是设置变量属性的,后面讲!你会看到后面吗?
# 访问就是把数字索引换成字符串索引,对于这里使用小括号我也很无奈!
$(array_name["index"])
# 获取所有元素值
${!array_name[@]}
${!array_name[*]}
# 获取所有下标值,这个叹号让人激动
${!array_name[@]}
${!array_name[*]}
# 长度
${#array_name[*]}
${#array_name[@]}
到这一步至少要明白变量取值用${}
也叫Shell内建命令
这个叫法吧,都得了解,万一有人装逼用“内建”,你说你不懂就像个傻逼!
之前说Shell解释程序负责的不是命令就是程序,其实命令说的就是内置命令,其他都是程序!
挺多的,准备好了吗?
命令 | 说明 |
---|---|
: | 扩展参数列表,执行重定向操作 |
. | 读取并执行指定文件中的命令(在当前 shell 环境中) |
alias | 为指定命令定义一个别名 |
bg | 将作业以后台模式运行 |
bind | 将键盘序列绑定到一个 readline 函数或宏 |
break | 退出 for、while、select 或 until 循环 |
builtin | 执行指定的 shell 内建命令 |
caller | 返回活动子函数调用的上下文 |
cd | 将当前目录切换为指定的目录 |
command | 执行指定的命令,无需进行通常的 shell 查找 |
compgen | 为指定单词生成可能的补全匹配 |
complete | 显示指定的单词是如何补全的 |
compopt | 修改指定单词的补全选项 |
continue | 继续执行 for、while、select 或 until 循环的下一次迭代 |
declare | 声明一个变量或变量类型。 |
dirs | 显示当前存储目录的列表 |
disown | 从进程作业表中刪除指定的作业 |
echo | 将指定字符串输出到 STDOUT |
enable | 启用或禁用指定的内建shell命令 |
eval | 将指定的参数拼接成一个命令,然后执行该命令 |
exec | 用指定命令替换 shell 进程 |
exit | 强制 shell 以指定的退出状态码退出 |
export | 设置子 shell 进程可用的变量 |
fc | 从历史记录中选择命令列表 |
fg | 将作业以前台模式运行 |
getopts | 分析指定的位置参数 |
hash | 查找并记住指定命令的全路径名 |
help | 显示帮助文件 |
history | 显示命令历史记录 |
jobs | 列出活动作业 |
kill | 向指定的进程 ID(PID) 发送一个系统信号 |
let | 计算一个数学表达式中的每个参数 |
local | 在函数中创建一个作用域受限的变量 |
logout | 退出登录 shell |
mapfile | 从 STDIN 读取数据行,并将其加入索引数组 |
popd | 从目录栈中删除记录 |
printf | 使用格式化字符串显示文本 |
pushd | 向目录栈添加一个目录 |
pwd | 显示当前工作目录的路径名 |
read | 从 STDIN 读取一行数据并将其赋给一个变量 |
readarray | 从 STDIN 读取数据行并将其放入索引数组 |
readonly | 从 STDIN 读取一行数据并将其赋给一个不可修改的变量 |
return | 强制函数以某个值退出,这个值可以被调用脚本提取 |
set | 设置并显示环境变量的值和 shell 属性 |
shift | 将位置参数依次向下降一个位置 |
shopt | 打开/关闭控制 shell 可选行为的变量值 |
source | 读取并执行指定文件中的命令(在当前 shell 环境中) |
suspend | 暂停 Shell 的执行,直到收到一个 SIGCONT 信号 |
test | 基于指定条件返回退出状态码 0 或 1 |
times | 显示累计的用户和系统时间 |
trap | 如果收到了指定的系统信号,执行指定的命令 |
type | 显示指定的单词如果作为命令将会如何被解释 |
typeset | 声明一个变量或变量类型。 |
ulimit | 为系统用户设置指定的资源的上限 |
umask | 为新建的文件和目录设置默认权限 |
unalias | 刪除指定的别名 |
unset | 刪除指定的环境变量或 shell 属性 |
wait | 等待指定的进程完成,并返回退出状态码 |
有几个重要命令,可以戳此了解
注:这里的内置命令和普通的Linux终端命令不同,这里是写在Shell脚本中的
#!/bin/bash
read -n 1 -p "Enter a char > " char # -n num 表示读取num个字符而不是整行 -p 显示后面的提示信息
printf "\n" # 换行
echo $char
Shell 不能直接进行算数运算,必须使用数学计算命令
接受它好嘛,就是个奇葩!因为,在 Bash中,如果不特别指明,每一个变量的值都是字符串
OK,数学计算命令有哪些呢 ?
运算操作符/运算命令 | 说明 |
---|---|
(( )) | 用于整数运算,效率很高,推荐使用。 |
let | 用于整数运算,和 (()) 类似。 |
$[] | 用于整数运算,不如 (()) 灵活。 |
expr | 可用于整数运算,也可以处理字符串。比较麻烦,需要注意各种细节,不推荐使用。 |
bc | Linux下的一个计算器程序,可以处理整数和小数。Shell 本身只支持整数运算,想计算小数就得使用 bc 这个外部的计算器。 |
declare -i | 将变量定义为整数,然后再进行数学运算时就不会被当做字符串了。功能有限,仅支持最基本的数学运算(加减乘除和取余),不支持逻辑运算、自增自减等,所以在实际开发中很少使用 |
(()) 可以用于整数计算,bc 可以小数计算,其他的别学了,你没空!
下面开始了解关键字咯
最简单的就是只用if
# 注意分号和结尾的'fi'
if condition; then
statement(s)
fi
# 也可以换行写then,就不加分号了呢
# 如果有两个分支
if condition
then
statement1
else
statement2
fi
# 任意数目的分支
if condition1
then
statement1
elif condition2
then
statement2
elif condition3
then
statement3
……
else
statementn
fi
if 语句的判断条件,结果不是真就是假,从本质上讲,判断的就是命令的退出状态
例如condition中
$a==$b
这个判断会返回一个结果,为 0 表示“成功”,除 0 以外的其它任何退出状态都为“失败”(当然,失败的不是那么彻底,更多的是提示)其他语言一般0表示失败,接受吧!就是这么奇葩
实际上,每一条 Shell 命令,不管是 Bash 内置命令(例如 cd、echo),还是外部的 Linux 命令(例如 ls、awk),还是自定义的 Shell 函数,当它退出(运行结束)时,都会返回一个比较小的整数值给调用(使用)它的程序
这和函数的返回值是不同的,这只是“状态”
# 当然,可以使用逻辑运算符组合退出状态
运算符 | 使用格式 | 说明 |
---|---|---|
&& | expression1 && expression2 | 逻辑与运算符,当 expression1 和 expression2 同时成立时,整个表达式才成立。 如果检测到 expression1 的退出状态为 0,就不会再检测 expression2 了,因为不管 expression2 的退出状态是什么,整个表达式必然都是不成立的,检测了也是多此一举。 |
|| | expression1 || expression2 | 逻辑或运算符,expression1 和 expression2 两个表达式中只要有一个成立,整个表达式就成立。 如果检测到 expression1 的退出状态为 1,就不会再检测 expression2 了,因为不管 expression2 的退出状态是什么,整个表达式必然都是成立的,检测了也是多此一举。 |
! | !expression | 逻辑非运算符,相当于“取反”的效果。如果 expression 成立,那么整个表达式就不成立;如果 expression 不成立,那么整个表达式就成立 |
和经常一起使用的还有
test
命令,用来检测某个条件是否成立,详情戳我当然,
test
也可做文件检测还有数值比较test 命令比较奇葩,
>、<、==
只能用来比较字符串,不能用来比较数字,比较数字需要使用-eq、-gt
等选项;
[[ ]]
是 Shell 内置关键字
它和test 命令类似,也用来检测某个条件是否成立
test 能做到的,[[ ]] 也能做到,而且 [[ ]] 做的更好;test 做不到的,[[ ]] 还能做到
# 用法
[[ expression ]] # 注意前后的两个空格,没有不行!
# 支持正则表达式
[[ str =~ regex ]]
如上所示,在 Shell [[ ]] 中,可以使用
=~
来检测字符串是否符合某个正则表达式
#!/bin/bash
read tel # 读取输入流
if [[ $tel =~ ^1[0-9]{10}$ ]]
then
echo "你输入的是手机号码"
else
echo "你输入的不是手机号码"
fi
if
作用类似,基本格式如下case expression in
pattern1)
statement1
;;
pattern2)
statement2
;;
pattern3)
statement3
;;
……
*)
statementn
esac
pattern
可以是一个数字、一个字符串,甚至是一个简单的正则表达式为啥是俩分号?匹配内容为哈用半个括号?管他呢!
循环,知道吗?基本格式如下
while condition
do
statements
done
#!/bin/bash
i=1
sum=0
while ((i <= 100))
do
((sum += i))
((i++))
done
echo "The sum is: $sum"
until
循环
和 while 循环恰好相反,当判断条件不成立时才进行循环,一旦判断条件成立,就终止循环
有意思吗?憨批…
#!/bin/bash
i=1
sum=0
until ((i > 100))
do
((sum += i))
((i++))
done
echo "The sum is: $sum"
for
循环
for((exp1; exp2; exp3))
do
statements
done
和C语言风格很类似
#!/bin/bash
sum=0
for ((i=1; i<=100; i++))
do
((sum += i))
done
echo "The sum is: $sum"
for in
Python风格
for variable in value_list
do
statements
done
value_list是个列表,
in value_list
部分可以省略,省略后的效果相当于in $@
这里说列表就类比Python中的概念,融会贯通,还不是纠结的时候
#!/bin/bash
sum=0
for n in 1 2 3 4 5 6
do
echo $n
((sum+=n))
done
echo "The sum is "$sum
#!/bin/bash
for filename in *.sh
do
echo $filename
done
select in
用来增强交互性,它可以显示出带编号的菜单,用户输入不同的编号就可以选择不同的菜单,并执行不同的功能
select variable in value_list
do
statements
done
#!/bin/bash
echo "What is your favourite OS?"
select name in "Linux" "Windows" "Mac OS" "UNIX" "Android"
do
echo $name
done
echo "You have selected $name"
# 运行结果 What is your favourite OS? 1) Linux 2) Windows 3) Mac OS 4) UNIX 5) Android #? 4↙ You have selected UNIX #? 1↙ You have selected Linux #? 9↙ You have selected #? 2↙ You have selected Windows #?^D
ctrl+D退出
总结规律:Shell编程有头有尾!fi/done/esac…
break
&continue
一般break可以跳出整个循环(一层),continue跳过此次循环(一次)执行下一次!
但 Shell 中的 break 和 continue 可以跳出多层和多次,注意体会
break n
n 表示跳出循环的层数,如果省略 n,则表示跳出当前的整层循环(n=1)
continue n
如果省略 n,continue 会跳过本次循环
加上n,则会在本层循环内跳过去n次
#!/bin/bash
sum=0
while read n; do
if((n<1 || n>100)); then
continue
fi
((sum+=n))
done
echo "sum=$sum"
注意区别这里的“层”和“次”
break是结束某层,循环停止了;continue是跳过某次,循环继续
和其他语言类似,只不过多了n
和其他语言的函数类似,语法略有差别
# 定义
function name() {
statements
[return value]
}
# 调用,反正是不带括号
name
name param1 param2 param3 # 多个参数之间以空格分隔
$#
可以获取传递的参数的个数,#总代表长度$@
或者$*
可以一次性获取所有的参数#!/bin/bash
function getsum(){
local sum=0 # 限定作用域
for n in $@
do
((sum+=n))
done
return $sum
}
getsum 10 20 55 15 #调用函数并传递参数
echo $?
$?
表示函数的返回值,也可以表示上一个命令执行的结果函数返回值
函数的返回结果和执行命令同样,是最后一行命令的退出状态,0表示成功,其他表示失败
说白了Shell 函数不需要写return,或者写成
return $?
即可那么怎么接收函数中返回的计算结果(变量值)呢?有两种方式:
- 一种是借助全局变量,将得到的结果赋值给全局变量;
- 一种是在函数内部使用 echo、printf 命令将结果输出,在函数外部使用
$()
或者反引号捕获结果
#!/bin/bash sum=0 #全局变量 function getsum(){ for((i=$1; i<=$2; i++)); do ((sum+=i)) #改变全局变量 done return $? #返回上一条命令的退出状态 } read m read n if getsum $m $n; then echo "The sum is $sum" #输出全局变量 else echo "Error!" fi
#!/bin/bash function getsum(){ local sum=0 #局部变量 for((i=$1; i<=$2; i++)); do ((sum+=i)) done echo $sum return $? } read m read n total=$(getsum $m $n) echo "The sum is $total" #也可以省略 total 变量,直接写成下面的形式 #echo "The sum is "$(getsum $m $n)
这里说的捕获就是使用
$()
将命令的执行结果赋值给变量(命令替换),这个结果是echo输出的而不是函数的最后一行命令的退出状态详情参考
Linux 中一切皆文件,包括标准输入设备(键盘)和标准输出设备(显示器)在内的所有计算机硬件都是文件。
为了表示和区分已经打开的文件,Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)
文件描述符 | 文件名 | 类型 | 硬件 |
---|---|---|---|
0 | stdin | 标准输入文件 | 键盘 |
1 | stdout | 标准输出文件 | 显示器 |
2 | stderr | 标准错误输出文件 | 显示器 |
输出重定向是指命令的结果不再输出到显示器上,而是输出到其它地方,一般是文件中
在输出重定向中,
>
代表的是覆盖,>>
代表的是追加,详解
fd
和>
之间不能有空格,fd是指文件描述符
输入重定向就是改变输入的方向,不再使用键盘作为命令输入的来源,而使用文件作为命令的输入
Linux 文件描述符到底是什么?
一个 Linux 进程启动后,会在内核空间中创建一个 PCB 控制块,PCB 内部有一个文件描述符表(File descriptor table),记录着当前进程所有可用的文件描述符,也即当前进程所有打开的文件
除了文件描述符表,系统还需要维护另外两张表:
- 打开文件表(Open file table)
- i-node 表(i-node table)
通过文件描述符(数组下标),可以找到文件指针,从而进入打开文件表
要想真正读写文件,还得通过打开文件表的 i-node 指针进入 i-node 表
详情戳此
重定向本质
通过上面可以发现,打开文件是通过文件描述符(数组下标)找到文件指针(可以理解为指针,本质上还是数组中存储的内存地址嘛),然后进入打开文件表和 i-node 表,这两个表里面才真正保存了与打开文件相关的各种信息
即文件指针是文件描述符和真实文件之间最关键的“纽带”;这条纽带却非常脆弱,很容易被修改
Linux 系统提供的函数可以修改文件指针(修改数组值,描述符不动),比如
dup()
、dup2()
;Shell 也能修改文件指针,输入输出重定向就是这么实现的详情戳此,话说这个指针应该是指向打开文件表和表中的具体位置?
使用exec命令操作文件描述符
使用 exec 命令可以永久性地重定向,后续命令的输入输出方向也被确定了,直到再次遇到 exec 命令才会改变重定向的方向;
exec >log.txt
将当前 Shell 进程的所有标准输出重定向到 log.txt 文件,它等价于exec 1>log.txt
重定向的恢复
代码块重定向
将重定向命令放在代码块的结尾处,就可以对代码块中的所有命令实施重定向
#!/bin/bash sum=0 while read n; do ((sum += n)) done <nums.txt #输入重定向 echo "sum=$sum" #!/bin/bash sum=0 while read n; do ((sum += n)) echo "this number: $n" done <nums.txt >log.txt #同时使用输入输出重定向 echo "sum=$sum" #!/bin/bash { echo "C语言中文网"; echo "http://www.beylze.com/d/file/20190908/ggmqcykqgwe.net"; echo "7" } >log.txt #输出重定向 { read name; read url; read age } <log.txt #输入重定向 echo "$name已经$age岁了,它的网址是 $url"
代码块,就是由多条语句组成的一个整体;for、while、until 循环,或者 if…else、case…in 选择结构,或者由
{ }
包围的命令
Here Document(嵌入文档)
有时候命令需要处理的数据量很小,将它放在一个单独的文件中有点“大动干戈”,不如直接放在代码中来得方便
嵌入,就是把数据和代码放在一起,而不是分开存放
详情戳此
command <<END
document
END
# command是 Shell 命令,<<END是开始标志,END是结束标志,document是输入的文档(也就是一行一行的字符串)
[roy@localhost ~]$ cat <<END
> shell教程
> http://www.beylze.com/d/file/20190908/i5zpldli3og
> 已经进行了三次改版
> END
Shell教程
http://www.beylze.com/d/file/20190908/i5zpldli3og
已经进行了三次改版
Here String(内嵌字符串)
是 Here Document 的一个变种
告诉 Shell 把 string 部分作为命令需要处理的数据
如果 string 中带有空格,则必须使用双引号或者单引号包围
# 将小写字符串转换为大写
[roy@localhost ~]$ tr a-z A-Z <<< "one two three"
ONE TWO THREE
将多个命令划分为一组,或者看成一个整体
{ command1; command2; command3; . . . }
(command1; command2; command3;. . . )
由花括号
{}
包围起来的组命名在当前 Shell 进程中执行,而由小括号()
包围起来的组命令会创建一个子 Shell,所有命令都在子 Shell 中执行花括号和命令之间必须有一个空格,并且最后一个命令必须用一个分号或者一个换行符结束
组命令可以将多条命令的输出结果合并在一起,在使用重定向和管道时会特别方便
ls -l > out.txt #>表示覆盖
echo "http://www.beylze.com/d/file/20190908/vvtxomnk1w5.html >> out.txt #>>表示追加
cat readme.txt >> out.txt
# 组合
{ ls -l; echo "http://www.beylze.com/d/file/20190908/vvtxomnk1w5.html; cat readme.txt; } > out.txt
(ls -l; echo "http://www.beylze.com/d/file/20190908/vvtxomnk1w5.html; cat readme.txt) > out.txt
# 与管道命令结合,送给lpr命令做输入
{ ls -l; echo "http://www.beylze.com/d/file/20190908/vvtxomnk1w5.html; cat readme.txt; } | lpr
在系统中,系统运行的应用程序几乎都是从 init(pid为 1 的进程)进程派生而来的,所有这些应用程序都可以视为 init 进程的子进程,而 init 则为它们的父进程
使用pstree -p
命令就可以看到 init 及系统中其他进程的进程树信息(包括 pid)
CentOS 7 为了提高启动速度使用
systemd
替代了 init
子进程的创建
使用 fork() 函数可以创建一个子进程;除了 PID(进程ID)等极少的参数不同外,子进程的一切都来自父进程,,包括代码、数据、堆栈、打开的文件等,就连代码的执行位置(状态)都是一样的,这种子进程又被称为子 Shell(sub shell)
还有一种创建子进程的方式,就是子进程被 fork() 出来以后立即调用 exec() 函数加载新的可执行文件,而不使用从父进程继承来的一切
详情戳此
和命令替换类似,命令替换是把一个命令的输出结果赋值给另一个变量,进程替换则是把一个命令的输出结果传递给另一个(组)命令
# echo 命令在父 shell 中执行,而 read 命令在子 Shell 中执行,当 read 执行结束时,子 Shell 被销毁,REPLY 变量也就消失了
echo "http://www.beylze.com/d/file/20190908/y5jvity5mfg.html | read
echo $REPLY
那怎么解决这个不同父子进程中变量的共享问题呢?
进程替换有两种形式:
# 借助输入重定向,它的输出结果可以作为另一个命令的输入
<(commands)
# 例如
read < <(echo "http://c.biancheng.net/shell/)
echo $REPLY
# 借助输出重定向,它可以接收另一个命令的输出结果
>(commands)
echo "C语言中文网" > >(read; echo "你好,$REPLY")
第一个
<
或者>
都是重定向注意空格
进程替换的本质
为了能够在不同进程之间传递数据,实际上进程替换会跟系统中的文件关联起来,这个文件的名字为
/dev/fd/n
(n 是一个整数)。该文件会作为参数传递给()
中的命令,()
中的命令对该文件是读取还是写入取决于进程替换格式是<
还是>
该文件是系统内部文件,我们一般查看不到,一般为
/dev/fd/63
即就是这个文件起到了数据中转或者数据桥梁的作用
可以将两个或者多个命令(程序或者进程)连接到一起,把一个命令的输出作为下一个命令的输入,以这种方式连接的两个或者多个命令就形成了管道(pipe)
管道使用竖线|
连接多个命令,这被称为管道符
mysqldump -u root -p '123456' wiki | gzip -9 | ssh username@remote_ip "cat > /backup/wikidb.gz"
上面命令做了数据库的备份压缩和安全拷贝,如果分开执行每一次都会产生临时文件,使用管道就避免了创建临时文件
使用了管道的命令有如下特点
命令的语法紧凑并且使用简单。
通过使用管道,将三个命令串联到一起就完成了远程 mysql 备份的复杂任务。
从管道输出的标准错误会混合到一起
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a7nfTbVa-1618755742141)(C:\Users\szspl\Desktop\secureCopy.png)]
注意重定向和管道的区别
command > file
command1 | command1
不要随便尝试
command1 > command1
,可能会损坏命令的程序
实例
# 查看文件 log.txt 是否存在于当前目录下
[c.biancheng.net]$ ls | grep log.txt
log.txt
# 也可以将最后的输出重定向
[c.biancheng.net]$ ls -al | grep log.txt >output.txt
[c.biancheng.net]$ cat output.txt
-rw-rw-r--. 1 mozhiyan mozhiyan 0 4月 15 17:26 log.txt
# 也可以使用重定向做输入
[c.biancheng.net]$ tr a-z A-Z <os.txt | sort | uniq
总之,别直接将命令用重定向符号连接
将几个命令通过管道符组合在一起就形成一个管道,通过这种方式使用的命令就被称为过滤器
严谨的说,如果一个 Linux 命令是从标准输入接收它的输入数据,并在标准输出上产生它的输出数据(结果),那么这个命令就被称为过滤器,过滤器通常与 Linux 管道一起使用
常用过滤器命令
命令 | 说明 |
---|---|
awk | 用于文本处理的解释性程序设计语言,通常被作为数据提取和报告的工具。 |
cut | 用于将每个输入文件(如果没有指定文件则为标准输入)的每行的指定部分输出到标准输出。 |
grep | 用于搜索一个或多个文件中匹配指定模式的行。 |
tar | 用于归档文件的应用程序。 |
head | 用于读取文件的开头部分(默认是 10 行)。如果没有指定文件,则从标准输入读取。 |
paste | 用于合并文件的行。 |
sed | 用于过滤和转换文本的流编辑器。 |
sort | 用于对文本文件的行进行排序。 |
split | 用于将文件分割成块。 |
strings | 用于打印文件中可打印的字符串。 |
tac | 与 cat 命令的功能相反,用于倒序地显示文件或连接文件。 |
tail | 用于显示文件的结尾部分。 |
tee | 用于从标准输入读取内容并写入到标准输出和文件。 |
tr | 用于转换或删除字符。 |
uniq | 用于报告或忽略重复的行。 |
wc | 用于打印文件中的总行数、单词数或字节数 |
常见用法戳此
说白了就是一堆废话!
如何检测子Shell和子进程?戳此了解
$
变量可以显示进程ID,但要注意:在普通的子进程中,$ 确实被展开为子进程的 ID;但是在子 Shell 中,$ 却被展开成父进程的 ID,是个坑!
BASH_SUBSHELL
和SHLVL
,用它们来检测子 Shell 非常方便
信号被用于进程间的通信。信号是一个发送到某个进程或同一进程中的特定线程的异步通知,用于通知发生的一个事件
在 Linux 中,信号在处理异常和中断方面,扮演了极其重要的角色
哪些情况会引发信号:
- 键盘事件
ctrl + c
ctrl + \
- 非法内存 如果内存管理出错,系统就会发送一个信号进行处理
- 硬件故障 同样的,硬件出现故障系统也会产生一个信号
- 环境切换 比如说从用户态切换到其他态,状态的改变也会发送一个信号,这个信号会告知给系统
其他就是主动发信号了!
当进程收到一个信号时,可能会发生以下 3 种情况:
进程可能会忽略此信号。有些信号不能被忽略,而有些没有默认行为的信号,默认会被忽略
进程可能会捕获此信号,并执行一个被称为信号处理器的特殊函数
进程可能会执行信号的默认行为。例如,信号 15(SIGTERM) 的默认行为是结束进程
当一个进程执行信号处理时,如果还有其他信号到达,那么新的信号会被阻断直到处理器返回为止
每个信号都有以SIG
开头的名称,并定义为唯一的正整数
输入
kill -l
命令,将显示所有信号的信号值和相应的信号名信号值被定义在文件 /usr/include/bits/signum.h 中,其源文件是
/usr/src/linux/kernel/signal.c
信号值和信号名是按规则的逻辑上的定义,可以暂时理解为调用其他程序(信号处理)的触发器,并不需要结合硬件考虑,但需要理解数据结构相关的内容,了解信号如何在进程间传递信息
信号是为进程服务的,首先需要理解Linux下的PCB(进程控制块)
Linux下的PCB
进程控制块(PCB)是系统为了管理进程设置的一个专门的数据结构(task_struct),用来记录进程的外部特征,描述进程的运动变化过程
系统利用PCB来控制和管理进程,所以PCB是系统感知进程存在的唯一标志。进程与PCB是一一对应的
别把进程想的太复杂!至于是怎么调度起硬件资源的,就是和内核CPU的事情了
使用 disown
命令可以将某个作业从列表中删除
在Shell向各作业发送
SIGHUP
信号的时候(意思是Shell要退出),就可以跳过这个作业让他继续执行着!详情戳此
Linux进程是什么?
当你在 Linux 系统中执行一个程序时,系统会为这个程序创建特定的环境。这个环境包含系统运行这个程序所需的任何东西,这个环境就叫一个进程;即所有命令和程序的执行都会启动一个新的进程
每个运行的 Shell 都分别是一个进程。当你从 Shell 调用一个命令时,对应的程序就会在一个新进程中执行
进程都有它自己的生命周期,比如,创建、执行、结束和清除。每个进程也都有各自的状态
操作系统通过被称为 PID 或进程 ID 的数字编码来追踪进程(唯一)
运行进程有两种方式:前台和后台
进程的查看在《Linux基础》中说过,主要是
ps aux
和ps -le
向进程发送信号
trap
捕获信号
# 命令格式
trap command signal
command表示接收到指定信号时将要采取的行动,signal是要处理的信号名
例如:在脚本执行时按下
CTRL+C
,将显示"program exit…"并退出(CTRL+C
的信号是SIGINT),咋们想忽略这个信号,只需要trap '' 2
,表示在捕获到这个信号时不采取任何动作!
import
,这里使用source
使用叹号定位法可以调用任意历史命令
[roy@localhost ~]$ history
7 ls
8 vi test.go
9 ls
10 history
11 export HISTTIMEFORMAT='%F %T'
12 histroy
13 export HISTTIMEFORMAT='%F %T'
14 history
15 export HISTTIMEFORMAT=
16 man grep
17 ls
18 ll
19 history
[roc@roclinux ~]$ !16
更多配置请戳
fcgiwrap
apt install -y fcgiwrap # 如果不能下载把虚拟机改为NAT模式联网
# 启动
nohup fcgiwrap -f -c 4 -s unix:/run/fcgiwrap.socket &
nohup fcgiwrap -f -c 4 -s tcp:192.168.154.130:9000 & # 推荐(虚拟机IP)
# 两种方式效果相同,都是为了让nginx找到fcgi建立通信
# 第二种,nginx和fcgi可以不在同一台机器上
# 会提示 nohup:ignoring input and appending output to 'nohup.out'
# 退出
killall -9 fcgiwrap
fcgi.conf
/etc/nginx
目录下的fastcgi_params中保存了很多变量信息,可以使用;包括了$document_root和$fastcgi_script_namefastcgi_param
指定的是脚本文件的路径,$document_root
就是root指定的路径systemctl restart nginx
,如果出错是因为配置没写对!# 503服务器错误(正在维护/CPU占用大导致)
sudo chown roykun:roykun /run/fcgiwrap.socket
# nginx.conf
user roykun;
index.cgi
文件#!/bin/bash
. ./echo_env.sh
echo_env
# 架子啊echo_env.sh,并执行里面的echo_env函数
echo_env(){
echo '$request_method:' $request_method;
echo '<br>'
echo 'Hello fcgi'
}
# 配置nginx要使用nginx定义和加载的变量,这里是shell,可以使用系统变量,详见我的博客
# 《Linux Shell》
# 完整报错信息 error.log
2021/04/21 03:52:59 [error] 25326#25326: *6 FastCGI sent in stderr: "Cannot get
script name, are DOCUMENT_ROOT and SCRIPT_NAME (or SCRIPT_FILENAME) set and is the
script executable?" while reading response header from upstream, client: 127.0.0.1,
server: _, request: "GET /cig/ HTTP/1.1", upstream: "fastcgi://unix:/run/fcgiwrap.socket:", host: "127.0.0.1:8088"
127.00.0.1:8088/cgi/index.cgi
可以下载,但不能访问/cgi/# 报错信息
*1 directory index of "/var/www/fcgi/cgi/" is forbidden,
client: 127.0.0.1, server: localhost, request: "GET /cgi/ HTTP/1.1",
host: "127.0.0.1:8088"
#!/bin/bash html(){ # 定义一个函数 echo -e "Content-Type: text/html;charset=utf-8\n\n" } get(){ html # 指定函数 echo "get 方法" # 打印 if [[ -n $QUERY_STRING ]]; then # $QUERT_STRING就是get请求发来的查询字符串;-n就代表键值对个数,用来判断有没有! echo $QUERY_STRING fi } post(){ html echo "post 方法" if [[ -n $POST_DATA ]]; then # 判断是post echo $POST_DATA fi } cgi(){ if [ "$REQUEST_METHOD" = "GET" ];then # 请求方式是get的话 get "$REQUEST_METHED" elif [ "$REQUEST_METHOD" = "POST" ];then read -n $CONTENT_LENGTH POST_DATA post "$POST_DATA" fi } cgi
http://127.0.0.1:8088/cgi/?name=roy&age=18
即可 (name=roy&age=18 )Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。