目录
Shell基础
常用指令
父子shell
在当前shell中打开其他shell时,会创建新的shell程序,称为子shell(chile shell)。
1 | $ ps --forest |
通过进程列表调用命令可创建子shell,将多条命令以';'
作为间隔,放置在'()'
中执行。进程列表是一种命令分组,另一种命令分组是在'{}'
中执行,但不会创建子shell。
1 | $ pwd; ls; ps -f; echo $BASH_SUBSHELL |
在shell脚本中,经常使用子shell进行多进程处理,但是会明显拖慢处理速度,一种高效的使用方法是后台模式
1 | $ # 将命令置入后台模式 |
环境变量
环境变量(environment variable)用于存储有关shell会话和工作环境的信息,分为局部变量和全局变量。局部变量只对创建它们的shell可见;全局变量对shell会话和所生成的子shell都是可见的,用printenv
或env
输出全局变量
1 | $ env | less |
注意变量的作用域
- 局部环境变量在各进程内是独立的,即父子进程间变量无关联;
- 设定全局环境变量的进程所创建的子进程中,全局环境变量可见;
- 子进程只能暂时修改变量(包括删除),退出后父进程内变量不改变。
1 | $ # 在子shell中该变量不可见 |
以设置环境变量PATH
变量为例,用'$'
读取变量值,':'
作为分割符进行拼接
1 | $ echo $PATH |
若希望
PATH
变量持久化,将export
命令记录在以下几个文件中(无需全部记录)。
以下是shell默认的主启动文件,在每次登录Linux时执行(系统级),在Ubuntu系统中,该文件内部执行调用文件/etc/bash.bashrc
/etc/profile
以下四个文件作用相同,都是用户级的启动文件,一般大多数Linux发行版都只用到一到两个。shell会按照
.bash_profile
、.bash_login
、.profile
的顺序,执行第一个找到的文件(其余的被省略)。注意.bashrc
是在以上三个文件中被执行的。
$HOME/.bash_profile
$HOME/.bash_login
$HOME/.profile
$HOME/.bashrc
但是如果bash是作为交互式shell启动,只会检查执行
$HOME/.bashrc
,而/etc/profile
和$HOME/.profile
等均被忽略。
输入/输出重定向
通过输入/输出重定向,可将标准输入/标准输出重定向到另一个位置(如文件)。Linux将每个对象视作文件处理,用文件描述符(file descriptor)来标识文件对象。文件描述符是一个非负整数,每个进程一次最多可以有9个文件描述符。其中比较特殊的是标准输入(STDIN, 0)、标准输出(STDOUT, 1)、标准错误(STDERR, 2)。
执行时重定向
输入重定向
输入重定向是将文件内容重定向到命令,符号是'<'
,例如用wc
对文本进行计数
1 | $ wc < .bashrc |
还有一种是内联输入重定向(inline input redirection),符号是'<<'
,无需使用文件进行重定向,直接从stdin读取数据,必须指定一个文本标记来标记输入的开始和结尾。
1 | $ wc << EOF # 标记符,也可定义为其他文本 |
输出重定向
将命令输出发送到文件中,符号是'>'
,会覆盖已有数据,可以用'>>'
进行内容追加而不覆盖
注意,错误信息未被重定向。
1 | $ echo "hello!" > inputRedirection. txt |
错误重定向
一般错误输出和正常输出都会显示在屏幕上,但如果需要将错误信息重定向,则可通过指定文件描述符。例如重定向错误到文本err.logs
,而其余正常输出,可通过2>
指定文本文件
1 | $ wget 2> err.logs |
同时将正常输出重定向到文本out.logs
1 | $ wget 1> out.logs 2> err.logs |
若想同时重定向输出和错误到文本outerr.logs
,通过&>
指定
1 | $ wget &> outerr.logs |
脚本中重定向
输入/输出
在脚本中向文本描述符desc
输人/输出的命令如下,注意空格。
1 | command >&desc |
例如向标准错误STDERR
输出数据
1 |
|
如果执行时不指定错误重定向,将被默认打印到屏幕上(默认错误与输出打印到同一位置,即屏幕上)
1 | $ ./test.sh |
若指定错误重定向,即可输出到文本
1 | $ ./test.sh 2> err.logs |
自定义文件描述符
可通过exec
自定义文件描述符
1 | exec desc< filename # 从文件创建输入重定向 |
例如in.logs
原始文件内容如下
1 | $ cat in.logs |
编写脚本,从in.logs
创建输入输出重定向,并将文件描述符定义为3
1 |
|
1 | $ ./test.sh |
再次查看in.logs
文件内容
1 | $ cat in.logs |
又如,将STDIN, STDOUT, STDERR
均重定向到各自文件
1 |
|
1 | $ cat in.logs |
重定向到已有文件描述符
1 | exec descNew>&desc # 创建输出重定向 |
1 |
|
可以看到执行后,输出到3的数据也被显示到STDOUT中
1 | $ ./test.sh |
管道
管道可将一个命令的输出作为另一个命令的输入,是将第一个命令重定向到第二个命令,称为管道连接(piping)。Linux系统会同时调用多个命令,在内部将他们连接,而不是依次执行(管道通信)。例如,用apt-get
搜索openssl
安装包,排序sort
后通过less
查看
1 | $ apt search openssl | grep openssl* | sort | less |
变量
除了环境变量,shell支持在脚本中定义和使用用户变量,临时存储数据。
- 变量名可以由字母、数字和下划线组成,长度不超过20,首个字符不能以数字开头,区分大小写,不可使用保留关键字;
- 在赋值时同样地,赋值符两侧不能出现空格;
- shell脚本会自动决定变量值的数据类型,在脚本结束时所有用户变量被删除;
- 注意
'$'
的使用:引用变量值时需要,而引用变量进行赋值等操作时不需要。1
2
3
4
5
6
7
8
9
10
11$ var1=1; var2=2
$ echo var1 # var1被视作字符串
var1
$ echo $var1
1
$ var1=var2 # var1内容更改为字符串var2
$ echo $var1
var2
$ var1=$var2 # var1内容更改为变量var2的值
$ echo $var1
2 - 变量名外面的花括号界定符,加花括号是为了帮助解释器识别变量的边界,比如
1
2
3
4
5
6
7
8
9
10
11
12$ for name in Jack Tom Bob; do
> echo "This is $nameBoy" # nameBoy被视作变量名
> done
This is
This is
This is
$ for name in Jack Tom Bob; do
> echo "This is ${name}Boy" # name被视作变量名,自动拼接字符串
> done
This is JackBoy
This is TomBoy
This is BobBoy
字符串
字符串是shell编程中最常用最有用的数据类型,定义字符串时,可以选择单引号、双引号、无引号,但是有部分限制:单引号内引用变量值无效,且不能使用转义字符。
1 | $ name=louishsu |
字符串可进行拼接
1 | $ name=louishsu |
字符串长度、子字符串、查找字符串
1 | $ # 字符串长度 |
变量参数
以下介绍如何定义变量及删除变量
1 | $ # 未创建变量 |
数组参数
shell可使用数组
1 | $ # 定义数组变量 |
参数传递
位置参数
在执行脚本时,可将命令行参数传递给脚本使用,通过位置参数调用
1 |
|
1 | $ ./test.sh 1 2 3 |
命名参数
-
通过
shift
命令处理
调用一次shift
命令,$1
参数被删除,其余所有参数向左移动,即$2
移动到$1
,$3
移动到$2
中,以此类推。例如,某脚本需处理命令行参数-a -b 3 -c -d
,其中-b
为命名参数,则脚本如下编写1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while [ -n "$1" ] # 不可缺少引号""
do
case "$1" in
-a) echo "Option -a" ;;
-b)
echo "Option -b"
shift
echo "Value of option -b is: $1"
;;
-c) echo "Option -c";;
*) echo "Invalid parameters";;
esac
shift
done1
2
3
4
5$ ./test.sh -a -b 5 -c
Option -a
Option -b
Value of option -b is: 5
Option -c -
通过
getopt
命令处理getopt
命令简单使用格式如下1
getopt optstring parameters
例如解析
-a -b 3 -c -d
,指定optsting
为ab:cd
,其中:
表示该处包含参数值,在输出--
后的参数均视作位置参数1
2$ getopt ab:cd -a -b 5 -c -d 1 2 3
-a -b 5 -c -d -- 1 2 3配合
set
命令,将脚本原始的命令行参数解析1
set -- $( getopt -q ab:cd "$@" )
脚本如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
set -- $( getopt ab:cd "$@" )
while [ -n "$1" ] # 不可缺少引号""
do
case "$1" in
-a) echo "Option -a" ;;
-b)
echo "Option -b"
shift
echo "Value of option -b is: $1"
;;
-c) echo "Option -c";;
--) break ;;
*) echo "Invalid parameter: $1";;
esac
shift
done1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26$ ./test.sh -a -b 5 -c -d
Option -a
Option -b
Value of option -b is: 5
Option -c
Invalid parameter: -d
$ ./test.sh -a -b5 -cd
Option -a
Option -b
Value of option -b is: 5
Option -c
Invalid parameter: -d
$ ./test.sh -ab5 -cd
Option -a
Option -b
Value of option -b is: 5
Option -c
Invalid parameter: -d
$ # 但是如下失败
$ ./test.sh -ab5cd
Option -a
Option -b
Value of option -b is: 5cd
用户输入
read
命令可提供用户输入接口,从标准输入或文件描述符中接受输入,实现脚本可交互。
基本输入: read
read
可指定多个变量,将输入的每个数据依次分配给各个变量,若变量数目不够则将剩余数据全部放入最后一个变量,如下
1 | $ read first last age |
指定-p
,可输出命令提示符
1 | $ read -p "Who are you? " first last age |
指定-t
进行超时处理
1 | $ read -t 5 first last age # 5秒 |
指定-s
,隐藏输入
1 | $ read -s -p "Enter your passwd: " passwd |
文件输入: cat | read
配合cat
指令,通过管道,实现文件输入
1 | $ cat test.txt | while read line; do |
或者通过重定向实现。
脚本退出: exit
shell中运行的命令都使用退出状态码(exit status)作为运行结果标识符,为0~255的整数,可通过$?
查看上个执行命令的退出状态码。按照惯例成功运行命令后的退出状态码为0,常用的如下
状态码 | 描述 |
---|---|
0 | 命令成功执行 |
1 | 一般性未知错误 |
2 | 不适合的shell命令 |
126 | 命令不可执行 |
127 | 未查找到命令 |
128 | 无效的退出参数 |
128+x | 与linux信号x相关的严重错误 |
130 | 通过ctrl+c终止的命令 |
255 | 正常范围之外的退出状态码 |
shell脚本会以最后一个命令的退出码退出,用户也可通过exit
命令指定。注意若退出结果超过255,会返回该值对256的模。
1 | $ # 正常退出 |
命令替换: ( command )
shell脚本最有用的特性是将命令输出赋值给变量,有两种方法可以实现
- 反引号字符
'
( command )
格式,$
进行取值
例如,以时间信息创建文件
1 | $ time=$(date +%y%m%d) # 或 time=`date +%y%m%d` |
运算和测试
数学运算
$( expr expression )
仅支持整数运算。支持逻辑操作符|, &
、比较操作符<, <=, >, >=, =, !=
、运算操作符+, -, *, /, %
(注意乘号符需进行转义\*
)。
1 | $ var1=4; var2=5 |
此外还支持部分字符串操作
$[ expression ]
用[ operation ]
格式将数学表达式包围,$
进行取值,此时乘号符无需进行转义。支持高级运算,如幂运算**
、移位运算>>, <<
、位运算&, |, ~
、逻辑运算&&, ||, !
等
1 | $ var1=4; var2=5 |
let expression, $(( expression ))
let expression
等价于(( expression ))
,都支持一次性计算多个表达式,以最后一个表达式的值作为整个命令的执行结果。不同之处是,let
以空格作为分隔符,(())
以,
作为分隔符。显然前者没有后者灵活。 同样的,(( expression ))
用$
进行表达式的取值。
1 | $ var1=4; var2=5 |
可快速实现变量自增、自减操作
1 | $ i=0 |
内建计算器bc
内建计算器支持浮点运算,实际上是一种编程语言,bash计算器能识别
- 数字(整数、浮点数)
- 变量(简单变量、数组)
- 注释(
#
或/* */
格式) - 表达式
- 编程语句(如
if-then
) - 函数
浮点运算的精度通过内建变量scale
控制,表示保留的小数位数,默认值是0
1 | $ bc |
在脚本中使用bc
命令有两种方式
-
单行运算:
通过命令替换和管道实现,格式为
variable=$( echo "options; expression" | bc )
例如1
2
3
4$ var1=4; var2=5
$ var3=$( echo "scale=2; $var1 / $var2" | bc )
$ echo $var3
.80 -
多行运算:
通过命令替换和内联输入重定向实现,格式为1
2
3
4
5
6variable=$(bc << EOF
options
statements
expressions
EOF
)需要注意的是,
bc
内部变量和shell变量是独立的,变量名可重复使用,例如1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33$ var3=$(bc << EOF
> scale=2
> $var1 / $var2 # 引用shell变量
> EOF
> )
$ echo $var3
.80 # 输出shell变量运算结果
$ var3=$(bc << EOF
> scale=2
> var1=5; var2=4 # 重新定义变量
> var1 / var2
> EOF
> )
$ echo $var3
1.25 # 输出bc变量运算结果
$ echo $var1 # 不会修改shell变量
4
$ echo $var2
5
$ var3=$(bc << EOF
> scale=2
> var1=5; var2=4 # 重新定义变量
> $var1 / $var2 # 引用shell变量
> EOF
> )
$ echo $var3
.80 # 输出shell变量运算结果
$ echo $var1 # 不会修改shell变量
4
$ echo $var2
5
测试命令: test expression, [ expression ]
测试命令用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试,还可进行复合测试,可通过test
命令或[ option ]
实现
数值测试: -eq, -ne, -gt, -ge, -lt, -le
参数 | 说明 |
---|---|
-eq | 等于则为真 |
-ne | 不等于则为真 |
-gt | 大于则为真 |
-ge | 大于等于则为真 |
-lt | 小于则为真 |
-le | 小于等于则为真 |
1 | $ var1=4; var2=5 |
字符测试: =, !=, <, >, -n -z
参数 | 说明 |
---|---|
= | 等于则为真 |
!= | 不等于则为真 |
< | 小于则为真 |
> | 大于则为真 |
-n | 长度非0或未定义,则为真 |
-z | 长度为0则为真 |
注意:
- 大于号
>
和小于号<
必须转义,否则被视作重定向符,字符串值视作文件名; - 大写字母被认为是小于小写字母的。
1 | $ var1="Test"; var2="test" |
注意,若在比较数值时采用<, >
等符号,会将数值视作字符串,同样也存在未转义识别为重定向符的问题
1 | $ if [ 4 > 5 ]; then |
文件测试: -e, -d, -f, …
参数 | 说明 |
---|---|
-e file | 如果文件存在则为真 |
-d file | 如果文件存在且为目录则为真 |
-f file | 如果文件存在且为普通文件则为真 |
-s file | 如果文件存在且至少有一个字符则为真 |
-c file | 如果文件存在且为字符型特殊文件则为真 |
-b file | 如果文件存在且为块特殊文件则为真 |
-r file | 如果文件存在且可读则为真 |
-w file | 如果文件存在且可写则为真 |
-x file | 如果文件存在且可执行则为真 |
-O file | 如果文件存在且属于当前用户所有则为真 |
-G file | 如果文件存在且默认组与当前用户相同则为真 |
file1 -nt file2 | 文件1比文件2新则为真 |
file1 -ot file2 | 文件1比文件2旧则为真 |
复合条件测试: !, -o / ||, -a / &&
运算符 | 说明 | 举例 |
---|---|---|
! | 非运算,表达式为 true 则返回 false,否则返回 true。 | [ ! false ] 返回 true。 |
-o / || |
或运算,有一个表达式为 true 则返回 true,满足就近原则,即运算符前表达式为真则跳过后一表达式 | [ condition1 -o condition1 ] 或 [ condition1 ] || [ condition1 ] |
-a / && | 与运算,两个表达式都为 true 才返回 true。 | [ condition1 -a condition1 ] 或 [ condition1 ] && [ condition1 ] |
1 | $ if [ $var1 -le $var2 -o $var3 -le $var4 ]; then |
结构化命令
分支
if-then-elif-else-fi
完整的if-then
语句如下
1 | if condition/command |
注意,if
后可接命令或测试语句,当所接命令退出码为0时判定为真,测试语句逻辑为真时判定为真。
1 | $ if pwd; then |
支持针对字符串比较的高级特性,如模式匹配,使用[[ expression ]]
1 | $ if [[ $USER == l* ]]; then # 双等号 |
case-in
多选择语句,可以用case
匹配一个值与一个模式,如果匹配成功,执行相匹配的命令。取值将检测匹配的每一个模式。一旦模式匹配,则执行完匹配模式相应命令后不再继续其他模式。如果无一匹配模式,使用星号 * 捕获该值,再执行后面的命令。完整格式如下
1 | case variable in |
1 | $ var=3 |
循环
for-do-done
-
迭代
用于迭代列表,
in
列表是可选的,如果不用它,for循环使用命令行的位置参数。在迭代结束后,variable
保存itemN
的值且在不修改的情况下一直有效。1
2
3
4for variable in item1 item2 ... itemN # 注意无`()`
do
commands
done以输出数字列表为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15$ for number in 1 2 3; do
> echo "The number is $number"
> done
The number is 1
The number is 2
The number is 3
$ nums=(1 2 3)
# $ for number in $nums; do # 一种错误做法,只会输出1
$ for number in ${nums[*]}; do # 迭代数组
> echo "The number is $number"
> done
The number is 1
The number is 2
The number is 3迭代字符串与数组有所不同
1
2
3
4
5
6
7
8$ str="I am louishsu"
$ for wd in $str; do # 迭代字符串
# $ for wd in ${str[*]}; do # 同上,也可迭代字符串
> echo $wd
> done
I
am
louishsu还可迭代输出命令结果、通配符等,
in
后可接多个命令或目录1
2
3
4
5
6
7
8
9
10
11
12
13
14$ for file in $( ls; pwd ); do
> echo "$file"
> done
Downloads
anaconda3
backup
/home/louishsu
$ for file in /home/louishsu/*; do
> echo $file
> done
/home/louishsu/Downloads
/home/louishsu/anaconda3
/home/louishsu/backup -
C/C++风格
1
2
3
4for (( variable assignment ; condition ; iteration process ))
do
commands
done注意
- 变量赋值可带等号;
condition
中变量不需$
;- 可同时定义两个变量。
1
2
3
4
5for (( i=0, j=0; i<3 && j<4; i++, j+=2 )); do
> echo $i, $j
> done
0, 0
1, 2
while-do-done
基本格式如下,在condition
为假时停止循环
1 | while condition |
1 | $ var=0 |
until-do-done
基本格式如下,与while
相反,在condition
为真时停止循环
1 | until condition |
1 | $ var=0 |
循环控制: break, continue
循环控制语句,包括break/continue
,作用同C/C++或Python,不做过多介绍
1 |
|
1 |
|
函数
创建和调用函数
创建函数格式如下,注意函数名唯一,且shell中的函数支持递归调用
1 | function func { |
调用函数时,在行中指定函数即可,但是函数定义必须在调用之前
1 | commands |
参数传递
作用域: local
默认情况下,脚本中定义的任何变量都是全局变量(包括函数体内定义的变量),可以在函数体中读取全局变量进行操作
1 |
|
1 | $ ./test.sh |
在函数体内可定义局部变量,使用local
关键字,注意
- 局部变量在函数体外不可见;
- 即使声明相同名称的局部变量,shell也会保证两个变量是分离的。
1 |
|
1 | $ ./test.sh |
变量参数
类似shell脚本的参数传递,函数同样使用标准的参数环境变量进行参数传递,用$0
表示函数名,$1, $2, ...
表示参数,用$#
获取参数数目,用$*/$@
获取全部参数。
由于函数使用特殊参数环境变量进行参数传递,因此无法直接获取脚本在命令行中的参数值,两者不关联。
1 |
|
1 | $ ./test.sh 1 2 3 |
数组参数
与函数传递数组,不能简单通过数组名进行;利用命令替换获取返回数组。
1 |
|
1 | $ ./test.sh |
返回值: return, echo
-
默认退出状态码
若函数未指定返回语句return
,则执行结束后标准变量$?
内存储函数最后一条命令的退出码状态。 -
指定返回值
使用return
退出函数并返回指定的退出状态码,同样地保存在标准变量$?
中,但是用这种方式获取返回值需要注意以下两点- 函数退出后立即取返回值,防止被覆盖;
- 退出码范围是0~255;
- 若函数中命令执行错误导致提前退出函数,则此时
$?
中为错误状态码,不可作为函数输出。
1
2
3
4
5
6
7
8
function add {
return $[ $1 + $2 ]
}
var1=4; var2=5
add $var1 $var2
echo "$var1 + $var2 = $?"1
2$ ./test.sh
4 + 5 = 9 -
用命令替换获取函数输出作为返回值
这种方式可以避免与状态码复用,还可以返回如浮点、字符串等类型1
2
3
4
5
6
7
8
function add {
echo "$[ $1 + $2 ]"
}
var1=4; var2=5
sum=$( add $var1 $var2 )
echo "$var1 + $var2 = $sum"注意到,函数中的
echo
并没有输出到STDOUT
中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17$ ./test.sh
4 + 5 = 9
```
# 文件包含: source
用`source`命令在当前shell上下文中执行命令,而不是创建新shell,其快捷别名为**点操作符**(dot operator)
例如创建函数脚本`funcs.sh`
``` bash
#!/bin/bash
function add {
echo "$[ $1 + $2 ]"
}
function sub {
echo "$[ $1 - $2 ]"
}
在test.sh
中调用函数
1 |
|
1 | $ ./test.sh |
总结
- 注意区分各类括号的使用
- 变量取值:
${ variable }
- 命令替换:
$( command )
- 整数计算:
$[ expression ]
- 多行整数计算:
$(( expression1, expression2, ... ))
- 测试:
[ expression ]
- 高级字符串比较测试:
[[ expression ]]
- 变量取值:
- 注意数值比较和字符串比较的差异
- 重定向中符号的使用
- 注意函数参数的传递