shell 编程

作者IYATT-yx

4 月 25, 2021

最近更新于 2022-04-18 10:38

介绍

shell 本身是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。shell 既是一种命令语言,又是一种程序设计语言。作为命令语言,它交互式地解释和执行用户输入的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。

当一个用户登录 Linux 之后,系统初始化程序 init 就为登录的用户运行一个名为 shell (外壳)的程序。shell 其实就是一个命令行解释器,它为用户提供了一个向 Linux 内核发送请求以便运行程序的的界面系统程序,用户可以用 shell 来启动、挂起、停止,甚至编写一些程序。

目前流行的 shell 有 ash、bash、ksh、csh 和 zsh 等,平时我用的都是 Debian系Linux,目前 Debian、Ubuntu 默认终端都是 bash,我现在用的 Kali Linux 以前也是默认 bash,现在的新版本改为 zsh 了(记得之前我用的 Kali Linux 2020.1 还是 bash,现在用的 2021.1就是 zsh 了)。(补:bash 有40个内部命令,因为可以使用方向键查阅和快速输入并修改命令,以及可以通过已经输入的部分,按Tab匹配命令等基本易用的特性,是大多数 Linux 的默认 shell;zsh 是 Linux 最大的 shell 之一,共有84个内部命令)

对于我来说,zsh 相对 bash,感觉最明显的差异就是多了“预测”,假如你之前执行过一个命令,那么当你再次输入且只输入了一部分的时候,zsh就会自动给你显示剩下的部分出来,如果显示的预测符合你要想继续输入的,直接按右键,就上膛了,回车执行。当然如果执行过的命令里有多个开头部分有一段一样或者是没有执行过的命令,那么就继续手动输吧。同时输入命令伴随颜色变化,当你已经输入的部分是个不存在的命令时显示为红色,当已经输入部分为存在的命令时变为绿色(基于 Kali Linux 2021.1 zsh 默认配色方案下的)。当已经输入的路径(准备操作路径或文件时)或者是已经输入的部分再补上某些字母后 是系统中确实存在的情况下,路径会显示下划线,而如果中间名字输错了(路径错了),下划线就没有了,可以十分方便的知道输入错误。

使用下面命令可以查看当前使用的终端:

echo $SHELL

修改默认终端:

修改前先确认自己的系统中是否已经安装对应的 shell 程序,使用 which 命令查询,比如我查 sh ,显示出了它的路径,说明已经安装的

然后编辑 /etc/passwd ,注意需要以 root 权限编辑才能保存

第一行是系统 root 用户,行尾就是设置它的默认终端的路径。下面那行是我系统的默认普通用户,也都是在行尾设置它的默认终端路径。如果要修改,直接改就行了,然后保存,重启系统。

shell 程序设计基础

输入输出重定向

在 Linux 中,每一个进程都有3个特殊的文件描述符:标准输入(Standard Input,0)、标准输出(Standard Output,1)和标准错误输出(Standard Eoor,2)。这3个特殊的文件描述符使进程一般情况下接受标准输入终端的输入,同时由标准终端来显示输出,Linux 同时也允许使用者使用普通的文件或管道来取代这些标准输入输出设备。在 shell 中,使用者可以利用 “>”和“<“来进行输入输出的重定向。

比如: 命令 > 文件名 ,将命令的执行结果保存到文件中。用 >> 将命令执行输出追加到文件尾。

管道(pipe)

pipe 同样可以再标准输入输出和标准错误输出间做代替工作,这样一来,可以将某个程序的输出送到另一个程序的输入,使用符号 ” | “.

比如:ls -l / | grep bin,利用 ls 查询 / 路径下的文件,并将输出的结果通过管道传入 grep ,使用 grep 筛选出带有 bin 关键词的,再输出到终端。

前台和后台

在 shell 下,一个新进程的产生默认是前台方式执行(下一个命令必须等该命令运行结束后才可输入),如果在命令后加上 & ,则以后台方式运行(进程在后台保持运行的同时,还可以在当前终端输入执行其它命令)。

shell 定义的环境变量

shell 在开始执行时就已经定义了一些和系统工作环境有关的变量,用户可以重新定义这些变量,常用的 shell 环境变量有:

· HOME 用于保存注册目录的路径(家目录)

· PATH 用于保存用冒号分隔的目录路径名,shell 将按 PATH 变量中所给的顺序搜索这些目录,找到的第一个与命令名称一致的可执行文件将被执行。(类似于 Windows 下的环境变量 PATH)

· TERM 终端类型

· UID 当前用户的标识符

· GID 当前用户组的标识符

· PWD 当前工作目录的路径

· PS1 主提示符,在特权用户下,默认主提示符为 #,在普通用户下,默认主提示符为 $

· PS2 在 shell 接受用户键入命令的过程中,如果用户在输入行的末尾输入 \ 然后回车,显示这个辅助提示符,提示用户继续输入命令的其余部分,默认的辅助提示符为 >

用户自定义变量

在定义变量时,变量名前不应加符号 $ ,在引用变量的内容时才加;在给变量赋值时,等号两边不能留空格,若变量保存的内容中包含了空格,则整个字符串都要用双引号括起来。

当我们需要只读变量时,可以用下面的命令。

readonly 变量名

在任何时候,建立的变量都只是当前 shell 的局部变量,如果在当前shell 中执行一个shell程序,这个程序是访问不了我们定义的变量的。

(当你在一个 shell 中运行 一个 shell 脚本的时候,当前 shell 会为这个脚本创建一个子 shell 并在里面执行shell脚本,在这个子 shell 中是访问不了你在父 shell 中定义的变量的)

当然也有办法解决这个问题,使用

export 变量名

export 变量名=变量值

两种形式都可以,那么如果在当前 shell 使用 export 定义了一个变量,你继续在当前终端执行 shell 脚本,运行这个脚本的子 shell 也会获得变量的拷贝,所以该 shell 脚本也可以访问这个变量了。

位置参数

位置参数是一种在调用 shell 脚本 的命令行中按照各自的位置决定的变量,即在程序名后输入的参数。位置参数之间用空格分隔,shell 脚本中依次取参数为 $n( $1 , $2, $3 ……)另外 $0 不是参数,而是执行的命令本身。$* 代表所有位置参数的内容。$@ 也代表所有位置参数的内容,不过把每个参数区分对待(for in 可以一个一个的取出来)。$# 代表位置参数的个数。

预定义变量

预定义变量和环境变量类似,也是 shell 一开始就定义了的变量,所不同的是,用户只能根据 shell 的定义来使用这些变量,而不能重定义。所有预定义变量都是由符号 $ 和另一个符号组成的,常用的 shell 预定义变量有

$? 命令执行后返回的状态。Linux 中,命令退出状态为 0 表示该命令正确执行,任何非0 值表示运行出错。C/C++中 exit 退出值以及main函数的return值就是这个意义)

$$ 当前进程的进程号。常用于临时文件命名,保证名字不会重复。

$! 后台运行的最后一个进程号

$0 当前执行的进程名

参数置换的变量

shell 提供了参数置换功能以便用户可以根据不同的条件来给变量赋不同的值。参数置换的变量有4种,这些变量通常与某一个位置参数相联系,根据是否已经设置指定的位置参数决定变量的取值。

(1)变量=${参数-word} 如果设置了参数,则用参数的值置换变量的值,否则用 word 置换。

(2)变量=${参数=word} 如果设置了参数,则用参数的值置换变量的值,否则把变量设置为 word,然后用 word 替换参数的值。注意位置参数不能用于这种方式,因为 shell 程序中不能为位置参数赋值。

(3)变量=${参数?word} 如果设置了参数,则用参数的值置换变量的值,否则就显示 word 并从 shell 种退出;如果省略了 word,则显示标准信息。常用于出错指示。

(4)变量=${参数+word} 如果设置了参数,则用 word 置换变量,否则不进行置换。

运行 shell 程序的方法

用户可以使用任何文本编辑器来编写 shell 程序,因为 shell 程序是解释执行的,不需要编译。按照 shell 编程的惯例,以 bash 为例,程序的第一行一般为 ”#!/bin/bash”,其中 # 表示该行是注释,符号 “!” 告诉 shell 运行后面的命令,并用文件的其余部分作为输入,即用 /bin/bash 去执行 shell 的内容。

第一种

bash shell程序名

第二种

当用编辑器生成一个文件时,系统赋予文件的权限都是 “rw-r–r–“,没有执行权限。则首先使用chmod为文件增加执行权限(x)

chmod a+x 文件名

然后就可以直接将 shell程序文件 当作可执行文件运行

./文件名

shell 程序设计的流程控制

和一般的高级程序设计语言一样,shell 也提供了用来控制程序执行流程的命令,包括条件分支和循环结构,用户可以用这些命令建立非常复杂的程序。

与传统语言不同的是,shell 用于指定条件值得不是布尔表达式,而是命令和字符串。

test 命令

test 命令用于检查某个条件是否成立,它可以进行数值、字符串和文件3个方面的测试,其测试符和相应的功能如下:

(1)数值测试:

-eq 等于则为真

-ne 不等于则为真

-gt 大于则为真

-ge 大于等于则为真

-lt 小于则为真

-le 小于等于则为真

(如果知道对应的英文单词的话,其实都不用刻意去记:equal 等于;not equal 不等于;greater 大于……..)

(2)字符串测试

=或== 等于则为真

!= 不相等则为真

-z 字符串 字符串为空则为真

-n 字符串 字符串不为空则为真

(3)文件测试

-e 如果文件存在则为真

-r 如果文件存在且可读则为真

-w 如果文件存在且可写则为真

-x 如果文件存在且可执行则为真

-s 如果文件存在且至少有一个字符则为真

-d 如果文件存在,且为目录则为真

-f 如果文件存在且为普通文件则为真

-c 如果文件存在且为字符设备文件则为真

-b 如果文件存在且为块设备文件则为真

Linux 还提供了 与(-a)、或(-o)、非(!)3个逻辑操作符用于将测试条件连接起来。

注释

单行注释使用 #

# 这是一行注释

多行注释

:<<结束标志
第一行注释
第二行注释
第三行注释

“结束标志” 四个字可以自行替换,只要多行注释的结束也是一样的就行

结束标志
:<<end
第一行注释
第二行注释
第三行注释
end

if 条件语句

一般格式

if 条件命令串
then
    条件为真时执行的语句
elif 条件命令串
then
    条件为真时执行的语句
else
    前面条件都不满足时执行的语句
fi

示例

#!/bin/bash

if test 1 -eq 1
then
    echo "test命令使用示例 1"
fi

# 可以用中括号表示 test ,注意需要左括号后和右括号前留空格
if [ 1 -eq 1 ]
then
    echo "test命令使用示例 2"
fi
#!/bin/bash
:<<end
接受一个参数:
    1       输出A
    2       输出B
    其它    提示错误
end

# 参数只能为1个
if [ $# -ne 1 ]
then
    echo "必须是有1个参数"
    exit 1
fi

# 根据不同参数有不同输出
if [ $1 == 1 ]
then
    echo "A"
elif [ $1 == 2 ]
then
    echo "B"
else
    echo "参数错误!"
fi

for 循环

一般格式

for 变量名 [in 数值列表]
do
    执行的语句
done

变量名自己定,然后会在循环中依次将数值列表中的值赋给这个变量。如果省略了 in 和后面的数值列表,那么变量会在循环中依次获得位置变量的值(即参数),循环中执行的语句位于 do 和 done 之间。示例:

#!/bin/bash

echo "第一种循环:"
LIST="你 好 , 中 国"
for VAR in $LIST
do
    echo $VAR
done

echo -e "\n第二种循环:"
for VAR
do
    echo $VAR
done

while、until 循环

一般格式

while 条件为真循环
do
    要循环执行的语句
done

until 条件为假循环
do
    要循环执行的语句
done

示例:

#!/bin/bash

VALUE=0
while [ $VALUE -lt 3 ]
do
    echo $VALUE
    ((VALUE++))
done

VALUE=0
until [ $VALUE -ge 3 ]
do
    echo $VALUE
    ((VALUE++))
done

case 条件选择

case 条件选择为用户提供了根据字符串或变量的值从多个选项中选择一项的方法,其格式为:

case string in
表达式1)
    执行语句
    ;;
表达式2)
    执行语句
    ;;
......
*)
    执行语句
    ;;
esac

shell 通过计算 string 的值,将其结果依次与表达式1、2……等进行比较,知道找到一个匹配的表达式为止,如果找到了则执行对应的语句,直到遇到一对分号。

在 case 表达式中也可以使用 shell 的通配符(“*”、“?”、“[]”),通常用“*”作为 case 命令最后的表达式,用于在前面找不到任何相应匹配项时执行。(类似于 C/C++ 中 switch 的 default)

示例

#!/bin/bash

# 检查参数个数
if [ $# -ne 1 ]
then
    echo "有且仅需要输入1个参数"
    exit 1
fi

# 检查参数是否合乎要求,只能是一个字符
if [ ${#1} -ne 1 ]
then
    echo "参数只能包含一个字符"
    exit 1
fi

# 使用 case 条件选择判断
case $1 in
[A-Z])
    echo "输入的是一个大写字母"
    ;;
[a-z])
    echo "输入的是一个小写字母"
    ;;
[0-9])
    echo "输入的是一个数字"
    ;;
*)
    echo "输入的是未知符号"
    ;;
esac

无条件控制语句 break 和 continue

break 用于立即中止当前循环的执行,而 continue 用于跳过本次循环后面的语句,进入下一次循环。仅可在 do 和 done 之间才有效。

函数定义

在 shell 中还可以定义函数,其基本格式为

函数名()
{
    语句
}

也可以在定义的函数名前加上关键词 function ,并与函数名间隔一个空格

调用函数时

函数名 参数1 参数2 ...

在函数定义的时候不用带参数说明,但是在调用函数时可以传入参数,在函数内部,这些参数被被赋予相应的位置参数 $1、$2、$3……。另外在函数体中可以使用 return 返回值,在调用完函数后,马上获取 $? (上一个命令执行后返回的状态)即可得到函数返回值。函数体中没有写 return,那么就默认是 return $?,即函数体中最后执行的一条命令的退出值作为函数的返回值。

#!/bin/bash

testFunction()
{
    echo $1

    return 0
}

testFunction 测试函数使用
echo 函数的返回值是: $?

命令分组

在 shell 中有两种命令分组的方法——“()”和“{ }”,当 shell 执行()中的命令时将再创建一个新的子进程,然后这个子进程去执行“()”中的命令。当用户在执行某个命令时不想让命令运行时对状态集合(如位置参数、环境变量、当前工作目录等)的改变影响到下面语句的执行时,就应该把这些命令放在“()”中,这样就能保证所有的改变只对子进程产生影响,而父进程不受任何影响。“{ }”用于将顺序执行的命令的输出结果用于另一个命令的输入(管道方式)。当我们要真正使用“()”和“{ }”时(如计算表达式优先级),则需要在其前面加上转义符“\”,以便让 shell 知道它们不是用于命令执行的控制。

用 trap 命令捕捉信号

trap 命令用于在 shell 程序中捕捉信号,捕捉到信号后可以有3种反应的方式。

(1)执行一段程序来处理这一信号

trap 命令在 shell 接收到与 signal-list 清单中数值相同的信号时,执行双引号中的命令串。

trap 'commands' signal-list
trap "commands" signal-list

另外,在 trap 命令中,单引号和双引号是不同的,当 shell 程序第一次碰到 trap 命令时,将 commands 中的命令扫描一遍,此时若 commands 是用单引号括起来的话,那么 shell 不会对 commands 中的变量和命令进行替换,否则 commands 中的变量和命令将用作当时具体的值来替换。

使用命令 kill -l 查看信号有哪些,前 31 个为不可靠信号,后面的为可靠信号(不过 Kali 中只有 31 个信号)。后面 进程间通信 信号 部分会继续详谈。

Kali Linux 2021.1

Ubuntu 20.04

下面是信号解释表(图片来源:https://blog.csdn.net/qq_43038236/article/details/108228488)

特别强调:9号命令不可捕获,你也捕获不了,任何进程收到这个信号都会被无条件杀死。后面进程间通信还会说这个信号的,它也是进程间通信的一种方式。

· 接收信号的默认操作;

为了恢复信号的默认操作,使用下面的 trap 命令

trap signal-list

· 忽略这一信号。

trap "" signal-list
  • 下面写一个示例程序:

这个程序会循环输出 var 的值(保持自增自增),同时在循环中利用 trap 捕获 3号信号(QUIT),在运行这个 shell 程序的时候按 Ctrl + \ 就是发送 QUIT 信号,trap接收到信号后执行 echo 停止程序; sleep 3; exit 0 (向终端输出 ”停止程序“,然后休眠3秒,之后退出)。

#!/bin/bash

var=0
while true
do
    echo $var
    ((var++))
    sleep 1
    trap "echo 停止程序; sleep 3; exit 0" QUIT
done

bash 程序的调试

在编程的过程中难免会出错,有时调试程序比编写程序花费的时间还要多,shell 程序同样如此。

shell 程序的调试主要是利用 bash 命令解释程序的选项。几个常用的参数选项为:

-e 如果一个命令失败就立即退出

当 shell 运行时,若遇到不存在或不可执行的命令、重定向失败或命令非正常结束等情况,如果未经重新定向,该出错信息会打印在终端上,而 shell 程序仍将继续执行。

-n 读入命令但是不执行它们

-u 置换时把未设置的变量看作出错

未置变量退出特性允许用户对所有变量进行检查,如果引用了一个未赋值的变量,则会终止 shell 程序的执行。shell 通常允许未置变量的使用,在这种情况下,变量的值为空。如果设置了未置变量退出选项,则一旦使用了未置变量就会显示错误信息,并终止程序的运行。

-v 当读入shell 输入行时把它们显示出来

-x 执行命令时把命令和它们的参数显示出来

调试 shell 程序的主要方法是利用 shell 命令解释程序的 -v 和 -x 选项来追踪程序的执行。-v 选项使 shell 程序在执行的过程中,把它读入的每一个命令行都显示出来,而 -x 选项使 shell 程序在执行程序的过程把它执行的每一个命令在行首用一个 + 加上命令名显示出来,并把每一个变量和该变量所取得值也显示出来。

除了使用 shell 得 -v 和 -x 选项以外,还可以在 shell 程序内部采取一些辅助调试得措施。例如,可以在 shell 程序得一些关键地方使用 echo 命令把必要得信息显示出来,它得作用相当于 C 中的 printf 语句,这样就可以知道程序运行到什么地方以及程序的状态。

  • 上面的的所有选项也可以在 shell 程序内部用 set -选项 的形式引用,而 set +选项 则禁止该选项起作用。如果只是想对程序的某一部分使用某些选项的时候,则可以将该部分用上面两个语句包围起来。

bash 的内部命令

bash 命令解释器包含了一些内部命令,内部命令在目录列表中是看不见的,它们由 shell 本身提供。常用的命令有 echo、eval、exec、export、readonly、read、shift、wait 和 ”.“,下面简单介绍其命令格式和功能。

(1)echo

echo arg

在屏幕上打印出由 arg 指定的字符串

(2)eval

eval args

当 shell 程序执行到 eval 语句时,shell 读入参数 args,并将它们组合成一个新的命令,然后执行。

(3)exec

exec 命令参数

当 shell 程序执行到 exec 命令时,不会去创建新的子进程,而是转而去执行指定的命令,当指定的命令执行完时,该进程,也就是最初的 shell 程序就终止了,所以 shell 程序中 exec 命令后面的语句将不再执行。(有点类似于 Linux 系统 API 的 exec 族函数,在写多进程程序时,使用 exec 族函数用指定的 命令/程序 替换原有进程的代码。后面也会单独谈到的。)

(4)export

export 变量名

或

export 变量名=变量值

shell 程序可以用 export 命令把它的变量向下带入子 shell ,从而让子进程继承父进程中的环境变量,但子 shell 不能用 export 命令把它的变量向上带入父 shell。不带任何变量名的的 export 命令将显示出当前所有的 export 变量。

(5)readonly

readonly 变量名

将一个用户定义的 shell 变量标识为 只读,不带任何参数的 readonly 命令将显示出所有的只读变量。

(6)read

read 变量名表

从标准输入设备读入一行,分解成若干字,赋值给 shell 程序内部定义的变量。

(7)shift

shift 语句按下面方式重新命名所有的位置参数变量,$2 变为 $1,$3 变为 $2 ……在程序中每使用一次 shift 语句,都使所有的位置参数依次向左移动一个位置,并使位置参数 $# 减 1,直到减为 0.

(8)wait

shell 程序在后台等待启动的所有子进程结束,wait 的返回值总为真。

(9)exit

退出 shell 程序,在 exit 命令后可有选择的指定一个数字作为返回状态。

(10)”.“(点)

. shell程序文件名

使 shell 程序读入指定的 shell 程序文件并依次执行文件中的所有语句(即代替 source 命令)。

作者 IYATT-yx