Linux shell 编程基础
shell 是一个用 C 编写的程序,是用户与系统内核之间的桥梁。其提供了一个界面,用户通过该界面访问操作系统的内核服务。Ken Thompson 的 sh 是第一种 Unix Shell,Windows Explorer 是一个典型的图形界面 Shell。其实,shell 也是一种解释性的程序设计语言,用户通过编写 shell 脚本在 Linux 上进行自动运维等。常见的 Linux shell 种类很多,如 Bourne Shell(/usr/bin/sh或/bin/sh)、Bourne Again Shell(/bin/bash)、C Shell(/usr/bin/csh)、K Shell(/usr/bin/ksh)、Shell for Root(/sbin/sh)等,国内常用的是 /bin/bash,也是大多数 Linux 系统默认的 shell。在一般情况下,人们并不区分 Bourne Shell 和 Bourne Again Shell,所以,像 #!/bin/sh,它同样也可以改为 #!/bin/bash.
脚本格式
shell 脚本一般需要
- 以 #!/bin/bash 开头,#! 告诉系统其后路径所指定的程序即是解释此脚本文件的 Shell 程序;
- 脚本命名常以 .sh 结尾;
- 脚本需要可执行权限.
1 | # 利用 vim 创建脚本 |
#! /bin/bash
、 #! /usr/bin/env bash
(或者 #! /usr/local/miniconda/bin/python
、#! /usr/bin/env python
)称为 shebang,是告知操作系统按照指定的解释器执行脚本程序。前一种是指定解释器绝对路径(/bin/bash
、/usr/local/miniconda/bin/python
),后一种是灵活的指定解释器路径,可以跨系统使用(/usr/bin/env
是当前系统环境变量)。
执行脚本程序的方法:
/bin/bash script.sh
,bash script.sh
,sh script.sh
。这种方式是推荐的。当脚本未赋予执行权限且没有设置shebang时,都可以执行成功;/codes/script.sh
(绝对路径),./script.sh
(相对路径)。需要脚本具有执行权限,可以不设置 shebang;source script.sh
,. script.sh
。不需要脚本具有执行权限,可以不设置 shebang;bash < script.sh
,sh < script.sh
很少用。不需要脚本具有执行权限,可以不设置 shebang。
推荐:设置 shebang: #! /usr/bin/env bash
(python 建议使用解释器绝对路径设置 shebang,因为不同环境有不用的依赖包),同时赋予脚本执行权限: sudo chmod +x script.sh
,执行命令:/bin/bash /codes/script.sh
。
对于 python,如果给脚本增加了 shebang,且赋予脚本可执行权限,可以使用绝对路径执行,前面可以不用再写 python 解释器:
1 | # 利用 vim 创建脚本 |
shell 变量
shell 变量分为系统变量和用户自定义变量。
系统变量
系统变量常见的有:$HOME,$PWD,$SHELL,$USER
等等,查看某个系统变量值可使用命令echo $HOME
完成,当想查看所有系统变量时可使用命令:set
完成。
用户自定义变量
变量名称一般习惯为大写,不过小写也能通过。建议大写,遵守规范。
变量名的命名须遵循如下规则:
- 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头
- 中间不能有空格,可以使用下划线 _
- 不能使用标点符号
- 不能使用bash里的关键字(可用help命令查看保留关键字)
注意,变量赋值等号两端不要有空格。用 $
获取变量值。
自定义变量常使用如下方法:
1 | # 变量=值 |
撤销变量,撤销后无法使用该变量:
1 | # unset 变量 |
声明静态变量,这种变量不能被 unset
1 | # readonly 变量 |
注意:echo 后面如果跟双引号,能够正常解析出变量值;但是,如果跟单引号,则只会作为字符串处理返回。比如:
1 | # 使用双引号 |
结果如下:
1 | B=2 |
但使用单引号时:
1 | # 使用单引号 |
结果如下:
1 | B=$B |
将命令返回值赋值给变量
1 | A=`date` |
预定义变量
预定义变量指 shell 设计者预先定义好的变量,可以直接在 shell 脚本中使用
$$
(功能:当前进程的进程号 PID);$!
(功能:后台运行的最后一个进程的进程号 PID);$?
(功能:最后依次执行的命令的返回状态,如果为 0,表示正确执行;如果非 0(具体值有命令决定),表示执行错误);- 一些获取位置参数的变量,下面介绍。
示例:
1 | # 创建脚本 |
设置环境变量
基本语法
export 变量=值
(功能:将 shell 变量输出为环境变量,直接在命令行运行,则直接生效但退出当前 shell 时失效。可以写入到配置文件 ~/.bashrc 或 /etc/profile 中);source 配置文件
(功能:让配置文件立即生效);echo $变量
(功能:查看环境变量是否失效)。
shell 注释
单号注释,在当前行开头添加 #
1 | # A=3 |
多行注释,方法如下:
1 | :<<! |
把需要注释的内容放在 :<<!
和 !
之间,建议注释符分别占一行。
位置参数
当我们执行一个 shell 脚本时,如果希望获取到命令行的参数信息,需要位置参数,如下
1 | # 将位置参数 100 和 99 供 hello.sh 使用 |
脚本中如何使用位置参数:
$n
(功能:n 为自然数,$0
表示命令本身,如 ./hello.sh,$1
表示第 1 个参数,如 100,依次类推,对于超过 9 个的位置参数,需增加大括号,如${10}
;$*
(功能:表示命令行中所有的参数,把所有参数看成一个整体,解译为 “$1
c$2
c$3
c$4
“,其中 c 为分隔字符,默
认为空白键,所以为 “$1
$2
$3
“);$@
(功能:表示命令行中所有的参数,不过不是看作一个整体,而是一个”数组”,可以按顺序读取,解译为 “$1
“ “$2
“ “$3
“,每个变量是独立的(用双引号括起来),一般使用中建议采用该方式);$#
(功能:表示命令行中所有参数的个数)。
示例:
1 | # 编写脚本 |
序列
1 | for i in ${seq 5} |
结果为
1 | 1 |
运算式
像其他语言一样,shell 中也可以进行运算操作,方法如下:
$[运算式]
# 注意中括号和运算式之间可有一个空格,也可不要;$(( 运算式 ))
# 注意内层小括号和运算式之间可有一个空格,也可不要;expr 运算式
# expr 与运算式之间有一个空格,运算式中运算符与运算数之间有一个空格。常见运算有:expr \*, /, %
分别表示乘,除,取余;- 如果想将运算结果赋值给变量,需添加 ``,注意使用的是反引号 ` 而不是单引号 ‘,如 A=$[ 2 + 3 ] 或 A=`expr 2 + 3`
示例:
1 | # 编写脚本 |
运算符
运算符包括算术运算符、关系运算符、布尔运算符、字符串运算符和文件测试运算符。
算术运算符
下表列出了常用的算术运算符,假定变量 a 为 10,变量 b 为 20:
运算符 | 说明 | 举例 |
---|---|---|
+ | 加法 | expr $a + $b 结果为 30。 |
- | 减法 | expr $a - $b 结果为 -10。 |
* | 乘法 | expr $a \* $b 结果为 200。 |
/ | 除法 | expr $b / $a 结果为 2。 |
% | 取余 | expr $b % $a 结果为 0。 |
= | 赋值 | a=$b 把变量 b 的值赋给 a。 |
== | 相等。用于比较两个数字,相同则返回 true。 | [ $a == $b ] 返回 false。 |
!= | 不相等。用于比较两个数字,不相同则返回 true。 | [ $a != $b ] 返回 true。 |
注意:条件表达式要放在方括号之间,并且要有空格,例如: [$a==$b]
是错误的,必须写成 [ $a == $b ]
。
关系运算符
关系运算符只支持数字,不支持字符串,除非字符串的值是数字。
下表列出了常用的关系运算符,假定变量 a 为 10,变量 b 为 20:
运算符 | 说明 | 举例 |
---|---|---|
-eq | 检测两个数是否相等,相等返回 true。 | [ $a -eq $b ] 返回 false。 |
-ne | 检测两个数是否不相等,不相等返回 true。 | [ $a -ne $b ] 返回 true。 |
-gt | 检测左边的数是否大于右边的,如果是,则返回 true。 | [ $a -gt $b ] 返回 false。 |
-lt | 检测左边的数是否小于右边的,如果是,则返回 true。 | [ $a -lt $b ] 返回 true。 |
-ge | 检测左边的数是否大于等于右边的,如果是,则返回 true。 | [ $a -ge $b ] 返回 false。 |
-le | 检测左边的数是否小于等于右边的,如果是,则返回 true。 | [ $a -le $b ] 返回 true。 |
布尔运算符
下表列出了常用的布尔运算符,假定变量 a 为 10,变量 b 为 20:
运算符 | 说明 | 举例 |
---|---|---|
! | 非运算,表达式为 true 则返回 false,否则返回 true。 | [ !false ] 返回 true。 |
-o | 或运算,有一个表达式为 true 则返回 true。 | [ $a -lt 20 -o $b -gt 100 ] 返回 true。 |
-a | 与运算,两个表达式都为 true 才返回 true。 | [ $a -lt 20 -a $b -gt 100 ] 返回 false。 |
逻辑运算符
以下介绍 Shell 的逻辑运算符,假定变量 a 为 10,变量 b 为 20:
运算符 | 说明 | 举例 |
---|---|---|
&& | 逻辑的 AND | [[ $a -lt 100 && $b -gt 100 ]] 返回 false |
|| | 逻辑的 OR | [[ $a -lt 100 || $b -gt 100 ]] 返回 true |
注意,运算式是有两个中括号嵌套包裹。
字符串运算符
下表列出了常用的字符串运算符,假定变量 a 为 “abc”,变量 b 为 “efg”:
运算符 | 说明 | 举例 |
---|---|---|
= | 检测两个字符串是否相等,相等返回 true。 | [ $a = $b ] 返回 false。 |
!= | 检测两个字符串是否不相等,不相等返回 true。 | [ $a != $b ] 返回 true。 |
-z | 检测字符串长度是否为0,为0返回 true。 | [ -z $a ] 返回 false。 |
-n | 检测字符串长度是否不为 0,不为 0 返回 true。 | [ -n "$a" ] 返回 true。 |
$ | 检测字符串是否为空,不为空返回 true。 | [ $a ] 返回 true。 |
文件测试运算符
文件测试运算符用于检测 Unix 文件的各种属性。
属性检测描述如下:
操作符 | 说明 | 举例 |
---|---|---|
-b file | 检测文件是否是块设备文件,如果是,则返回 true。 | [ -b $file ] 返回 false。 |
-c file | 检测文件是否是字符设备文件,如果是,则返回 true。 | [ -c $file ] 返回 false。 |
-d file | 检测文件是否是目录,如果是,则返回 true。 | [ -d $file ] 返回 false。 |
-f file | 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 | [ -f $file ] 返回 true。 |
-g file | 检测文件是否设置了 SGID 位,如果是,则返回 true。 | [ -g $file ] 返回 false。 |
-k file | 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。 | [ -k $file ] 返回 false。 |
-p file | 检测文件是否是有名管道,如果是,则返回 true。 | [ -p $file ] 返回 false。 |
-u file | 检测文件是否设置了 SUID 位,如果是,则返回 true。 | [ -u $file ] 返回 false。 |
-r file | 检测文件是否可读,如果是,则返回 true。 | [ -r $file ] 返回 true。 |
-w file | 检测文件是否可写,如果是,则返回 true。 | [ -w $file ] 返回 true。 |
-x file | 检测文件是否可执行,如果是,则返回 true。 | [ -x $file ] 返回 true。 |
-s file | 检测文件是否为空(文件大小是否大于0),不为空返回 true。 | [ -s $file ] 返回 true。 |
-e file | 检测文件(包括目录)是否存在,如果是,则返回 true。 | [ -e $file ] 返回 true。 |
其他检查符:
- -S: 判断某文件是否 socket。
- -L: 检测文件是否存在并且是一个符号链接。
条件判断和流程控制
基本语法如下:
1 | [ condition ] |
注意,condition 与中括号之间有一个空格;当 condition 为非空时,如 [ A ]
,返回 true;当为空 [ ]
时,返回 false.
应用实例,如当某个程序执行正确时,执行某子程序,判断为 [ $? ]
,0 为 true,大于 1 为 false.
if … else …
实例代码:
1 | # 创建脚本 |
case
基本语法
1 | case $变量 in |
for
基本语法1
1 | for 变量 in v1 v2 vn |
基本语法2
1 | for (( 初始值; 循环控制条件; 变量变换)) |
实例代码:
1 | # 编写脚本 |
seq
生成序列,用法如下:
1 | seq [选项] 尾数 |
示例:
1 | (base) ➜ / echo $(seq 2) |
while
基本语法
1 | while [ condition ] |
实例程序
1 | # 编写脚本 |
until 循环
until 循环执行一系列命令直至条件为 true 时停止。
until 循环与 while 循环在处理方式上刚好相反。
一般 while 循环优于 until 循环,但在某些时候—也只是极少数情况下,until 循环更加有用。
until 语法格式:
condition 一般为条件表达式,如果返回值为 false,则继续执行循环体内的语句,否则跳出循环。
1 | until condition |
实例代码
1 |
|
跳出循环
在循环过程中,有时候需要在未达到循环结束条件时强制跳出循环,Shell 使用两个命令来实现该功能:break 和 continue。
break命令
break命令允许跳出所有循环(终止执行后面的所有循环)。
下面的例子中,脚本进入死循环直至用户输入数字大于 5。要跳出这个循环,返回到shell提示符下,需要使用 break 命令。
1 |
|
continue
continue 命令与 break 命令类似,只有一点差别,它不会跳出所有循环,仅仅跳出当前循环。
对上面的例子进行修改:
1 |
|
运行代码发现,当输入大于 5 的数字时,该例中的循环不会结束,语句 echo “游戏结束” 永远不会被执行。
read 读取控制台输入
基本语法
1 | read (选项) (参数) |
实例代码
1 | # 编写脚本 |
函数
shell 中有自带的系统函数,用户也可以自定义函数。
系统函数
系统函数有很多,比如 basename, dirname 等
1 | # basename [文件路径] [扩展名] |
1 | # dirname [文件路径] |
自定义函数
基本语法
1 | [ function ] funcname [()] |
实例代码
1 | # 编写脚本 |
数组
数组中可以存放多个值。Bash Shell 只支持一维数组(不支持多维数组),初始化时不需要定义数组大小(与 PHP 类似)。
与大部分编程语言类似,数组元素的下标由 0 开始。
Shell 数组用括号来表示,元素用”空格”符号分割开,语法格式如下:
1 | array_name=(value1 value2 ... valuen) |
实例
1 |
|
我们也可以使用下标来定义数组:
1 | array_name[0]=value0 |
读取数组元素
读取数组元素值的一般格式是:
1 | ${array_name[index]} |
实例
1 |
|
获取数组长度
1 |
|
字符串操作
shell 中有一些字符串操作方法,速度比较快
字符串截取
获取字符串长度:
1
2
3
4${#string}
# 或
${expr length $string}
# 后者速度稍慢示例:
1
2s=abc123cba
echo ${#s}结果为
1
9
从左边截取子字符串:
${string:postion:length}
1
2s=abc123cba
echo ${s:5:3}结果为
1
3cb
从右边截取子字符串:
${string:空格-length}
或${string:num1-num2}
这里num2
大于num1
且为正数1
2echo ${s:0-4}
echo ${s:1-5}结果为
1
3cba
删除子字符串
- 从左边匹配到第一个并删除左边:
${string#match_substring}
,这里match_substring
为匹配子字符串,也可以为正则表达式结果为1
2
3
4s="C:\Windows\win.ini"
echo ${s#*\\} # 这里 \\ 表示转义 \,* 表示匹配所有,所以 *\\ 表示匹配第一个\及其前面的所有内容
echo ${s#*W}
echo ${s#*w}1
2
3Windows\win.ini
indows\win.ini
s\win.ini - 从左边匹配到最后一个并删除左边:
${string##match_substring}
结果为1
echo ${s##*\\}
1
win.ini
- 从右侧匹配到第一个并删除右边:
${string%match_substring}
结果为1
echo ${s%\\*}
1
C:\Windows
- 从右侧匹配到最后一个并删除右边:
${string%%match_substring}
结果为1
echo ${s%%\\*}
1
C:
替换子字符串
- 替换第一个:
${string/match_substring/replace_substring}
将string
中的match_substring
为replace_substring
结果为1
2
3s=abc123321cba
echo ${s/123/456}
echo ${s/c/C}1
2abc456321cba
abC123321cba - 替换所有:
${string//match_substring/replace_substring}
结果为1
echo ${s//1/I}
1
abcI2332Icba
set 命令
Linux set
命令用于设置 shell。具体的,set
命令能设置所使用 shell 的执行方式,可依照不同的需求来做设置。往往该命令被忽视,导致脚本安全性和维护性降低。Bash 在执行脚本的时候,会自动创建一个新的 shell,如
1 | bash hello.sh |
会自动创建一个新 shell,hello.sh 在该 shell 中执行,bash 默认为该 shell 执行环境配置各种参数,set
命令可以用来修改这个新 shell 执行环境的允许参数, 默认环境参考下面语法部分。
语法
1 | set [--abefhkmnptuvxBCEHPT] [-o option-name] [argument …] |
参数说明
1 | -e 等价于 -o errexit,若指令传回值不等于 0,即脚本出现一处错误,则立即退出 shell; |
实例
对于 hello.sh,脚本内容如下:
1 |
|
除了上面的情况,set -e
有一个例外情况,就是不适用于管道命令。所谓管道命令,就是多个子命令通过管道运算符(|)组合成为一个大的命令。Bash 会把最后一个子命令的返回值,作为整个命令的返回值。也就是说,只要最后一个子命令不失败,管道命令总是会执行成功,因此它后面命令依然会执行,set -e
就失效了。对于管道命令,可以使用参数:set -eo pipefail
可以组合使用各参数,如:
1 | # 写法一 |
或者在执行脚本时传入:
1 | bash -euxo pipefail hello.sh |
查看当前 shell 的设置,可直接通过如下命令查看:
1 | set |