带你初识 Shell 脚本的世界

概述

Shell 是一种脚本语言,那么,就必须有解释器来执行这些脚本,常见的脚本解释器有:

  • bash:是 Linux 标准默认的 shell。bash 由 Brian Fox 和 Chet Ramey 共同完成,是 BourneAgain Shell 的缩写,内部命令一共有 40 个。

  • sh :由 Steve Bourne 开发,是 Bourne Shell 的缩写,sh 是 Unix 标准默认的 shell。

另外还有:ash、 csh、 ksh 等。我们可以在终端通过以下命令来查看当前系统支持哪些 shell 类型:

cat /etc/shells

比如我的系统是 Mac OS,那么结果显示如下:

MoosdeMacBook-Pro:Desktop moos$ cat /etc/shells
# List of acceptable shells for chpass(1).
# Ftpd will not allow users to connect who are not using
# one of these shells.

/bin/bash
/bin/csh
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh


Shell 经过了 POSIX 的标准化,使其可以在不同的 Linux 系统上进行移植,它属于解释性语言。本文关注的是 bash,也就是 Bourne Again Shell,由于易用和免费,Bash 在日常工作中被广泛使用。同时,Bash 也是大多数Linux 系统默认的 Shell。Shell 的运行模式大概分为两种:

  • 交互式:在终端直接输入命令进行执行,并等待执行完毕后再去执行下一条命令。

  • 非交互式:以非交互式方式执行的shell就是运行过程中不需要与用户输入输出打交道的shell。其实就是命令的集合,将命令集合直接放在一个脚本文件中,然后由 Bash Shell 读取并执行。一般情况下运维人员会把需要处理的命令逻辑写入脚本文件中,一次执行即可。也就是实现了 Linux 的自动化运维,效率也更高。

本文中讲解的 Shell 脚本都是基于非交互式,这样更容易对某一具体脚本功能做集中化管理。

第一个 Shell 脚本

按照旧例,我们先从一个 Hello world 开始,首先创建一个 sh 格式的文件 hello_world.sh

touch hello_world.sh

接着打开该文件,并加入如下 code:

#!/bin/bash
# first shell 
echo "Hello, world!"

程序很简单,一共就三行,第二行是注释。#! 是一种约定的标记,表示该脚本应该通过什么类型的解释器来执行,可以看到,我们这里使用的是 bash 类型解释器。那么我们的第一个脚本编写好了,应该如何执行呢?其实很简单,只需要在终端执行下面命令:

MoosdeMacBook-Pro:Desktop moos$ chmod +x hello_world.sh 
MoosdeMacBook-Pro:Desktop moos$ ./hello_world.sh 
Hello, world!
MoosdeMacBook-Pro:Desktop moos$ 

chmod +x 命令是为了让 hello_world.sh 拥有可执行权限,接着通过 ./hello_world.sh 就可以成功执行我们的第一个脚本啦,可以看到终端的确打印出了预期的提示。echo 命令是 Shell 的一个内部指令,用于在屏幕上打印出指定的字符串。除此以外还有 printf 命令,可以实现更加强大的格式化输出操作,它们的用法将在后面介绍。

和大多数语言一样,Shell 也拥有完整的语法结构,例如变量、函数、表达式、运算符、语句块等等,其中很多规则和主流语言类似,但作为脚本语言,它也拥有一些特殊的技能,我们下面一起来了解一下。

Shell 中的变量

为了方便,我们通过下面这个例子来看一下如何定义变量:

#!/bin/bash
player_name='Moosphon'
player_country="China"
player_score=96
echo $player_name
echo ${player_country}
player_name=Jack
echo "${player_name}'s score is ${player_score}"

在终端执行上述脚本的结果如下所示:

MoosdeMacBook-Pro:Desktop moos$ ./hello_world.sh 
Moosphon
China
Jack's score is 96

我们在定义变量的时候不需要为其指定具体的数据类型,常见的数据类型有整型、字符串和数组。关于变量的定义,需要注意的是,变量名和等号之间不能有空格,这可能和你熟悉的所有编程语言都不一样。同时,变量的命名须遵循如下规则:

  • 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头
  • 变量名中间不能有空格,可以使用下划线
  • 不能使用标点符号
  • 不能使用bash里的关键字(可用 help 命令查看保留关键字)

当然,除了这些基本操作以外,Shell 还支持通过 Linux 语句来给变量赋值:

for file in `ls /Users/xx/Desktop`
或
for file in $(ls /Users/xx/Desktop)

顾名思义,以上是遍历桌面目录下的所有文件。另外,我们可以在 shell 中定义只读变量,即有点类似于 Java 中的 final 类型变量,一旦赋值以后就不能被修改其值:

#!/bin/bash
myName="Jack"
readonly myName
myName="Tom"
#会报错:/bin/sh: NAME: This variable is read only.

字符串

字符串类型应该是最常用到的数据类型了,它可以通过单引号,双引号,甚至不用引号来定义,上方的例子中已经给出示例。单引号只能原样输出字符串,比较局限,而双引号支持格式化操作,例如添加转义符。也支持占位操作,即上面例子中在字符串中穿插具体变量,虽然是可以直接通过 $paramName 的方式来使用,但会引起歧义,推荐使用 ${paramName} 的方式。这里再来针对字符串几个常见的功能需求来看一下 Shell 中该如何实:

  • 获取字符串长度

    我们可以通过以下语句来获取字符串的长度:

    word="friend"
    echo ${#friend} 
    #输出结果为:6
    
  • 字符串提取

    字符串的提取也很简单:

    welcome_words="Hello everyone, welcome to my kingdom!"
    echo ${welcome_words:6:8}
    #输出结果为:everyone
    

    其中,第一个冒号后面的数字是初始位置,而第二个冒号后的数字则是要截取的长度。

  • 获取子字符串位置

    获取字符串中某个字符的位置需要借助于 expr 命令来求表达式的值:

    welcome_words="Hello everyone, welcome to my kingdom!"
    echo ${welcome_words:6:8}
    echo `expr index "$welcome_words" e`
    #输出结果为:1
    

数组

Shell 中同样拥有数组类型,bash 支持一维数组(不支持多维数组),并且没有限定数组的大小。与其他主流语言类似,数组元素的下标由 0 开始编号,获取数组中的元素要利用下标,下标可以是整数或算术表达式,其值应大于或等于 0。我们可以通过如下方式来定义和使用数组:

#!/bin/bash
player_array=(Jack Tom Bob Lucy)
#or
player_array[0]=Jack
player_array[1]=Tom
player_array[2]=Bob
player_array[3]=Lucy

echo "the first player is ${player_array[0]}"
#结果:the first player is Jack
echo "All players: ${player_array[@]}"
#结果:All players:Jack Tom Bob Lucy
echo "We have ${#player_array[@]} players"
#or
echo "We have ${#player_array[*]} players"
#结果:We have 4 players

定义数组和其他语言类似,可以通过括号声明一个元素集合,并在内部定义元素,也可以通过指定数组下标值的形式来定义数组。值得注意的是,我们可以直接通过 array[@] 来获取所有元素,并可以通过 ${#array[]@} 或者 ${#array[*]} 来获取数组大小。

变量类型

运行 shell 时,会同时存在如下三种变量:

  • 局部变量

    局部变量在脚本或命令中定义,仅在当前 shell 实例中有效,其他 shell 启动的程序不能访问局部变量。

  • 环境变量

    所有的程序,包括 shell 启动的程序,都能访问环境变量,有些程序需要环境变量来保证其正常运行。必要的时候 shell 脚本也可以定义环境变量。

  • shell 变量

    shell 变量是由 shell 程序设置的特殊变量。shell 变量中有一部分是环境变量,有一部分是局部变量,这些变量保证了 shell 的正常运行

本文之前的示例中一直都在使用局部变量,环境变量部分涉及到内容还比较多,本文篇幅有限,后续会专门针对环境变量来发文一篇,详细介绍环境变量的分类以及如何查看和配置。

Shell 的参数传递

如果参数只能控制在脚本代码内部,那么它就失去了灵活性,因此,Shell 提供了参数传递功能,可以让我们在执行脚本时传入参数,就像给一个函数传参一样。我们先来看下面这个简单示例:

#!/bin/bash
echo "输入的玩家昵称: $1"
echo "输入的玩家分数: $2"
echo "玩家信息的种类: $#"
echo "玩家信息: $*"

通过以下命令格式来执行:

➜  Desktop ./hello_world.sh Jack 100
输入的玩家昵称: Jack
输入的玩家分数: 100
玩家信息的种类: 2
玩家信息: Jack 100

我们通过 $1...$n 来表示用户输入的第几个参数变量,可以看到我们在终端输入了两个参数,$1 代表我们输入的第一个参数 Jack,而 $2 则代表输入的第二个参数值 100。除此以外,Shell 还提供了其他特殊字符来处理参数:

参数格式 说明
$# 传递到脚本的参数个数
$* 以一个单字符串显示所有向脚本传递的参数。 如 $*用引号括起来的情况、以"2 … $n"的形式输出所有参数。
$$ 脚本运行的当前进程ID号
$! 后台运行的最后一个进程的ID号
$@ $* 相同,但是使用时加引号,并在引号中返回每个参数。 如 $@" 括起来的情况、以"2" … "$n" 的形式输出所有参数。
$- 显示 Shell 当前使用的选项,与 set 命令功能相同。
$? 显示最后命令的退出状态。0 表示没有错误,其他任何值表明有错误。

Shell 中的运算符

与其他编程语言类似,Shell 中也提供了很多丰富的运算符,下面我们来一起认识一下。

算数运算符

原生 bash 虽然没有提供算数运算功能,但可以通过其他命令来实现,常见的有 awkexpr,一般我们都使用 expr 就可以。举个例子,我们可以通过下面语句实现加法运算:

#!/bin/bash
math_score=94
english_score=90
sum_score=`expr $math_score + $english_score`
echo "学生数学成绩: ${math_score}"
echo "学生英语成绩: ${english_score}"
echo "学生总分为: ${sum_score}"

我们需要注意两点:

  1. 运算符和表达式之间要留有空格,否则会出错
  2. 运算表达式需要被反引号 ``` `` 包裹

下面列举一下常见的算数运算符,并给出用法示例:

运算符 说明 举例(a=5, b=4)
+ 加法 expr $a + $b 结果为 9。
- 减法 expr $a - $b 结果为 1。
* 乘法 expr $a \* $b 结果为 20。
/ 除法 expr $b / $a 结果为 1。
% 取余 expr $b % $a 结果为 4。
= 赋值 a=$b 将把变量 b 的值赋给 a。
== 相等。用于比较两个数字,相同则返回 true。 [ $a == $b ] 返回 false。
!= 不相等。用于比较两个数字,不相同则返回 true。 [ $a != $b ] 返回 true。

关系运算符

关系运算符只支持数字,不支持字符串,除非字符串的值是数字。下表列出了常用的关系运算符,假设变量 a 为 6,变量 b 为 8:

运算符 说明 举例
-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 为 1,变量 b 为 2:

运算符 说明 举例
! 非运算,表达式为 true 则返回 false,否则返回 true。 [ ! false ] 返回 true。
-o 或运算,有一个表达式为 true 则返回 true。 [ $a -lt 2 -o $b -gt 10 ] 返回 true。
-a 与运算,两个表达式都为 true 才返回 true。 [ $a -lt 2 -a $b -gt 10 ] 返回 false。

逻辑运算符

下面看一下 Shell 的逻辑运算符,我们假设变量 a 为 1,变量 b 为 2:

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

字符串运算符

下表列出了常用的字符串运算符,我们假设变量 a 为 "Jack",变量 b 为 "Tom":

运算符 说明 举例
= 检测两个字符串是否相等,相等返回 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。
-c file 检测文件是否是字符设备文件,如果是,则返回 true。
-d file 检测文件是否是目录,如果是,则返回 true。
-f file 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。
-g file 检测文件是否设置了 SGID 位,如果是,则返回 true。
-k file 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。
-p file 检测文件是否是有名管道,如果是,则返回 true。
-u file 检测文件是否设置了 SUID 位,如果是,则返回 true。
-r file 检测文件是否可读,如果是,则返回 true。

值得一提的是,Shell 里面的中括号 ( 即 [] ) 可用于一些条件的测试,上述的运算符一节已经看到它出现了很多次,可以用来做算术比较、字符串比较、文件属性的逻辑判断等等,后续我们编写脚本过程中会经常和它打交道。判断条件如果太多可能容易出错,这时候我们可以通过 test 命令来代替执行条件检测,如:

if [ $var -eq 0 ]; then echo "True"; fi
# 上下等价
if test $var -eq 0; then echo "True"; fi

流程控制语句作为编程语言中至关重要的组成部分,Shell 自然也具备丰富的流程控制语句。思想都是殊途同归,只不过写法上有所差异,需要我们多练习和熟悉。下面我们着重介绍一下其中的条件语句和循环语句。

Shell 中的条件语句

if - else 语句

bash 中的 if-else 语句写法比较特别,我们先从下面这个简单的例子看起:

#!/bin/bash
math_score=94
english_score=90
sum_score=`expr $math_score + $english_score`
# if else
if [ $math_score == 100 ]; then
    echo "perfect score!"
elif [ $math_score -gt 90] && [ $math_score -lt 100 ]; then
    echo "good job!"
else
    echo "normal score"
fi

可以看到,我们需要在 if 语句中额外添加 then 关键字,接着另起一行执行当前条件下的逻辑;另外,elif 关键字就比较好理解了,即常见的 else-if,需要注意的是,我们需要在 if-else 语句最后添加限定符 fi 来表示当前条件语句的结束。此外,上述的脚本程序也可以写成如下格式:

#!/bin/bash
math_score=94
english_score=90
sum_score=`expr $math_score + $english_score`
# if else
if [ $math_score == 100 ] 
then
    echo "perfect score!"
elif [ $math_score -gt 90] && [ $math_score -lt 100 ]
then
    echo "good job!"
else
    echo "normal score"
fi

case 语句

Shell 中的 case 语句可以简单理解为其他语言中的 switch-case 语句,即多选择语句,适用于选择场景较多的情况。下面,我们先看一个简单示例:

#!/bin/bash
echo '请输入你的数学考试成绩:'
read score
case $score in
    99|100) echo '太棒了!'
    ;;
    9[0-8])  echo '很优秀!'
    ;;
    [6-8][0-9])  echo '表现的还可以!'
    ;;
    *)  echo '下次要努力!'
    ;;
esac

从上述示例可以看到,我们 case 语句与其他语言中的写法也不太一样,这里可以做出以下简单总结:

  • case $参数 in 是标准写法格式,每一个条件后需要以 ) 右括号来跟随,固定格式,可以联想记忆为 Java 当中的冒号。
  • 我们可以使用固定值来作为匹配项,也可以使用 | 来分隔多个需要执行相同逻辑的条件,或者用正则表达式来匹配。
  • 每一个条件逻辑处理完要换行紧跟 ;;,可以理解为 Java 当中的 break
  • 至于最后的 *) 格式类似于 Java 中的 default 关键字功能。
  • 最终也需要添加 esac 关键字来表示语句的结束。
  • 顺带一提,这里开头的 read 命令用来获取终端用户的输入内容,并传递给 score 变量。

Shell 中的循环语句

Shell 中同样支持各种循环操作,常见的有for、while、until 等,下面我们来具体看一下它们的用法。

for 循环

Shell 中 for 循环比较简单,示例如下:

#!/bin/bash
score_array=(89 78 99 72 100)
for score in ${score_array[@]}
do
    echo "学生的成绩依次是: ${score}"
done
#or
for score in 89 78 99 72 100
do
    echo "学生的成绩依次是: ${score}"
done

可以看到上面是两种方式的 for 循环,比较简单,就不细说了。

while 循环

关于 while 循环的示例如下:

#!/bin/bash
number=1
while [ $number -le 8 ]
do
    echo "当前数字: ${number}"
    let "number++"
done

上述程序用来输出小于等于 8 的数字,输出结果如下:

当前数字: 1
当前数字: 2
当前数字: 3
当前数字: 4
当前数字: 5
当前数字: 6
当前数字: 7
当前数字: 8

let 命令用于执行表达式,表达式中不需要 $ 符号来修饰变量。当然,提到 while 就不得不说一下比较常用的无限循环了,其实很简单:

#!/bin/bash
while :
do
    echo '德玛西亚,永世长存'
done
#or
while true; do
    echo '德玛西亚,永世长存'
done

until 循环

until 与 while 正好相反,它是在条件为 false 时继续执行,直到 true 时停止循环,我们将上面的用 while 循环实现的输出 1-8 数字的例子用 until 实现一下:

#!/bin/bash
number=1
until [[ $number -gt 8 ]]; do
    echo "当前数字: ${number}"
    let "number++"
done

Shell 中的函数

函数与命令的执行结果可以作为条件语句使用。要注意的是,和 C 语言不同,shell 语言中 0 代表 true,0 以外的值代表 false。针对函数,我们举个简单的示例:

#!/bin/bash
function calculateTotalScore() {
    total_score=0
    score_chinese=$1
    score_math=$2
    score_english=$3
    total_score=`expr ${score_chinese} + ${score_math} + ${score_english}`
    echo "这位学生的总分为: $total_score"
}

calculateTotalScore 92 100 89
#执行结果:这位学生的总分为: 281

函数的传参我们通过 $n 来获取,最终执行的时候只需要在函数后追加参数即可。

其他常见命令

echo 命令先前使用的时候已经讲过了,下面我们来看看与它类似的命令 printf

printf 命令

printf 命令使用引用文本或空格分隔的参数,外面可以在 printf 中使用格式化字符串,还可以定制字符串的宽度、左右对齐方式等。我们举个简单的示例:

#!/bin/bash
echo "学生数学成绩表:"
printf "%-6s %-6d\n" '小明' 98
printf "%-6s %-6d\n" '小红' 89

执行结果如下:

➜  Desktop ./hello_world.sh 
学生数学成绩表:
小明 98    
小红 89 

这里的 %s%d 作为格式替换符,可以作为占位符,根据后续传入的具体值来替换对应位置的符号。上述示例 %-6s 中的 -6 表示表示左对齐,没有 - 符合则表示右对齐,任何字符都会被显示在 6 个字符宽的字符内,如果不足则自动以空格填充,超过也会将内容全部显示出来。下面来看下常见的格式替换符以及其含义:

格式替换符 含义 描述
%s 字符串替换符 对应位置参数必须是字符串或者字符型,否则报错
%d 十进制整数替换符 对应位置参数必须是十进制整数,否则报错
%c 字符替换符 对应位置参数必须是字符串或者字符型,否则报错
%f 浮点数替换符 对应位置参数必须是浮点数或整数,否则报错

具体的效果可以自己尝试在脚本中编写看下效果。此外,printf 命令中还支持转义字符,如示例中的 /n 表示换行符,成功在终端生效了。如果没有指定具体参数的值,那么格式替换符会被数据类型下的默认值代替,如 %s 用 NULL 代替,%d 用 0 代替。

test 命令

Shell 中的 test 命令用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试。我们直接看下面示例:

#!/bin/bash
score_math=99
score_english=68
if test $[score_math] -gt $[score_english]
then
    echo 'Good at math'
else
    echo 'Good at english'
fi

read input_name
target_name="Tom"
if test $input_name = $target_name
then
    echo 'Yes, is Tom!'
else
    echo 'No, not him!'
fi


cd /Destop
if test -e ./hello_world.sh -o -e ./Frame.png
then
    echo '至少有一个文件存在!'
else
    echo '两个文件都不存在'
fi

值得一提的是,除了使用 expr 来 执行算数运算,我们也可以在 [] 来实现算数运算,如:

#!/bin/bash
a=10
b=8
c=3
result=$[a+b*c]
echo "计算结果为: $result"
#计算结果为:34

本文关于 Shell 脚本的基础介绍就到这里,篇幅比较长,只涉及到 Shell 中的基础语法和用法,后续会涉及到具体应用实战和高级用法补充。

你可能感兴趣的:(带你初识 Shell 脚本的世界)