shell-编程的基本元素
文中所有需要赋予执行权限的脚本文件,请自行使用 chmod +x
添加。
变量
1 |
|
由这个例子可以看出,shell 语言中的一切变量都是字符串类型的。
shell 中有 3 种变量:用户变量、位置变量和环境变量。其中用户变量在编程过成功使用最多,位置变量在对参数判断和命令返回判断时会使用,环境变量主要是在程序运行时需要设置。
用户变量
就是用户在 shell 编程过程中定义的变量,分为全局变量和局部变量。默认情况下,用户定义的 shell 变量为全局变量,如果要指定局部变量,则需使用 local
限定词。
shell 中的特殊字符号
Linux Shell 中的特殊字符
特殊字符 | 含义 |
---|---|
~ | 主目录,相当于$HOME |
` | 命令替换符,例如 pwd 返回 pwd 命令执行的结果字符串 |
# | shell 脚本中的注释 |
$ | 变量表达式符号 |
& | 后台作业,将此符号置于命令末端,则让命令于后台运行 |
* | 字符串通配符 |
( | 启用子 shell |
) | 停止子 shell |
\ | 转义下一个字符 |
| | 管道 |
[ | 开始字符集通配符 |
] | 结束字符集通配符 |
{ | 开始命令块 |
} | 结束命令块 |
; | shell 命令分隔符 |
‘ | 强引用 |
“ | 弱引用 |
< | 输入重定向 |
> | 输出重定向 |
/ | 路径名目录分隔符 |
? | 单个人一些字符 |
! | 管道逻辑 NOT |
示例:
1 |
|
强引用和弱引用
1 |
|
双引号中的变量是弱引用,单引号中的变量是强引用。
变量语法的真实面目
变量的标识方式 $varname
实际上是常用语法 ${varname}
的简略模式。
为什么会有这两种不同的语法呢?原因有二:
如果代码中的位置参数超过
9
个,第十个参数必须要用语法${10}
而不是$10
。如果要在用 ID 后面放置一个下划线,例如
echo $UID_
,则 shell 会试图使用UID_
作为变量名。因此,在这里 shell 分不清到底UID
是变量还是UID_
是变量。正确的写法是echo ${UID}_
,如果变量名后面跟的是一个非小写字符、数字或下划线,则使用第一种写法就没问题。
字符串操作
大括号操作符允许我们使用 shell 字符串操作的更多高级功能,即字符串处理运算符。字符串处理运算符允许你完成如下操作:
确保变量存在且有值
设置变量的默认值
捕获未设置变量而导致的错误
删除匹配模式的变量的值部分内容
替换运算符
变量运算符 | 替换 |
---|---|
${varname:-word} | 如果 varname 存在且非 null,则返回 varname 的值;否则,返回 word (*如果变量未定义,则返回默认值 word *) |
${varname:=word} | 如果 varname 存在且非 null,则返回 varname 的值;否则将其置为 word ,然后返回其值(*如果变量未定义,则设置变量为默认值 word* ) |
${varname:?message} | 如果 varname 存在且非 null,则返回 varname 的值;否则打印 message ,并退出当前脚本。如果省略 message 的话,shell 返回 param null or not set (用于捕获由于变量未定义而导致的错误) |
${varname:+word} | 如果 varname 存在且非 null,则返回 word ;否则返回 null(用于测试变量存在) |
*表中每个冒号都是可选的。如果省略冒号,则将每个定义中的 存在且非 null 改为 存在,即变量运算符值判断变量是否存在。*
除了上面的变量替换运算符之外,还有如下的模式匹配运算符,通常用于切割路径名称,例如文件名后缀名和路径前缀。
模式匹配运算符
变量运算符 | 替换 |
---|---|
${varname#pattern} | 如果模式匹配变量取值的开头处,则删除匹配的最短部分,并返回剩下部分 |
${varname##pattern} | 如果模式匹配变量取值的开头处,则删除匹配的最长部分,并返回剩下部分 |
${varname%pattern} | 如果模式匹配变量取值的结尾处,则删除匹配的最短部分,并返回剩下部分 |
${varname%%pattern} | 如果模式匹配变量取值的结尾处,则删除匹配的最长部分,并返回剩下部分 |
${varname/pattern/string}、${varname//pattern/string} | 将 varname 中匹配模式的最长部分替换为 string 。第一种格式中,只有匹配的第一部分被替换。第二种格式中,所有匹配的部分都被替换。如果模式以 # 开头,则必须匹配 varname 的开头,如果模式以 % 开头,则必须匹配 varname 的结尾。如果``string为空,匹配部分被删除。如果 varname为 @或 *`,操作被一次应用与每个未知参数,并且扩展为结尾列表 |
示例:
1 |
|
echo ${PATH//:/'\n'} -e
可以 使用echo $PATH | sed 's/:/\n/g'
替换。
命令替换
命令替换的语法是:command
1 |
|
这里将 pwd
命令的输出字符串作为参数产地给 echo 命令,然后输出。
位置变量
位置变量也被称为系统变量、未知参数,是 shell 脚本运行时传递给脚本的参数,同时也表示在 shell 函数内部的函数参数。($0~$9,${10})
示例1:
1 |
|
输出结果:
1 |
|
示例2:
1 |
|
输出结果:
1 |
|
shell 内置一个 shift
命令,用于“截去”参数列表最左端的一个参数。执行了 shift
之后 $1
的值将永远失效,$2
的值会被赋给 $1
,以此类推。
环境变量
常用的 shell 环境变量
名称 | 描述 |
---|---|
PATH | 命令搜索路径,以冒号作为分隔符。注意与 DOS 下不同的是,当前目录不在系统路径里 |
HOME | 用户 home 目录的路径名,是 cd 命令的默认参数 |
COLUMNS | 默认的行编辑器 |
VISUAL | 默认的可视编辑器 |
FCEDIT | 命令 fc 使用的编辑器 |
HISTFILE | 命令历史文件 |
HISTSIZE | 命令历史文件中最多可包含的命令条数 |
HISTFILESIZE | 命令历史文件中包含的最大行数 |
IFS | 定义 shell 使用的分隔符 |
LOGNAME | 用户登录名 |
指向一个需要 shell 监视其修改时间的文件。当该文件修改后,Shell 将发消息 “You have mail” 给用户 | |
MAILCHECK | shell 检查 MAIL 文件的周期,单位是秒 |
MAILPATH | 功能与 MAIL 类似。但可以用一组文件,以冒号分隔,每个文件后可跟一个问号和一条发向用户的消息 |
SHELL | shell 的路径名 |
TERM | 终端类型 |
TMOUT | shell 自动退出的时间,单位为秒,若设为 0 ,则禁止 shell自动退出 |
PROMPT_COMMAND | 指定在主命令提示符前应执行的命令 |
PS1 | 主命令提示符 |
PS2 | 二级命令提示符,命令执行过程中要求输入数据时用 |
PS3 | select 的命令提示符 |
PS4 | 调试命令提示符 |
MANPATH | 寻找手册页的路径,以冒号分隔 |
LD_LIBRARY_PATH | 寻找库的路径,以冒号分隔 |
函数
函数的使用规则
- 先定义,后使用。
- 共享当前脚本的变量。并且,允许你以给未知参数赋值的方式向函数传递参数。函数内部使用
local
限定词创建局部变量。 - 函数使用
exit
命令,会退出脚本。使用return
可以返回调用函数的地方。 return
语句返回函数执行最后一条命令的退出状态。- 使用内置命令
export -f
可以将函数导出到子 shell 中。 - 可以使用
source
或dot
命令将保存在其他文件中的函数,装入当前脚本。 - 函数可以递归调用,没有调用限制
- 可以使用
declare -f
查看登录会话中定义的函数。函数以字母顺序打印所有函数定义。如果只想看函数名,则使用declare -F
。
函数的自动加载
如果想在每次启动系统时,自动加载函数,则只需要将函数写入启动文件中即可。例如将函数写入 $HOME/.profile
文件,每次启动时, source $HOME/.profile
都会自动加载函数。
函数的定义
1 |
|
两者没有功能上的区别
示例
1 |
|
执行结果:
1 |
|
函数的参数和返回值
由于函数是在当前 shell 中执行,所以变量对函数和 shell 都可见。在函数内部对变量做任何改动也会影响 shell 的环境。
参数 可以像使用命令一样,向函数传递位置参数。位置参数是函数私有的,对位置参数的任何操作并不会影响函数外部使用的任何参数。
局部变量限定词
local
使用local
时,定义的变量为函数的内部变量。内部变量在函数退出时小时,不会影响到外部同名的变量。返回方式
return
return
命令可以在函数体内返回函数被调用的位置。如果没有指定return
的参数,则函数返回最后一条命令的退出状态。return
命令同样也可以返回传给他的参数。按照规定,return
命令只能返回0
到255
之间的整数。如果函数体内使用exit
命令,则退出整个脚本。示例
1
2
3
4
5
6
7
8#! /bin/bash
# sum.sh
# 数字相加
function sum ()
{
let "sum=$1+$2"
return $sum
}执行结果:
1
2
3
4$ souce sum.sh
$ sum 2 5
$ echo $?
7
条件控制与流程控制
if/else 语句
语法结构:
1 |
|
退出状态
每一条命令或函数,在退出时都会返回一个小的整数给调用它的程序。这就是命令或函数的退出状态。
按照惯例,函数以及命令的退出状态用 0
来表示成功,而非零表示失败。
POSIX 定义了与退出状态的值相对应的含义
值 | 含义 |
---|---|
0 | 命令退出成功 |
>0 | 在重定向或单词展开期间(~、变量、命令、算数展开、单词切割)失败 |
1~125 | 命令退出失败。特定退出值的定义,参见不同命令的定义 |
126 | 命令找到,但无法执行命令文件 |
127 | 命令无法找到 |
>128 | 命令因收到信号而死亡 |
退出状态与逻辑操作
shell 语法的一个神奇之处在于它允许在逻辑上操作退出状态。这种支持给我们在编码中带来诸多方便。常见的逻辑操作有NOT
、AND
与 OR
。
NOT 当需要在条件判定失败时进行某种操作,用
NOT
更方便,使用方法是将!
置于条件判定前。1
2
3if ! condition
then statements
fiAND
AND
操作可以一次执行多个判断条件,操作符是&&
。shell 会有限执行第一个条件判断,如果成功,则执行第一个。所有条件判断成功,则整个判断语句视为成功。1
2
3
4if condition1 && condition2
then
statement
fiOR 与
AND
相反,OR
操作是只要两个或多个条件中有一个成功,则整个判断视为成功。1
2
3
4if condition1 || condition2
then
statement
fi
条件测试
if 语句
if
语句唯一可以测试的内容是退出状态。不能用于检测表达式的值。但是通过 test
命令,可以将表达式值的测试与 if
语句连用。
shell 中 test
命令方法详解
Shell中的 test 命令用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试。
- 判断表达式
表达式 | 含义(返回真) |
---|---|
if test condition | 表达式为真 |
if test ! condition | 表达式为假 |
test condition1 -a condition2 | 两个表达式都为真 |
test condition1 -o condition2 | 两个表达式有一个为真 |
- 判断字符串
参数 | 含义(返回真) |
---|---|
-n | 字符串的长度非零 |
-z | 字符串的长度为零 |
= | 字符串相等 |
!= | 字符串不等 |
- 判断数字
参数 | 含义(返回真) |
---|---|
-eq | 整数相等 |
-ne | 整数1不等于整数2 |
-ge | 整数1大于等于整数2 |
-gt | 整数1大于整数2 |
-le | 整数1小于等于整数2 |
-lt | 整数1小于整数2 |
- 判断文件(#)
表达式 | 含义 |
---|---|
test file1 -ef file2 | 两个文件具有同样的设备号和结点号 |
test file1 -nt file2 | 文件1比文件2 新 |
test file1 -ot file2 | 文件1比文件2 旧 |
test -b file | 文件存在并且是块设备文件 |
test -c file | 文件存在并且是字符设备文件 |
test -d file | 文件存在并且是目录 |
test -e file | 文件存在 |
test -f file | 文件存在并且是普通文件 |
test -g file | 文件存在并且是设置了组ID |
test -G file | 文件存在并且属于有效组ID |
test -h file | 文件存在并且是一个符号链接(同-L) |
test -k file | 文件存在并且是设置了sticky位 |
test -L file | 文件存在并且是一个符号链接(同-h) |
test -o file | 文件存在并且属于有效用户ID |
test -p file | 文件存在并且是一个命名管道文件 |
test -r file | 文件存在并且可读 |
test -s file | 文件存在并且为非空白文件 |
test -S file | 文件存在并且是一个套接字 |
test -t file | 文件描述符是在一个终端打开的 |
test -u file | 文件存在并且设置了它的set-user-id位 |
test -w file | 文件存在并且可写 |
test -x file | 文件存在并且可执行 |
test
命令有另一种形式,以 [...]
的语法,和使用 test
命令一样。因此,下面两个测试语句是等效的:
1 |
|
1 |
|
NOTE: 用中括号做判断时,“[”和“]”前面的空格是必须的,这是初学者常范的错误。
字符串比较
shell 支持字符串比较。结合 test
命令(或 [...]
),就能判断字符串比较的结果,再进行相关操作。下面表格列出了两个字符串操作的含义:
操作符 | 如果…则为真 |
---|---|
str1 = str2 | str1 匹配 str2 |
str1 != str2 | str1 不匹配 str2 |
-n str1 | str1 为非 null(长度大于 0) |
-z str1 | str1 为 null(长度为 0) |
示例1
1 |
|
执行结果:
1 |
|
由执行结果可知,文件不存在,或者文件内容为空,脚本都会给出错误提示信息。
NOTE 在 -s
函数与文件名之间必须有一个空格。
示例2
1 |
|
如果这个 shell 脚本接收的位置参数少于两个或者被 $1
指定的文件不存在,则 shell 过程直接退出。
示例3
1 |
|
执行结果:
1 |
|
文件属性检查
同判断文件
示例
1 |
|
case 语句
当脚本出现需要多次条件判断时,使用if-elif
这种方式会显得语句太长。这时 case
语句可以用更精细的方式表达 if-elif
类型的语句。语法如下:
1 |
|
任何 pattern
之间都可以由管道字符(|)分割的几个模式组成。这种情况下,expression
匹配其中一种情况,相应的语句即被执行。case
语句以 esac
结束。
示例
1 |
|
循环控制
for 循环
语法
1 |
|
list
为名称列表,我们在 for
循环中对名称列表中的每个对象进行相应操作。可以通过命令模式匹配等操作来获取名称列表,例如:
1 |
|
1 |
|
上面两个示例都可以遍历 mp3 文件,并且依次播放。但是,使用 find
命令会依次遍历当前目录下面的子目录,层层查找。而直接列出只会包含当前目录的文件夹。
在 for
循环中,如果 in list
被省略,则默认认为 in "$@"
,即命令行参数的引用列表。
1 |
|
while/until 循环
语法
- while
1 |
|
- until
1 |
|
while
和 until
的区别在于:当 condition
的退出状态为真时,while
循环继续执行,否则退出循环;当 condition
的退出状态为假时,until
循环继续执行,否则退出循环。
示例
1 |
|
1 |
|
和其他语言一样,shell 中通过 continue
跳出当前循环,提早进入下一轮循环。break
退出循环体,继续执行外层任务。
循环实例
下面使用 shift
、while
和 break
构建一个简单的命令参数处理程序
1 |
|
在 shell 中 getopts
命令可以简化选项处理。使用 getopts
重写上面的例子
1 |
|
bash/shell 解析命令行参数工具:getopts/getopt
bash 内置的 getopts
实例
1 |
|
getopts
后面的字符串就是可以使用的选项列表,每个字母代表一个选项。后面带 :
的意味着选项除了定义本身之外,还会带上一个参数作为选项的值。比如 d:
在实际的使用中就会对应 -d 30
,选项的值就是 30;getopts
字符串中没有跟随 :
的是开关型选项,不需要再指定值,相当于 true/false
,只要带了这个参数就是 true。如果命令行中包含了没有在 getopts
列表中的选项,会有警告信息,如果在整个 getopts
字符串前面也加上个 :
,就能消除警告信息了。
使用 getopts
识别出各个选项之后,就可以配合 case
来进行相应的操作了。操作中有两个相对固定的 “常量”,一个是 OPTARG
,用来取当前选项的值,另外一个是 OPTIND
,代表当前选项在参数列表中的位移。注意 case
中的最后一个选择── ?
,代表这如果出现了不认识的选项,所进行的操作。
选项参数识别完成之后,如果要取剩余的其它命令行参数,可以使用 shift
把选项参数抹去。就像例子里面的那样,对整个参数列表进行左移操作,最左边的参数就丢失了(已经用 case
判断并进行了处理,不再需要了)。位移的长度正好是刚才case循环完毕之后的OPTIND - 1,因为参数从1开始编号,选项处理完毕之后,正好指向剩余其它参数的第一个。在这里还要知道,getopts
在处理参数的时候,处理一个开关型选项,OPTIND
加1,处理一个带值的选项参数,OPTIND
则会加2。
最后,真正需要处理的参数就是 $1~$#
了,可以用 for
循环依次处理。
使用 getopts
处理参数虽然是方便,但仍然有两个小小的局限:
- 选项参数的格式必须是
-d val
,而不能是中间没有空格的-dval
。 - 所有选项参数必须写在其它参数的前面,因为
getopts
是从命令行前面开始处理,遇到非-
开头的参数,或者选项参数结束标记--
就中止了,如果中间遇到非选项的命令行参数,后面的选项参数就都取不到了。 - 不支持长选项, 也就是
--debug
之类的选项
另一个实例
1 |
|
运行结果:
1 |
|
外部强大的参数解析工具:getopt
先来看下getopt/getopts的区别
getopts
是 bash内建命令的, 而getopt
是外部命令getopts
不支持长选项, 比如: –date- 在使用
getopt
的时候, 每处理完一个位置参数后都需要自己shift
来跳到下一个位置,getopts
只需要在最后使用shift $(($OPTIND - 1))
来跳到 parameter 的位置。 - 使用
getopt
时, 在命令行输入的位置参数是什么, 在getopt
中需要保持原样, 比如-t
, 在getopt
的case
语句中也要使用-t
, 而getopts
中不要前面的-。 getopt
往往需要跟set
配合使用getopt -o
的选项注意一下getopts
使用语法简单,getopt
使用语法较复杂getopts
不会重排所有参数的顺序,getopt
会重排参数顺序getopts
出现的目的是为了代替getopt
较快捷的执行参数分析工作
下面是 getopt
自带的一个例子:
1 |
|
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!