0 前言

  因为工作需要频繁接触Linux,同时工作内容也和Linux相关,感觉有必要系统学习一下shell脚本的语法,特此记录。

本文以菜鸟教程为基础,提炼最常用内容,并结合工作经历进行补充,适合有一定编程基础的新手入门。

shell脚本基本格式
  shell脚本第一行都需要加上#!/bin/bash,这个是用来指定脚本默认的执行程序的,所以理论上也可以选择其他的执行程序,比如#!/bin/sh 或者 #!/bin/zsh等。

1 变量

1.1 基本使用方式

  • 定义变量val="test"
    变量可以被重复定义,直接多次赋值即可,不需要其他操作。注意,等号两边不能加空格(不是语法问题,是可能导致值错误)。常用的变量主要有两种类型,一个是数值型,还有就是字符串型,后者要加引号。

  • 使用变量echo $val 或者 echo ${val}
    变量使用时需要加上一个美元符号,或者是使用大括号括起来,这样可以更加清晰地界定变量名称的范围。

  • 删除变量unset val
    删除该变量就不能再使用,如果要使用就需要重新定义。

  • 只读变量readonly val
    这样可以设置该变量为只读变量,不能对该变量重新定义修改。

另外,需要明确,shell脚本中,变量之间是独立的,如果变量a由变量b计算得到,当更新变量b时,变量a不会自动更新。

1.2 特殊变量

  • $1, $2:类似这样一个美元符号加上一个数字的组合,是表示脚本执行时的参数。如果是在函数体内部,则表示函数的参数。如果数字大于10,则需要加大括号,如${10}
  • $0:表示这个脚本文件名(包含路径)。
  • $?:这个表示该指令上一条指令执行返回的结果。
  • $#:参数的个数

1.3 数组变量

  定义一个数组变量的基本格式如下

array_name=(value0 value1 value2 value3)

注意,这里元素之间分隔是不要逗号的,直接用空格分隔,或者换行分隔也行。另外还可以单独定义数组的各个分量,比如:

array_name[0]=value0
array_name[1]=value1
array_name[n]=valuen

而且这个数组的下标范围不需要像C语言那样先设置好,这个是没有数字范围限制的。

  如果要取数组中的元素,则可以通过${数组名[下标]}的方式来读取。这里面的中括号两侧不需要加空格。如果要获取数组的长度,可以按照下面这些方式来实现。

# 取得数组元素的个数
length=${#array_name[@]}
# 或者
length=${#array_name[*]}
# 取得数组单个元素的长度
length=${#array_name[n]}

这里如果不加#,单纯使用${array_name[*]}或者${array_name[@]}得到的是数组中所有的元素。

  这里顺便补充一下这个#的作用,除了可以获取数组的长度外,还可以获取字符串的长度,比如

string="abcd"
echo ${#string}   # 输出 4

关联数组/字典

  shell脚本还支持关联数组或者说字典,即索引内容可以是任意的字符串或者其他整数,比如

site["google"]="www.google.com"

但这里要提前声明这个数组是关联数组,使用declare -A的方式。所以一般使用的格式如下所示。

declare -A site
site["google"]="www.google.com"
site["runoob"]="www.runoob.com"
site["taobao"]="www.taobao.com"

然后和上面获取所有数组元素一样,这里也可以获取所有key:

declare -A site
site["google"]="www.google.com"
site["runoob"]="www.runoob.com"
site["taobao"]="www.taobao.com"

echo "数组的键为: ${!site[*]}"
echo "数组的键为: ${!site[@]}"

2 符号

2.1 字符串符号

  当需要给变量赋字符串时,就需要用引号引起来,既可以使用单引号,也可以使用双引号。如果使用单引号的话,字符串会原样输出,里面的变量和转义符号无效。而双引号则可以使用变量和转义符号等。

  此外,字符串也可以直接在赋值的时候就拼接,这里既可以使用单引号也可以使用双引号。比如

name="test"
greeting="hello, "$name" !"
# hello, test !

2.2 分号

  shell代码不需要行末以分号结尾,但有时候仍然可以看到使用分号的情况,其实就是分隔命令,这样可以实现一行写好几条指令,方便在命令终端直接执行。

  另外如果是两个分号;;,主要是用在case语句中,用来表示一种case结束,相当于C语言中的break

2.3 运算符号

算术运算符

  shell脚本是不支持数学运算的,如果要进行数学运算,需要使用指令,最常用的就是expr,举个例子

val=`expr 2 + 2`
echo "两数之和为:$val"

这里有几个注意点:

  • 表达式和运算符之间要有空格,即2 + 2
  • 表达式要使用 ` ` 反引号给括起来,注意不是单引号

关系运算符

  主要是比较数值的大小或判断是否相等,也就是形成条件表达式,主要有以下几种

运算符 说明 举例
-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。

同样,这里也有几个注意点:

  • 和前面运算符一样,这里的中括号是需要加空格的。结合前面的内容,可以总结为符号两边都要加空格,除了赋值和美元符号(变量相关)
  • 记忆方式:eq是equal的缩写,然后带e的都表示有等号,lt是lower than的缩写,gt是greater than的缩写,那自然可以得到le是小于等于,ge是大于等于。

  另外,如果想用最简单的< > <= >=这些符号,可以使用(( )),把比较表达式放在双小括号内,但记得也需要加空格。 比如 (( $a > 10 ))

布尔运算符

运算符 说明 举例
! 非运算,表达式为 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。

逻辑运算符

运算符 说明 举例
&& 逻辑的 AND [[ $a -lt 100 && $b -gt 100 ]] 返回 false
|| 逻辑的 OR [[ $a -lt 100 || $b -gt 100 ]] 返回 true

字符串运算符

运算符 说明 举例
= 检测两个字符串是否相等,相等返回 true。 [ $a = $b ] 返回 false。
!= 检测两个字符串是否不相等,不相等返回 true。 [ $a != $b ] 返回 true。
-z 检测字符串长度是否为0,为0返回 true。 [ -z $a ] 返回 false。
> < 按字典顺序比较。在 [[ ]] 中可直接使用;在 [ ] 中需转义为 \< [[ "apple" < "banana" ]]
-n 检测字符串长度是否不为 0,不为 0 返回 true。 [ -n "$a" ] 返回 true。
$ 检测字符串是否不为空,不为空返回 true。 [ $a ] 返回 true。

  除了=判断字符串是否相等外,还可以使用双等号==,但一般需要加双中括号,[[ $s == "test.bin" ]]在单中括号中使用双等号是兼容性差的写法,部分sh可能不支持。 此外,双括号内还可以使用模式匹配和正则表达式。比如

[[ $var == *.txt ]]  # 模式匹配
[[ $var =~ ^[0-9]+$ ]]  # 正则表达式

文件测试运算符

操作符 说明 举例
-d file 检测文件是否是目录,如果是,则返回 true。 [ -d $file ] 返回 false。
-f file 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 [ -f $file ] 返回 true。
-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。

这里做一个重要的总结

  • 双小括号里面都是数值运算表达式,所以里面可以使用C语言风格的运算符号,如++,–,+=,>,<,==,!=等,括号内侧可以没有空格,变量可以不加$前缀
  • 单小括号主要用于命令替换(类似于反引号)和定义数组
  • 单中括号和双中括号都是条件测试,双中括号是增强版的条件测试,更推荐使用。这两种括号内侧都必须要有空格
  • 在条件测试中,===是等价的。而且只要使用了===(或者上面提到的字符串比较符号),都表示对两边的内容视为字符串进行逐字符比较,因此a=1b=01如果用等号是不相等的;同理,只有使用-gt, -ne等数值比较符号,才会将两边的变量视为数值去比较。
  • 但是在双小括号中,===就和C语言一样,功能截然不同,一个是赋值,一个是比较符号。

2.4 输入输出重定向符号

输出重定向

command1 > file1

这个是指将command1指令执行的结果保存在file1中,而且是完全覆盖;
如果是这样:

command1 >> file1

则是指将内容追加在file1后面。

输入重定向

command1 < file1

把文件作为参数传入,然后再执行指令。

特殊重定向

  如果希望执行某个指令,但又不希望在屏幕上显示输出结果,就可以将输出重定向到/dev/null,他是一个特殊的文件,写入到他的内容都会被丢弃。

$ command > /dev/null

3 条件语句

  类比C语言,条件语句主要是两种,一个是if-else语句,一个是switch-case语句,shell脚本中也都有,下面分别介绍。

3.1 if-else

  基本的语法格式如下所示。

if condition1
then
    command1
elif condition2 
then 
    command2
else
    commandN
fi

可以看到,条件选择语句没有大括号限定范围,而是使用fi,即if倒着写来表示语句的结束,后面要介绍的case语句也是一样。另外elifelse部分是可选的部分,不一定有。也可以写在同一行,不过语句后面要加上分号;

if [ $(ps -ef | grep -c "ssh") -gt 1 ]; then echo "true"; fi

3.2 switch-case

  其基本的语法格式如下所示。

casein
模式1)
    command1
    command2
    ...
    commandN
    ;;
模式2)
    command1
    command2
    ...
    commandN
    ;;
esac

注意,这里每个选项都需要加一个右括号,然后每个case最后用两个分号分隔,表示break语句,最后要加上esac和前面的case呼应,表示一个区域,等同于大括号的作用。

4 循环语句

  和C语言一样,循环语句也具有两种,一种是for循环,一种是while循环。

4.1 for循环

  其基本的代码格式如下所示。

for var in item1 item2 ... itemN
do
    command1
    command2
    ...
    commandN
done

这里do...done就是用来表示大括号的,中间是循环体。

  上面in后面的内容可以是多个变量,也可以是一个数组变量,或者通配符,比如

for file in *
do
    echo "处理: $file"
done

4.2 while循环

  基本的语法格式如下所示

while condition
do
    command
done

4.3 循环结束

  在shell中也有break和continue指令。

#!/bin/bash
while :
do
    echo -n "输入 1 到 5 之间的数字:"
    read aNum
    case $aNum in
        1|2|3|4|5) echo "你输入的数字为 $aNum!"
        ;;
        *) echo "你输入的数字不是 1 到 5 之间的! 游戏结束"
            break
        ;;
    esac
done

注意这里的break指令不是跳出switch-case语句,而是跳出外面的while循环语句。

5 函数

  shell脚本中也可以自定义函数,定义好之后就可以在shell脚本中随便调用。函数定义格式如下所示。

[ function ] funname ()
{
    action;
    [return int;]
}

注意,这里中括号的部分是可选的,也就是说定义函数时可以不写 function ,也可以没有返回值。

  函数调用时,直接使用函数名字即可,不需要加括号。如果有参数,直接接在后面即可,类似于一般的指令:funcname param1 param2

5.1 变量作用域【更新】

  既然提到函数,那自然避不开变量的作用域,和一般的编程语言不一样,shell脚本中的变量作用域有点特殊: 只有在函数中显式地用loacl声明的变量才是函数中的局部变量,其他情况都是全局变量。也就是说while循环,switch-case以及其他的大括号,都不会影响变量的作用域。所以,完全可以在函数中创建一个变量,函数执行完毕后,这个变量就是全局有效。

6 常用基本命令

6.1 echo

  这个应该是最常用的shell指令了。除了最常见的输出信息到终端外,这个指令还可以结合其他符号实现一些高级功能。

常用参数

  • -e:识别转义字符,包括终端指定颜色的指令也需要这个
  • -n:不换行输出,指这一行末尾不加换行符,默认是会加换行符号的。

结合管道运算符|

  有时候需要调用函数或者另外的脚本且需要传递参数时,就可以使用echo para | other_script.sh这种格式来实现调用其他脚本并传递参数。

结合输入输出重定向符号>,>>

  结合输入输出重定向符号,可以实现文本文件的写入和添加。

6.2 printf

  除了echo指令外,printf也可以用来输出内容到终端,而且支持更加复杂的格式化输出,其用法和C语言中的printf差不多。

# 整数
printf "Decimal: %d\nHex: %x\nOctal: %o\n" 255 255 255

# 浮点数
printf "Float: %f\nScientific: %e\n" 3.14159 3.14159

# 字符串
printf "Name: %s\n" "Bob"

# 字符
printf "First letter: %c\n" "A"

6.3 export

  export 用于声明一个环境变量:export variable=value,该环境变量只在本进程和其子进程中可以访问。
  如果是在系统级的配置文件中,如/etc/profile中 export 一个变量,那么这个变量就会在整个系统运行期间都起作用。即使新开shell会话连接甚至,重启了服务器仍可生效。
  如果在用户级 ~/.bash_profile 之类的文件中,则每次启动shell 都会去读这个文件,所以每次打开shell也是可以取到这个值的。
  如果在某一次运行中,手动 export 一个变量,则只在这次shell的使用中,才能访问这个变量,或者在该shell启动的启动程序中,也是可以访问这个变量的,因为它们是这个shell的子进程。新开shell连接及重启服务器都会失效。

总之,export 作用就是 把本地变量变成全局变量(实际中叫环境变量)

参考链接

6.4 source

  使shell读入指定的shell脚本文件并依次执行文件中的所有语句。
source命令通常用于重新执行刚修改的初始化文件,使之立即生效,而不必注销并重新登录。

source filename 
. filename # 和上面等价

另外还需要注意source和直接执行脚本./other.sh的区别,后者会开启一个子shell,会继承父shell的环境变量,但子shell执行得到的环境变量不影响父shell。而source指令是把脚本复制到本窗口继续执行,不会新建shell,所以环境变量都是共用的,也会互相影响。

参考链接

6.5 read

  如果脚本中需要输入一些内容,就可以使用read指令,最常见的用法就是

read -p "提示信息" val

指将输入的内容存在val变量中。其他用法可以参考这个链接

6.5 pushd&popd

  这两个指令经常配合使用。在写相关脚本时,免不了要进入某个目录执行,执行完毕后又需要回到之前的目录,这种 “栈式目录切换”pushd&popd最为合适。以下是一个例子。

$ pushd /usr/local/bin
$ pushd /etc
$ popd

上述三行指令就是先进入到路径/usr/local/bin,再进入/etc,最后再回到/usr/local/bin,这样就不用记忆上一次执行脚本是在哪个路径了,非常方便。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐