Shell 脚本基础

Shell Script 定义

Shell Script 就是利用 shell 的功能所写的一个程序,这个程序使用纯文本文件,将一些 shell 的语法与命令写在里面,搭配正则表达式、管道命令与数据流重定向等功能达到所想要的处理目的。

脚本编写注意事项:

  • 命令执行时从上而下,从左到右地分析与执行;
  • 命令参数之间的多个空白都会被忽略掉;
  • 空白行会被忽略掉,包括 [tab] 也被视为空格;
  • 如果读到一个 [Enter](CR),就尝试开始执行该命令;
  • 一行内容太多,可使用 \+ [Enter] 来扩展至下一行;
  • # 可以作为批注,任何 # 后面的内容都会被注释掉。

编写脚本

先以一个最简单的脚本作为范例:

#!/bin/bash
# Program:
#   This program shows "Hello Hell!" in your screen.
# History:
# 2046/1/1  Ass First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
echo -e 'Hello Hell! \a \n'
exit 0

执行脚本:

[root@101c7 bin]$ bash sh01.sh 
Hello Hell!  

逐行内容分析如下:

  • #!/bin/bash

    声明这个脚本使用的 shell 名称,使这个脚本执行时能加载 bash 的相关环境配置文件(一般是 ~/.bashrc),并执行 bash 来使下面的命令能够执行。如果没有设置好这一行,脚本可能无法执行,因为系统无法判断需要使用什么 shell 来执行。

  • #开头的内容

    除了第一行 #! 用来声明 shell 外,其他 # 开头的行都是批注作用。

  • 主要环境变量的声明

    建议将一些重要的环境变量设置好。如 PATH 和 LANG。这样可以让程序在进行时可以直接执行一些外部命令,而不必写绝对路径。

  • 主要程序部分

    此处为 echo 命令。

  • 告知执行结果

    利用 exit 这个命令来让程序中断,并回传一个数值给系统。执行完脚本后使用 echo $? 命令可以显示 0 值。也可以利用 exit n 的功能自定义不超过 255 的退出状态码。

执行脚本

直接执行脚本需要对脚本拥有 rx 权限,方式有下面几种:

  • 可以通过绝对路径直接执行,比如 /root/test.sh
  • 可以通过相对路径直接执行,比如 ./test.sh
  • 可以将脚本放置到 PATH 指定的目录内,如 ~/bin/ 内,输入 test.sh 就能直接执行;
  • 以 bash 子进程来执行,例如 bash test.sh。以此方式执行 test.sh 只要有 r 的权限就能执行;
  • source 命令来执行脚本将直接在父进程中运行,例如 source test.sh。也可以使用快捷方法点操作符(Dot Operator)例如 . test.sh

直接执行脚本的方式会使用一个新的 bash 环境来执行脚本命令,也就是在子进程的 bash 内执行。这样子进程内的各项变量或操作在结束后不会传回到父进程中。

捕获信号

使用 trap 命令可以捕获 shell 中发出的 Linux 信号并直接处理。例如捕获 [Ctrl]+C 发出的 SIGINT 信号:

#!/bin/bash
trap "echo 'Trapped CTRL+C'" SIGINT
while [ 3 -lt 4 ]; do
echo "looping"
sleep 1
done

移除捕获可以用 trap -- SIGINT 来操作。

查询所有可以捕获的信号列表可以使用 -l 参数,其结果显示和 kill 的一样:

[root@server1 ~]$ trap -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8

trap 除了用作在进行某些特殊操作时忽略信号外,还可以用来删除临时文件。例如捕获一个由用户发送的中断,并使用 trap 清除在脚本的执行期间所创建的临时文件:

#!/bin/bash

function removeTemp() {
    if [ -f "$tmpFile" ]; then
        echo "Del temp files..." && rm -f "$tmpFile"
    else
        echo "No temp file."
    fi
}
trap removeTemp 1 2
tmpFile=/tmp/sigtrap$$
cat > $tmpFile

exit 0

注意 trap 命令必须写在脚本执行命令之前,否则可能无法捕获到信号。

判断测试

使用test指令用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试。

文件类型检测参数如下,检测类型前会先检查文件是否存在:

参数 功能
-e 文件名是否存在
-f 文件类型是否为文件
-d 文件类型是否为目录
-b 文件是否为 Block 设备
-c 文件是否为 Character 设备
-S 文件是否为 Socket 文件
-p 文件是否为 FIFO(pipe) 文件
-L 文件是否为连接文件

文件的权限检测参数如下,也会先检查文件是否存在。root权限常有例外:

参数 功能
-r 检测是否具有 r 权限
-w 检测是否具有 w 权限
-x 检测是否具有 x 权限
-u 检测是否具有 SUID 属性
-g 检测是否具有 SGID 属性
-k 检测是否具有 Sticky bit 属性
-s 检测文件是否为非空白文件

两个文件之间的比较参数如下,例如test file1 -nt file2

参数 功能
-nt newer than 缩写,判断file1是否比file2新。
-ot older than 缩写,判断file1是否比file2旧。
-ef 判断file1file2是否为同一文件。主要判定两个文件是否指向相同的inode

整数之间的判定,例如test n1 -eq n2

参数 功能
-eq 两数值相等 (equal)
-ne 两数值不等 (not equal)
-gt n1 大于 n2 (greater than)
-lt n1 小于 n2 (less than)
-ge n1 大于等于 n2 (greater than or equal)
-le n1 小于等于 n2 (less than or equal)

判定字符串的数据,例如[ -z "${JAVA}" ]

参数 功能
-n string 判定字符串 string 是否为 0,若 string 不为空返回 true
-z string 判定字符串 string 是否为 0,若 string 为空字符串返回 true
str1=str2 判定 str1 是否等于 str2,相等返回 true
str1!=str2 判定 str1 是否不等于 str2,不相等返回 true

多重条件判定,例如 test -r file -a -x file

参数 功能
-a 两个条件同时成立时才返回 true
-o 任何一个条件成立时返回 true
! 反向状态,例如 test ! -e file,当 file 不存在时返回 true

通常使用中括号 [] 来代替 test 进行数据判断,例如判断两个变量是否相等:

[root@101c7 bin]$ [ "$HOME" == "$MAIL" ] || echo 'no'
no

中括号 []用作判断时:

  • 中括号内每个组件都需要有空格来分隔;
  • 中括号内的变量最好都以双引号括起来;
  • 中括号内的常量最好都以单或双引号括起来。

数值运算

可以使用 $[计算式] 来进行简单整数运算:

#!/bin/bash
a=10
b=$[$a+4]
c=$[a*b]
echo $c

可以使用 $((计算式)) 来进行高级整数运算。在 (()) 中的变量不需要 $ 来引用:

#!/bin/bash
read -rp "first number:" nu1
read -rp "second number:" nu2
total=$((nu1**nu2))
echo -e "\nThe result of $nu1 * $nu2 is $total"

另一种不太常用的方式是使用命令 expr,因为有些符号在 shell 中另有含义(比如 *),这时需要用 \ 来转义:

[root@server1 ~]$ expr 5 \* \( 3 + 1 \)
20

如果想计算小数,可以调用 bc 来处理。其中 scale 指明了小数点位数。默认情况下 scale 为 0,使用 -l 参数启动时 scale 为 20:

#!/bin/bash
a=11;b=3
c=$(echo "scale=4; $a/$b"|bc)
echo $c

也可以用来进行不同进制之间的转换,例如十进制转十六进制。表达十六进制的字母必须大写,小写会被 bc 认为是变量:

#!/bin/bash
a=202;b=53
c=$(echo "ibase=10;obase=16; $a+$b" | bc -l)
echo $c

双方括号[[]]用来对字符串进行模式匹配:

#!/bin/bash
if [[ $USER = r* ]]; then
    echo "Hello $USER"
else
    echo "Deny"
fi

参数变量

如果脚本支持参数,那么脚本名或脚本路径为 $0 变量。想取绝对文件名而不是路径时,可以用 basename 命令来处理。

后面参数从 $1 开始赋值直到 $9。超过 9 个变量引用时要加上大括号,如在脚本中引用第 11 个变量采取 ${11} 来引用。

特殊的参数变量:

变量 说明
$# 代表输入的参数个数.
$@ 代表 $1$2$3 之意,每个变量是独立的,可以用 for 遍历。
$* 代表 $1c$2c$3,其中 c 为分隔符,默认是空格.

在脚本中使用 shift 可以进行参数变量偏移。比如 shift 3 代表拿掉前 3 个参数变量。这时遍历命令行参数的另一个好办法,尤其是在不知道有多少个参数时,可以只操作第一个参数,用 shift 移动参数后继续操作第一个参数:

#!/bin/bash
count=1
while [ -n "$1" ]; do
    echo "para$count = $1"
    count=$(($count + 1))
    shift
done

逻辑判断

许多程序要求对 shell 脚本中的命令施加一些逻辑流程控制。有一类命令会根据条件使脚本跳过某些命令。这样的命令称为结构化命令(Structured Command)。

循环语句内使用其他循环命令叫做嵌套循环(Nested Loop)。

if…then

if 语句本身并不执行任何判断,只会运行 ifelif 后面那个命令,如果命令的退出状态码是 0,位于 then 部分的命令就会被执行。否则 bash shell 会继续执行脚本中的下一个命令,直到 fi 为止。

if 后可以有多个条件判断式,用 &&|| 隔开代表 And 或 Or。

单条件判断的写法

if [ 条件判断式 ]; then
    条件成立时运行的命令;
fi

多条件判断的写法

if [ 条件判断式1 ]; then
    条件判断式1成立时运行的命令;
elif [ 条件判断式2 ]; then
    条件判断式2成立时运行的命令;
else
    条件判断式都不成立时运行的命令;
fi

case…esac

基本写法如下,其中 ;; 代表其他语言中的 break 跳出作用:

case $变量名称 in
    "第一个变量内容")
        程序段
        ;;
    "第二个变量内容")
        程序段
        ;;
    *)
        不符合所有条件的程序执行段
        exit 1
        ;;
esac

循环

在 Bash 脚本中,有多种循环结构,最常用的有 for 循环和 while 循环。

while do done

条件成立时进行循环,基本写法:

while [ 判断式 ]
do
    程序段落
done

until do done

条件成立时终止循环,基本写法:

until [ 判断式 ]
do
    程序段落
done

for…do…done

for 语法用来表示已知次数的循环。in 后面参数之间用空格隔开,基本语法:

for var in con1 con2 con3 ...
do
    程序段
done

内部字段分隔符(Internal Field Separator)定义了空格、制表符和换行符作为字段分隔符。如果值之间不是用空格分隔,可以临时修改 IFS 变量的值(例如使用冒号和换行符 IFS=:$'\n'),再使用 for 循环读取变量:

IFS.OLD=$IFS
IFS=:,
for循环程序段
IFS=$IFS.OLD

可以使用通配符来达成遍历目录的目的,例如遍历 /root/home 目录下的文件和目录:

for file in /root/* /home/.bash*

可以用 seq(sequence 的缩写)来定义连续数字。比如定义循环范围 1 到 10:

for nu in $(seq 1 10)

要定义连续字符可以写成 echo {a..z}。例如用 for 循环输出 1 到 5:

for i in {1..5}

可以使用自定义数值处理:

for ((初始值; 限制值; 执行步长))
参数 说明
初始值 直接以类似 i=1 设置好
限制值 值在限制值范围内继续循环,例如 i<=20
执行步长 每做一次循环时变量的变化范围,例如 i=i+1

控制循环

使用 break 命令可以用来退出任意类型的循环。如果有多层循环,break 会自动终止所在最内层的循环。可以用 break n 来指定要停止的外部循环。n 为 1 代表当前循环:

#!/bin/bash
for ((a = 1; a < 3; a++)); do
    echo "The $a loop"
    for ((b = 10; b < 50; b = b + 10)); do
        if [ $b -gt 30 ]; then
            break 2
        fi
        echo "Vaule is $b"
    done
done

使用 continue 命令可以提前终止某次循环,继续运行下次循环。同样在多层循环中可用 continue n 来指定要继续执行的循环层级:

#!/bin/bash
for ((a = 1; a < 10; a++)); do
    if [ $a -gt 5 ] && [ $a -lt 7 ]; then
        continue
    fi
    echo "now 'a' is $a"
done

循环输出

在 shell 脚本中,可以对循环的输出使用管道或进行重定向。通过在 done 命令之后添加一个处理命令来实现:

#!/bin/bash
for file in /root/*; do
    if [ -d $file ]; then
        echo "$(basename $file) is directory"
    elif [ -f $file ]; then
        echo "$(basename $file) is file"
    fi
done >output.txt

函数

函数是一个脚本代码块,可以为其命名并在代码中任何位置重用。

定义函数

可以使用 function 来定义函数,函数需要在调用前定义:

function 函数名(){
    程序段
}

调用函数

使用 函数名 参数1 参数2... 格式来调用函数。函数可以在内部使用调用时传入的参数。例如 $0 代表函数名,$1 代表第一个参数。

函数返回

使用 return 命令来退出函数并返回特定的退出状态码,用来控制函数状态。

#!/bin/bash
function sh1(){
    echo $[ $1 + 1 ]
    return 1
}
sh1 100
echo "Exit status is: $?"

退出状态码必须是 0~255。

函数变量

脚脚本内任意位置定义的有效变量都是全局变量。要在函数使用局部变量,在函数内使用 local 定义:

function taax(){
    local valuea=51
}

数组变量

函数中使用数组需要将数组变量的值分解成单个的值,然后将这些值作为函数参数使用。在函数内部可以将所有的参数重新组合成为一个新的数组变量:

#!/bin/bash
function testarr(){
    local newarray
    newarray=($(echo "$@"))
    echo "The array: ${newarray[*]}"
}
myarray=(1 2 3 4)
testarr ${myarray[*]}

函数递归

shell 中函数可以递归调用,也就是可以调用自己产生的结果:

#!/bin/bash
function pingf() {
    local temp=$(($1 - 1))
    if [ $temp -gt 1 ]; then
        pingf $temp
    else
        echo $temp
    fi
}
read -p "Type Number:" mynum
pingf mynum

函数库

可可以将函数定义成函数库文件,然后在其他脚本中引用。例如定义一个简单的 sh01.sh 文件:

#!/bin/bash
function sh1(){
    echo $[ $1 + 1 ]
}

在其他脚本中调用 sh01.sh 文件要使用 source 命令,这样确保引用的脚本不会在子 shell 中运行,而获取不到引用脚本中的变量或函数:

#!/bin/bash
. ~/sh01.sh
sh1 3

也可以通过这一方式来调用系统函数库。

重定向

在脚本中要重定向输出,除了和命令行一样的方法外,还能通过 exec 命令设置脚本执行期间重定向某类文件描述符:

#!/bin/bash
exec 2>stderror.txt
echo "this is stdout"
exec 1>stdout.txt
echo "stdout2file"
echo "stderror2file">&2

上面的运行结果第一个输出会在屏幕显示,第二个标准输出存到了 stdout.txt,第三个错误输出使用 >&2 来重定向到文件 stderror.txt,这是与命令行重定向稍微有区别之处。

重定向输入也是通过 exec 命令将文件指向文件描述符 0:

#!/bin/bash
exec 0<output.txt
while read out_line; do
    echo "$out_line"
done

除此之外,还可以重定向到其他文件描述符(3~8)来自定义作用。例如用文件描述符 3 储存标准输出:

#!/bin/bash
exec 3>&1
exec 1>stdout.txt
echo "stdout2file1"
exec 1>&3
echo "stdout"

要关闭文件描述符使用 exec 3>&-

创建临时文件

可以使用 mktemp -t 命令在 /tmp 下新建一个临时文件,使用完即可删除:

#!/bin/bash
tempfile=$(mktemp -t testfile.XXXX)
echo "testline1">$tempfile
cat $tempfile

创建临时目录使用 mktemp -d 命令在 /tmp 下新建:

#!/bin/bash
tempdir=$(mktemp -d testdir.XXXX)
cd $tempdir
tempfile=$(mktemp testfile.XXX)
echo "tempdir&tempfile">$tempfile
cat $tempfile

创建菜单

select 命令可以创建出菜单,获取输入并自动处理:

#!/bin/bash
function sh1() {
    echo "sh1 is runing"
}
function sh2() {
    echo "sh2 is runing"
}
PS3="Enter:"
select youropt in "Exit" "show sh1" "show sh2"; do
    case $youropt in
    "Exit")
        break
        ;;
    "show sh1")
        sh1
        ;;
    "show sh2")
        sh2
        ;;
    *)
        clear
        echo "Wrong input"
        ;;
    esac
done
clear

运行结果:

[root@server1 bin]$ bash sh01.sh 
1) Exit
2) show sh1
3) show sh2
Enter:2
sh1 is runing

如果想要制作出像内核功能选择那样的窗口,可以使用 dialog 工具。

调试脚本

可以使用 bash 的 -n 参数来检查脚本有无语法错误:

[root@101c7 bin]$ bash -n sh11.sh 
sh11.sh: line 8: syntax error: unexpected end of file

没有错误什么也不会输出。有错误会显示错误所在行与类型。可以和 -v 连用得到一个详细的输出。

使用 -x 参数将执行过程全部列出来:

[root@101c7 bin]$ bash -x sh11.sh 
+ s=1
+ (( i=101 ))
+ (( i<=110 ))
+ s=101
+ (( i=i+2 ))
+ (( i<=110 ))
+ s=10403

+ 开头的行表示命令串,实际输出没有 +

另外可以设置调试的范围,在希望开始调试的地方插入 set -x 命令,在希望结束调试的地方插入 set +x 命令。