Shell编程基础 - 从此成为Shell达人【1】
在刚工作那会负责集成发版、各种车端启动脚本等,完整的看了《Linux Shell核心编程指南》1这本书,算系统学习Shell编程,还自诩为Shell达人。如今换了工作,不再特别需要这项技能,曾经学习的Shell编程基本已经遗忘,此次快速的复习下并且记录成此文。
变量
变量定义
Shell变量的类型只能是字符串,所以定义变量时不需要指定类型。定义一个变量语法如下:
var=123 # 定义var为123
var="1 2 3" # 双引号将其内字符看成一个整体
unset var # 删除变量
有如下几点需要注意:
- 变量未被定义时定义,已经被定义则可以被重复赋值。类似
python
对变量的定义。 - 定义变量时等号两边不可有空格。
- 双引号将其内字符看成整体,不会被空格、换行符等中断。推荐定义变量时使用双引号。
变量引用
当需要引用变量时可在变量名前加$
符号,当变量名与其他非变量名字符混在一起时,需要{}
分隔。语法如下:
bash-3.2$ var="123"
bash-3.2$ echo $var
123
bash-3.2$ echo "$var"
123
bash-3.2$ echo "$var4"
bash-3.2$ echo "${var}4"
1234
bash-3.2$ echo "\${var}4"
${var}4
bash-3.2$ echo '${var}4'
${var}4
有如下几点需要注意:
line4在引用var变量前后加上双引号,当被引用的变量含有空格/换行符/制表符被看成一个整体。也就是双引号使得变量在作为命令输入时作为一个参数,而不是被空格分隔的几个参数。所以在大部分情况需要使用双引号。
如下代码,line2不使用双引号,导致传给
touch
命令的参数被空格分为了三个创建了三个文件,而line3引用变量使用引号使其作为一个参数传给touch
命令创建一个文件。:bash-3.2$ var="1 2 3" bash-3.2$ touch $var # 创建三个文件,为1 2 3 bash-3.2$ touch "$var" # 创建一个文件,为"1 2 3"
line6打印了不存在的变量var4,结果为空。在Shell中引用未被定义的变量结果为空。
line8使用
{}
分隔变量名和字符字面量,可以正确引用到var变量。所以在Shell中变量名和其它字符混在一起时,应当使用{}
分隔。line10展示了可以使用转义符
\
,屏蔽变量引用$
符号。line12展示了单引号可以屏蔽内部的所有转义符,不会进行变量替换、命令替换等特殊操作。
内置变量
下面是Shell中常见系统内置变量的表格:
类别 | 变量 | 描述 |
---|---|---|
环境变量 | $HOME | 当前用户的主目录路径 |
$PATH | 可执行程序的搜索路径列表,用 : 分隔 | |
$PWD | 当前工作目录的路径 | |
$OLDPWD | 之前的工作目录路径,可以用 cd - 切换回去 | |
$RANDOM | 返回0到32767的随机整数 | |
$PPID | 返回当前Shell进程的父进程,在从音频播放C++类的实现学习Linux系统子进程管理中有应用 | |
进程和脚本相关 | $? | 上一个命令的退出状态码,0为正常退出,其它为异常退出 |
$$ | 当前Shell进程的进程 ID (PID) | |
$! | 最后一个后台进程的 PID | |
$0 | 当前脚本的文件名 | |
$n | 当前脚本参数$1为第一个参数,以此类推 | |
$# | 传递给脚本或函数的参数个数 | |
$* | 传递给脚本或函数的所有参数,作为一个整体的字符串 | |
$@ | 传递给脚本或函数的所有参数,每个参数独立 | |
特殊符号变量 | $- | 当前Shell的选项标志(如 -i 表示交互模式) |
$_ | 上一个命令的最后一个参数或最后一次使用的参数 |
命令替换
反引号``和$()
都可以用作命令替换,将命令的输出结果作为字符串返回。如下示例,创建以当前年月日作为文件名的文件。
bash-3.2$ touch "$(date +%Y%m%d).txt"
bash-3.2$ touch "`date +%Y%m%d`.txt"
反引号``和$()
二者的区别在于$()
支持嵌套功能,而反引号不支持。所以推荐用$()
而不是反引号``。
变量作用域
首先需要了解,普通方法执行外部命令或者Shell脚本都会在子进程(SubShell)中执行,而使用source
或.
点命令来执行Shell脚本可以在当前Shell进程中执行脚本内容,就如同在Shell中一行一行输入脚本的命令。.
命令在一些低版本的Shell中不支持。使用方法如下:
bash-3.2$ . test.sh
bash-3.2$ source test.sh
而默认情况下,当前Shell进程所定义的变量不对其它进程可见。也就是在当前Shell中执行bash test.sh
,在test.sh
中引用不了当前Shell定义的变量,而source test.sh
由于在当前Shell中执行所以可以引用当前Shell定义的变量,当然在脚本中定义的变量可以被当前Shell所引用。如下例子。
bash-3.2$ var="123"
bash-3.2$ cat test.sh
echo "$var"
bash-3.2$ bash test.sh
bash-3.2$ source test.sh
123
使用export
修饰定义的变量可以被子进程引用,但是不能被父进程引用。如下,在执行export.sh
的子进程可以引用父进程被定义为export
的var
。但是子进程定义export
的var_inside
却不能被父进程所引用。子进程会继承父进程的一些属性,但是子进程很难“影响”到父进程。
bash-3.2$ cat export.sh
echo "$var"
export var_inside=123
bash-3.2$ export var="123"
bash-3.2$ bash export.sh
123
bash-3.2$ echo "$var_inside"
综上可以解释为何修改~/.bashrc
后需要执行source ~/.bashrc
,并且在~/.bashrc
定义的环境变量需要export
修饰。
在Shell中以下执行方法会在子进程(SubShell)中执行,包括如下:
执行外部命令。也就是除去一些Shell内部(built-in)命令,例如
cd
、alias
等,除去Shell的变量定义、循环、函数等语句。普通方式执行Shell脚本。
source
执行则不会创建子进程。执行含有管道的命令组,即使全部是内部命令也会在子Shell中执行。由于管道需要将标准输入和输出进行重定向,需要
fork
出子进程才能实现该功能。使用
&
将命令放置后台执行也会被放入子Shell中执行。由于在后台执行,天然不能放在当前Shell中执行,否则会阻塞当前的Shell进程。使用分组命令
()
和$()
执行命令也会在子Shell中执行。也就意味着上述执行方法需要注意变量作用域的问题,作为被执行的命令内部引用不到父Shell中定义的非
export
变量,而被在执行命令中定义的变量也不会被引入到父Shell中。
算数运算
整数运算
Shell中有如下算数运算的语法可以对整数进行运算,注意不可计算小数。
$(( 表达式 ))
:这种语法是
POSIX
标准的一部分,兼容性更好,推荐使用。bash-3.2$ x=2 bash-3.2$ echo $(( x+=2 )) 4
有如下几点需要注意:
- 表达式中引用变量不需要加
$
符号。 - 表达式两边不需要有空格,但是为了可读性最好加上空格。
- 表达式中引用变量不需要加
$[表达式]
:这种语法的用法和上边类似,虽然语法上更加简单但未被标准所支持,所以不推荐使用。
let
:该命令会进行运算但是不导出运算结果,需要后续语句从保存运算结果的变量中取值。
bash-3.2$ let x=10 bash-3.2$ let x++ bash-3.2$ echo $x 11
下面列出Shell支持的运算符号。
符号 | 含义描述 |
---|---|
++ | 自加1 |
– | 自减1 |
+ | 加法 |
- | 减法 |
* | 乘法 |
/ | 除法 |
** | 幂运算 |
% | 取余(求模) |
+= | 自加任意数 |
-= | 自减任意数 |
*= | 自乘任意数 |
/= | 自除任意数 |
%= | 对任意数取余 |
&& | 逻辑与 |
|| | 逻辑或 |
>、>= | 大于、大于等于 |
<、<= | 小于、小于等于 |
表达式?表达式:表达式 | 三元运算符 |
小数运算
小数计算Shell原生不支持,需要用其它命令来支持,例如bc
命令。例如:
bash-3.2$ echo "scale=2; 3/4" | bc
.75
其中scale可以指定保留几位小数,默认仅展示整数部分。
数组
数组定义
在Shell中支持索引数组和关联数组,其实严格意义上讲二者都是map: 索引数组是key类型为整数,value类型为字符串的map;关联数组是key类型为字符串,value类型为字符串的map,分别有如下特点。
索引数组
索引数组可以直接使用
数组名[索引号]=val
定义,索引号无需连续,故其严格讲是个map而不是array。bash-3.2$ name[1]="hello" bash-3.2$ name[2+2]='!' bash-3.2$ name[2]="world" bash-3.2$ echo "${name[*]}" # 取数组所有的值,按照索引从小到大排序,必须使用{} hello world ! bash-3.2$ echo "${#name[@]}" # 取数组数量 3 bash-3.2$ echo "${#name[*]}" 3
可以一次性初始化多个元素,不能指定每个元素的索引,默认以0为起始索引递增。
bash-3.2$ name=(hello world '!') bash-3.2$ echo ${name[0]} hello
还可以将其它命令输出字符串赋值给数组变量,数组每个元素是将输出用空格、换行符或制表符等分割的字符串。
bash-3.2$ files=($(ls)) bash-3.2$ echo ${files[*]} echo.sh exit.sh export.sh heredocument.sh range.txt replace.txt test.sh test.txt
关联数组
关联数组就可以使用字符串作为key,定义前需要用
declare -A 数组名
声明变量为关联数组。另外声明的变量自动拥有local
属性,作用域只在局部而不是默认的全局。从bash 4.0版本后才支持。定义方式如下,和索引数组类似有两种形式:
~/Documents/project/bash » declare -A man ~/Documents/project/bash » man[name]=tom ~/Documents/project/bash » man[age]=26 ~/Documents/project/bash » echo ${man[*]} 26 tom ~/Documents/project/bash » declare -A woman ~/Documents/project/bash » woman=([name]=lucy [age]=25) ~/Documents/project/bash » echo ${woman[*]} 25 lucy
数组遍历
首先需要区分@
和*
对数组的区别,如下:
${数组名[@]}
: 将数组元素视为若干个体,如同不使用双引号括起字符串。${数组名[*]}
: 将数组元素视为一个整体,如同使用双引号括起字符串。
故可以使用${数组名[@]}
遍历数组的值。
~/Documents/project/bash » for i in ${woman[@]}; do
\ echo $i
\ done
25
lucy
当然也可以通过索引(key)来遍历数组,如下语法可以取数组索引的列表,这里*
和@
都将结果视为若干个体,没有区别。
echo ${!woman[@]}
echo ${!woman[*]}
函数
函数定义与调用
在Shell中定义函数的语法有多种,如下都是合法的。个人推荐最后一种方式。使用unset
可以取消函数定义。
func1() {
# 代码序列
}
function func2 {
# 代码序列
}
function func3() {
# 代码序列
}
函数的调用方法和命令的执行方法类似,不需要括号()
,调用时也可以将参数通过空格间隔传入,在函数内部可以通过$1
、$2
等获取调用方传入的参数。当然也可以使用$@
获取所有的位置参数。如下示例。
bash-3.2$ cat test.sh
#!/bin/bash
function func() {
echo "$*"
}
func 1 2 3 # 函数调用
bash-3.2$ ./test.sh
1 2 3
函数变量作用域
在Shell中调用函数不会开启子进程,故在函数内部和在外部定义和使用变量的效果相同。函数外部的变量函数内部可以直接调用,函数内部定义的变量在函数执行后也可以被外部引用。在函数内部定义变量使用local
修饰,可以使变量作用域只在函数内部,且和全局同名变量也不会导致意外修改全局变量,并且其在函数内部引用优先级高于全局变量。
使用local
定义的变量作用域和一般的现代编程语言一致,故在函数内部定义的变量应该使用local
修饰,来保证不会影响到全局变量。
函数返回值
Shell函数的返回值是一个整数,逻辑上表明函数是否执行成功。默认函数的返回值为函数内部执行最后一个命令的返回值。也可以使用return
关键字返回指定整数(有效范围为0~255)作为返回值。调用方可以使用$?
来获取该返回值。
当然一般我们希望函数能返回更多信息,其实在函数中echo
各种字符可以作为传统意义上的“返回值”。可以使用$()
引用函数echo
出来的“返回值”。
bash-3.2$ function func() {
> echo "$*"
> }
bash-3.2$ func_result=$(func 1 2 3) # 将函数echo的返回值赋值给变量
bash-3.2$ echo $func_result
1 2 3
判断
if语句
条件if
语句基本的语法格式如下:
if 命令; then
命令序列
elif 命令; then
命令序列
else
命令序列
fi
需要注意的是if
的条件是判断命令是否正常结束,即判断命令的退出码是否为0正常结束。即如下示例。
if ls nofile; then
echo true
else
echo false
fi
一般的编程语言在if
判断条件是逻辑表达式是否为true,Shell中提供test
、[ ]
和[[ ]]
条件测试命令,支持将传入的逻辑表达式是否为true转换成该命令是否正常结束,从而支持逻辑表达式。
条件测试命令
在Shell中支持test
、[ ]
和[[ ]]
条件测试命令,可以进行字符串或数字的比较、测试文件属性等。测试的结果通过命令是否正常退出返回,可以在if
表达式中使用,也可以通过$?
获取测试结果。
test
和[ ]
是POSIX标准的测试语句兼容性强,而[[ ]]
功能更强大一些,兼容性稍差,但是也被主流解释器所支持,所以更推荐使用[[ ]]
命令。有如下[[ ]]
支持的功能,而test
和[ ]
不支持。
直观符号支持
[[ ]]
支持<
和>
符号,一般采用locale语言排序,使用LANG=C设置在排序时使用ASCII码;支持逻辑与&&
和逻辑或||
符号,而test
和[ ]
需要使用-a
和-o
表达对应符号意思。所以使用[ ]
非常蹩脚,不推荐。模式匹配支持
[[ ]]
中==
支持模式匹配,允许使用通配符,例如,*
、?
、[...]
等。如下示例:[[ $name == J* ]] # 判断name变量是否以J开头 [[ $name == J?cob ]] # 判断name变量是否在J和cob之间有任意单个字符 [[ $age == [0-9] ]] # 判断age变量是否为数字
正则匹配支持
[[ ]]
中支持使用=~
进行正则匹配,如下示例:[[ $age =~ [0-9]]] # 变量是否**含有**数字
分组()测试支持
[[ ]]
支持分组()测试,可以改变优先级,例如:[[ a == a && (b == b || c == d) ]] # 先判断括号中的结果
另外需要特别注意,左右方括号必须要有空格,并且比较符号两边也必须有空格。
以下是[[ ]]
命令支持的判断与比较。
字符判断
条件 描述 [[ $str1 == $str2 ]]
当 str1
和str2
相等时为真,支持通配符匹配[[ $str1 != $str2 ]]
当 str1
和str2
不相等时为真[[ -z $str ]]
当 str
为空时为真(zero)[[ -n $str ]]
当 str
非空时为真(not zero)[[ $str == pattern ]]
当 str
与pattern
匹配时为真[[ $str =~ regex ]]
当 str
匹配正则表达式regex
时为真整数判断
条件 描述 [[ $num1 -eq $num2 ]]
当 num1
等于num2
时为真[[ $num1 -ne $num2 ]]
当 num1
不等于num2
时为真[[ $num1 -lt $num2 ]]
当 num1
小于num2
时为真[[ $num1 -le $num2 ]]
当 num1
小于等于num2
时为真[[ $num1 -gt $num2 ]]
当 num1
大于num2
时为真[[ $num1 -ge $num2 ]]
当 num1
大于等于num2
时为真文件属性判断
条件 描述 [[ -e $file ]]
文件存在时为真 [[ -f $file ]]
普通文件存在时为真 [[ -d $file ]]
目录存在时为真 [[ -L $file ]]
符号链接存在时为真 [[ -b $file ]]
块设备文件存在时为真 [[ -c $file ]]
字符设备文件存在时为真 [[ -p $file ]]
命名管道(FIFO)存在时为真 [[ -r $file ]]
文件可读时为真 [[ -w $file ]]
文件可写时为真 [[ -x $file ]]
文件可执行时为真 [[ -s $file ]]
文件存在且大小大于 0 时为真
另外[[ ]]
支持逻辑取反符号!
,如下示例:
if [[ ! -z $str ]]; then
echo "字符串非空"
fi
命令控制语句
如果需要再一行代码中输入多条语句,可以使用;
、&&
和||
将多个命令分隔。其中:
- 分号
;
: 按照顺序执行,前后语句没有逻辑关系。 - 逻辑与
&&
: 仅当前一条执行成功,才会执行后一条语句,并且两个语句都成功才认为整行语句执行成功。例如,A命令&&B命令
,只有A命令执行成功才会执行B命令,并且两条命令都成功才会认为整行命令成功。 - 逻辑或
||
: 仅当前一条执行失败,才会执行后一条语句,并且两个语句其一成功就认为整行语句执行成功。例如,A命令||B命令
,只有A命令执行失败才会执行B命令,并且两条命令只要有一条执行成功就认为整行命令执行成功。
通过使用&&
和||
可以实现一行代码的if\else逻辑,比较推荐。如下示例,判断当前用户是否为root,需要注意&&
和||
先后问题,写反了意义就不同了。
[[ $USER == root ]] && echo true || echo false
# 错误的写法: [[ $USER == root ]] || echo true && echo false
虽然上述写法很优雅,但是本人曾经踩过一次坑。如果需要条件满足就执行某个命令,条件不满足则不执行任何命令逻辑,就会写出如下的代码。
[[ $USER == root ]] && echo true
默认情况下上述代码可以正确运行,但是一般Shell脚本会开启set -e
选项,脚本中命令执行失败就会结束不再继续执行,防止错误继续传递。而对于这种写法,在条件为false时整行语句就会被认为执行失败导致脚本意外退出。故即使不需要else也要加上,可以写成如下形式(:
命令为空命令)。
[[ $USER == root ]] && echo true || :
case语句
Shell的case
语句类似于一般语言中的switch/case
语句,但是提供了更多方便的用法。基本的语法如下。
case word in
模式1)
命令序列
;;
模式2|模式3)
命令序列
;;
*)
命令序列
;;
esac
case
命令会对word
关键词匹配下面的每个模式,模式支持多个条件匹配用|
隔开。
在命令序列需要使用结束符来表明之后的动作,支持如下三种结束符:
;;
: 类似C/C++中的break
语句不再继续匹配。;&
: 继续执行下一个模式匹配的命令,在C/C++的switch
语句中的默认行为。;;&
: 继续匹配下边的模式匹配,如果匹配就执行对应的命令序列。
模式匹配还支持一些特殊符号。如下表。
特殊字符 | 描述 |
---|---|
变量展开$ 、命令展开$() 等 | 展开功能 |
* | 匹配任意字符,例如*.txt) 匹配以txt为结尾的字符串 |
? | 匹配单个字符 |
[...] | 匹配括号内的任意单个字符,可以使用- 表示连续的字符集 |
扩展通配符 | 后续文章介绍 |
举个如下的case
语句使用例子,可以匹配y
、yes
和n
、no
不区分大小写。
read -p "continue? (y|n)" key
case $key in
[Yy]|[Yy][Ee][Ss]):
echo "yes"
;;
[Nn]|[Nn][Oo]):
echo "no"
;;
*)
echo "wtf"
;;
esac
select语句
select
语句支持设置多个选项,执行到该语句时会进入交互模式,等待用户输入编号。比较鸡肋。
bash-3.2$ cat select.sh
select item in "cpu" "ip"; do
echo $item
done
bash-3.2$ bash select.sh
1) cpu
2) ip
#? 1
cpu
#? 2
ip
#? 3
#? ^D
循环
for语句
在Shell中for语句有下面基本的语法格式。
for var [ in 字符串 ]; do
命令序列
done
有如下需要注意的地方:
可以忽略
in
和后边的字符串,此时默认取值为$@
,遍历当前作用域的参数。字符串按照空格、制表符、换行符($IFS指定)分割成子字符串,循环赋值给var。如下几种方式可以产生这样的字符串。
- 字面量: 直接写类似
for var in 1 2 3;do echo $var; done
,注意字符串如果被双引号包围,则代表一个整体,只会遍历一次。 - 花括号扩展: 例如
{1..5}
可以产生1~5以空格分隔的字符串;{1..10..2}
产生以1开始最大到10步长为2的字符串。但是其不可以引用变量,即{1..$i}
是非法的。后续文章系统介绍这些Shell的扩展。 seq
命令: 其可以生成序列,并且作为命令可以引用其它变量。例如:for var in ${seq 1 10}; echo $var; done
。- 数组: 可以使用
${array[@]}
来获取以空格分隔的字符串。
- 字面量: 直接写类似
另外Shell还支持C语言风格的for循环。如下,语法和C语言略有不同。其定义的i
变量在for
语句执行完后还可以引用。
for ((i=0; i<=5; ++i>));do
echo $i
done
while和until语句
使用while语句也可以遍历,其语法格式如下。
while 命令; do
语句序列
done
需要注意的是while语句中的命令和if
中的类似,推荐使用[[ ]]
格式的命令作为条件。如下示例,打印0~10。
i=0
while [[ $i -le 10 ]];do
echo "$i"
let ++i
done
until语句比较鸡肋,和while语句的区别在于: until循环语句在条件判断结果为true才结束循环,而while则是判断结果为false时结束。
中断与退出控制
Shell的循环也支持break
、continue
用于中断循环和中断此次循环。
不同于C语言,这两个命令后边可以跟数字参数,数字要求大于等于1,表示对第几层循环执行break
或continue
操作。
类似exit()
系统调用结束进程,Shell中也支持exit
命令结束Shell进程,可以指定退出码。