shell 编程(三):语法结构 - 高级


Shell 语法结构(高级部分)

一、条件测试

在 shell 脚本中对某一个条件进行测试的命令是:test 或者 [ ,该命令可以测试一个条件是否成立。

  • 如果测试结果为真,则命令的Exit Status为 0
  • 如果测试结果为假,则命令的Exit Status为 1(注意与C语言的逻辑表示正好相反)

例如测试两个数的大小关系:

wx@ubuntu:~/shell/01$ var=2
wx@ubuntu:~/shell/01$ test $var
wx@ubuntu:~/shell/01$ echo $?
0
wx@ubuntu:~/shell/01$ test $var -gt 5
wx@ubuntu:~/shell/01$ echo $?
1
wx@ubuntu:~/shell/01$ [ $var -gt 5 ]
wx@ubuntu:~/shell/01$ echo $?
1
wx@ubuntu:~/shell/01$ [ $var -gt 1 ]
wx@ubuntu:~/shell/01$ echo $?
0

虽然看起来很奇怪,但左方括号 /[ 确实是一个命令的名字,传给命令的各参数之间应该用空格隔开,比如,$VAR、-gt、3、] 是 /[ 命令的四个参数,它们之间必须用空格隔开。

命令 test 或 [ 的参数形式是相同的,只不过 test 命令不需要 ] 参数。

以 [ 命令为例,常见的测试命令如下所示:

[ -d DIR ]              如果DIR存在并且是一个目录则为真
[ -f FILE ]             如果FILE存在且是一个普通文件则为真
[ -z STRING ]           如果STRING的长度为零则为真
[ -n STRING ]           如果STRING的长度非零则为真
[ STRING1 = STRING2 ]   如果两个字符串相同则为真
[ STRING1 != STRING2 ]  如果字符串不相同则为真
[ ARG1 OP ARG2 ]        ARG1和ARG2应该是整数或者取值为整数的变量,OP是-eq(等于)-ne(不等于)-lt(小于)-le(小于等于)-gt(大于)-ge(大于等于)之中的一个

示例如下:

wx@ubuntu:~/shell/01$ mkdir test
wx@ubuntu:~/shell/01$ [ -d test ]
wx@ubuntu:~/shell/01$ echo $?
0
wx@ubuntu:~/shell/01$ [ -f test ]
wx@ubuntu:~/shell/01$ echo $?
1
wx@ubuntu:~/shell/01$ s1=hello
wx@ubuntu:~/shell/01$ s2=hello
wx@ubuntu:~/shell/01$ [ "$s1" = "$s2" ]
wx@ubuntu:~/shell/01$ echo $?
0
wx@ubuntu:~/shell/01$ s3=
wx@ubuntu:~/shell/01$ [ "$s1" = "$s3" ]
wx@ubuntu:~/shell/01$ echo $?
1
wx@ubuntu:~/shell/01$ [ "$s1" = $s3 ]
-bash: [: hello: unary operator expected
# 直接爆出语法错误,错误原因在于=右边需要一个字符串但是右边却是一个空,
# 也就是什么都没有,这样shell就会认为这是一个语法错误
# 正确的写法是在变量上加上双引号,这样展开也是一个空字符串

注意:

  • 作为一种好的Shell编程习惯,应该总是把变量取值放在双引号之中。

shell 还和 c 语言一样也支持对测试条件之间进行 与、或、非 的逻辑运算:

# 非
[ ! EXPR ]          EXPR可以是上表中的任意一种测试条件,!表示逻辑反
# 与
[ EXPR1 -a EXPR2 ]  EXPR1和EXPR2可以是上表中的任意一种测试条件,-a表示逻辑与
# 或
[ EXPR1 -o EXPR2 ]  EXPR1和EXPR2可以是上表中的任意一种测试条件,-o表示逻辑或

例如:

wx@ubuntu:~/shell/01$ [ -d test -a "$var" = "abc" ]
wx@ubuntu:~/shell/01$ echo $?
1
wx@ubuntu:~/shell/01$ [ -d test -a "$var" = "test" ]
wx@ubuntu:~/shell/01$ echo $?
0

二、分支语句

1.if-else

和其他的该机语言类似,shell 中也使用if、then、else、elif、fi来进行分支执行控制,其中 fi 是 if 语句块的结束标志。其基本的格式如下:

if 条件1 ; then
    条件1满足时执行的代码(多行)
elif 条件2 ; then
    条件2满足时执行的代码(多行)
else
    不满足上述条件时执行的代码(多行)
fi

其中,if 条件1 ; then 是两条语句,所以中间得加上 ; 号,也建议使用这种方式书写。当然也可以分开写,这样就不需要加分号了。如下:

if 条件1
then 
    条件1满足时执行的代码(多行)
elif 条件2
then
    条件2满足时执行的代码(多行)
else
    不满足上述条件时执行的代码(多行)
fi

if 命令的参数组成一条子命令,如果该子命令的 Exit Status为0(表示真),则执行then后面的子命令,如果Exit Status非0(表示假),则执行 elif、else 或者 fi 后面的子命令。

if后面的子命令通常是测试命令,但也可以是其它命令。Shell 脚本没有 {} 括号,所以用 fi 表示 if 语句块的结束。

其中,: 是一个特殊的命令,称为空命令,该命令不做任何事,但Exit Status总是真。如下:

if : ; then
    echo "always true!"
fi

接下来用一个实例还演示下分支语句的用法:

#1 /bin/sh
  
# 根据用户输入的内容来判断当前是中午还是下午
echo "is it morning? please answer yes or no"
read YES_NO
if [ "$YES_NO" = "yes" ]; then
        echo "good morning!"
elif [ "$YES_NO" = "no" ]; then
        echo "good afternoon!"
else
        echo "sorry, $YES_NO not recongnized"
        exit 1
fi
exit 0

上例中的read命令的作用是等待用户输入一行字符串,将该字符串存到一个Shell变量中。

此外,Shell 还提供了 **&& 和   ** 语法,和 C 语言类似,具有 Short-circuit 特性,很多 Shell 脚本喜欢写成这样:
test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1)
其中,&& 相当于 “if…then…” ,而   相当于 “if not…then…” 。
&& 和   用于连接两个命令,而上面讲的 -a 和 -o 仅用于在测试表达式中连接两个测试条件,要注意它们的区别,例如,
test "$VAR" -gt 1 -a "$VAR" -lt 3

和以下写法是等价的

test "$VAR" -gt 1 && test "$VAR" -lt 3

2.case-in

case 命令类似与 C 语言中的 switch/case 语句,而 esac 则表示 case 语句块的结束。

C 语言中的 case 语句只能匹配整型或字符型常量、字符串等表达式,而 Shell 脚本的 case 可以匹配字符串和Wildcard,每个匹配分支可以有若干条命令,末尾必须以 ;; 结束,执行时找到第一个匹配的分支并执行相应的命令,然后直接跳到 esac 之后,不需要像 C 语言一样用 break 跳出。其基本格式如下:

case 字符串 in
匹配字符串1)
    执行对应的命令(多条)
    ;;
匹配字符串2)
    执行对应的命令(多条)
    ;;
...
esac

在 case 的匹配字符串中支持通配符的使用。

例如,刚才上面的那个判断根据用户的输入来判断当前是否是早上或下午的例子可以使用 case in 语句块来实现:

#! /bin/sh
  
echo "is it morning? please answer yes or no!"
read YES_NO
case "$YES_NO" in
yes|y|Yes|Y|YES)
        echo "good morning!"
        ;;
[nN]*)
        echo "good afternoon!"
        ;;
*)
        echo "sorry,$YES_NO not recongnized. entry yes or no"
        exit 1
        ;;
esac
exit 0

三、循环语句

1.for循环

Shell 脚本的 for 循环结构和 C 语言很不一样,它类似于某些编程语言的foreach循环。其基本结构如下:

for 变量名 in 值1 值2 ... ; do
    执行语句(多句)
done

例如,遍历多个字符串,并将其打印出来:

#! /bin/sh
  
for FRUIT in apple banana pear; do
        echo "i like $FRUIT"
done

FRUIT 是一个循环变量,第一次循环 $FRUIT 的取值是 apple,第二次取值是 banana,第三次取值是 pear。

再比如,要将当前目录下的 chap0、chap1、chap2 等文件名改为 chap0~、chap1~、chap2~ 等(按惯例,末尾有~字符的文件名表示临时文件),这个命令可以这样写:

#! /bin/sh

for FILE_NAME in chap?; do
    mv $FILE_NAME $FILE_NAME~
done

2.while循环

while 的用法和 C 语言类似。基本结构为:

while 条件判断式 ; do
    条件满足时执行的代码(多句)
done

比如一个验证密码的脚本:

#! /bin/sh
  
# 用户密码验证脚本
COUNTER=1
while [ "$COUNTER" -le 3 ]; do
        echo "entry password:"
        read PASS
        if [ "$PASS" = "mm123" ]; then
                echo "password is correct"
                exit 0
        else
                echo "password is wrong, please try again"
        fi
        COUNTER=$(($COUNTER+1))
done
echo "sorry"
exit 1

3.break 和 continue

在 shell 循环中也支持 break 和 continue 命令。

  • break[n] 可以指定跳出几层循环
  • continue 跳过本次循环,没跳出整个循环。

四、位置参数和特殊变量

shell 当中有很多的特殊变量,其中都是以 $ 开头的。这些变量分别表示一些特殊的意义,以下是这些变量的整理:

$0  相当于 C 语言main函数的 argv[0]
$1$2...    这些称为位置参数(Positional Parameter),相当于 C 语言 main 函数的 argv[1]、argv[2]...
$#  相当于 C 语言 main 函数的 argc - 1,注意这里的#后面不表示注释
$@  表示参数列表 "$1" "$2" ...,例如可以用在 for 循环中的 in 后面。
$*  表示参数列表 "$1" "$2" ...,同上
$?  上一条命令的 Exit Status
$$  当前进程号

以下使用一个例子来演示一下这些变量的作用:

编写文件 06.sh,写入如下内容:

! /bin/sh
  
echo "test---\$0"
echo "$0"
echo "test---\$1"
echo "$1"
echo "test---\$2"
echo "$2"
echo "test---\$3"
echo "$3"
echo "test---\$#"
echo "$#"
echo "test---\$@"
echo "$@"
echo "test---\$*"
echo "$*"

接下来进行执行:

wx@ubuntu:~/shell/01$ ./06.sh aa bb cc

执行结果为:

test---$0
./06.sh
test---$1
aa
test---$2
bb
test---$3
cc
test---$#
3
test---$@
aa bb cc
test---$*
aa bb cc

另外的 $? 和 $$ 可以直接在命令行中直接执行

位置参数可以用 shift 命令 左移

比如 shift 3 表示原来的 $4 现在变成 $1,原来的 $5 现在变成 $2 等等,原来的$1、$2、$3丢弃,$0不移动。

不带参数的 shift 命令相当于 shift 1。例如:

#! /bin/sh
  
echo "$*"
shift 1
echo "$*"
shift 2
echo "$*"

执行结果如下:

wx@ubuntu:~/shell/01$ ./06.sh aa bb cc dd ee
aa bb cc dd ee
bb cc dd ee
dd ee

五、输入输出

1.输出 - echo

echo 显示文本行或变量,或者把字符串输入到文件。命令结构:

echo [option] string

其中支持的选项:

  • -e 解析转义字符
  • -n 不回车换行。默认情况echo回显的内容后面跟一个回车换行。

例如:

wx@ubuntu:~/shell/01$ echo "hello\n\n"
hello\n\n
wx@ubuntu:~/shell/01$ echo -e "hello\n\n"
hello


wx@ubuntu:~/shell/01$ echo -n "hello"
hellowx@ubuntu:~/shell/01$ 

2.输出 - 管道|

可以通过管道把一个命令的输出传递给另一个命令做输入。管道用竖线表示。

例如:

cat myfile | more
ls -l | grep "myfile"
# df -k 查看磁盘空间,找到第一列,去除“文件系统”,并输出
df -k | awk '{print $1}' | grep -v "文件系统"

3.输出 - tee

tee 命令把结果输出到标准输出,另一个副本输出到相应文件。

例如:

# 将当前列表下的文件详细信息打印出来并保存一份到 a.txt 中
ll | tee a.txt

4.输出 - 文件重定向

cmd > file             把标准输出重定向到新文件中
cmd >> file            追加
cmd > file 2>&1        标准出错也重定向到1所指向的file里
cmd >> file 2>&1       标准出错追加
cmd < file1 > file2    输入输出都定向到文件里
cmd < &fd              把文件描述符fd作为标准输入
cmd > &fd              把文件描述符fd作为标准输出
cmd < &-               关闭标准输入

其中这里的 1 2 表示的是文件描述符:

文件描述符   对应的c语言库   描述
0          stdin         标准输入,键盘鼠标
1          stdout        标准输出,显示终端
2          stderr        标准错误输出,显示终端

5.输入 - read

read 可以获取用户从标准输入中所输入的字符串,并将其保存到变量中去。其格式为:

read 变量

六、函数

和 C 语言类似,Shell 中也有函数的概念,但是函数定义中没有返回值也没有参数列表。格式为:

# 函数定义
函数名(){
    执行代码
}

# 函数执行
函数名 变量1,变量2,...

例如:

#! /bin/sh
  
foo(){
        echo "function foo is called"
        echo "$*"
}

echo "---start---"
foo aa bb cc
echo "---stop---"

运行结果如下:

wx@ubuntu:~/shell/01$ ./08.sh
---start---
function foo is called
aa bb cc
---stop---

注意:函数体的左花括号 { 和后面的命令之间必须有空格或换行,如果将最后一条命令和右花括号 } 写在同一行,命令末尾必须有 ; 号。

在定义 foo() 函数时并不执行函数体中的命令,就像定义变量一样,只是给 foo 这个名字一个定义,到后面调用 foo 函数的时候(注意Shell中的函数调用不写括号)才执行函数体中的命令。

Shell 脚本中的函数必须先定义后调用,一般把函数定义都写在脚本的前面,把函数调用和其它命令写在脚本的最后(类似 C 语言中的 main 函数,这才是整个脚本实际开始执行命令的地方)。

Shell 函数没有参数列表并不表示不能传参数,事实上,函数就像是迷你脚本,调用函数时可以传任意个参数,在函数内同样是用 $0、$1、$2 等变量来提取参数,函数中的位置参数相当于函数的局部变量,改变这些变量并不会影响函数外面的 $0、$1、$2 等变量。

函数中可以用 return 命令返回,如果 return 后面跟一个数字则表示函数的 Exit Status。

下面的这个脚本可以一次创建多个目录,各目录名通过命令行参数传入,脚本逐个测试各目录是否存在,如果目录不存在,首先打印信息然后试着创建该目录。

#! /bin/sh

is_directory()
{
  DIR_NAME=$1
  if [ ! -d $DIR_NAME ]; then
    return 1
  else
    return 0
  fi
}

for DIR in "$@"; do
  if is_directory "$DIR"
  then :
  else
    echo "$DIR doesn't exist. Creating it now..."
    mkdir $DIR > /dev/null 2>&1
    if [ $? -ne 0 ]; then
      echo "Cannot create directory $DIR"
      exit 1
    fi
  fi
done

注意 is_directory() 返回 0 表示真返回 1 表示假。

七、调试Shell脚本

shell 脚本提供了一些用于调试脚本的选项,如下所示:

  • -n:读一遍脚本中的命令但不执行,用于检查脚本中的语法错误

  • -v:一边执行脚本,一边将执行过的脚本命令打印到标准错误输出

  • -x:提供跟踪执行信息,将执行的每一条命令和结果依次打印出来

使用这些选项有三种方法:

一是在命令行提供参数

$ sh -x ./script.sh

二是在脚本开头提供参数

#! /bin/sh -x

第三种方法是在脚本中用set命令启用或禁用参数

#! /bin/sh
if [ -z "$1" ]; then
  set -x
  echo "ERROR: Insufficient Args."
  exit 1
  set +x
fi

set -x 和 set +x 分别表示启用和禁用-x参数,这样可以只对脚本中的某一段进行跟踪调试。