Linux-Shell脚本基础

背景

此前讲述的都是基于命令行的Linux命令,其实那些都是非常常用的,也是基础中的基础。本文基于之前的内容,讲述关于shell编程的一些语言基础和相关的结构化命令,如:条件判断、循环。

内容

脚本基础

之前我们讲,通过;可以在一行之中同时编辑多条命令运行,并且通过()可以将多条命令整合成一条命令运行起来,那么在shell脚本中如何运行多条命令呢?如下:

1
2
3
#!/bin/bash
who
whoami

在shell中,#起注释作用,除了首行以外,首行的#!/bin/bash表示运行该脚本所使用的程序。编写完成以后,如果你直接运行该文件(run.sh),则会提示command not found,这个是因为我们没有将shell脚本所在的路径纳入到PATH搜索环境中。解决方案如下:

1
PATH=$PATH:$(pwd)

或者,如果不想修改PATH的搜索路径,则通过指定脚本具体的路径来运行

1
./run.sh

但是这么运行的话,会提示Permission denied,这是因为该文件没有可执行权限,所以需要追加执行权限

1
chmod u+x run.sh

在shell中,有一个输出内容的命令echo

1
2
3
4
5
# 如果输出的内容可以用单引号/双引号包裹即可
echo "内容"

# -n:取消默认输出内容末尾的换行符
echo -n "内容"

同样的在shell脚本中,是允许用户直接访问环境变量的,访问的方式依然是$变量名,其中$在shell中被认为是直接调用变量的特殊符号,因此即便是被双引号包裹,依然不会影响。

1
2
3
4
5
# 比如直接访问登录的用户USER
echo $USER

# 如果需要打印出$符号,则需要转义
echo "\$12"

在shell脚本中允许用户自己定义用户变量,但是这些变量的作用域仅限于当前shell脚本运行的环境下,脚本运行结束,则脚本中定义的用户变量就会失效。

1
2
3
4
5
6
7
# 定义变量的格式依然遵循:修改的时候不要$,引用的时候需要$
变量名="变量值"

# shell脚本会自己判断变量的类型
var1="shuai"
var2=12
echo $var1

通过上述方式给用户变量赋值,是比较死的,有时候我们需要将命令的执行结果赋值给新的变量,这个在shell中也是支持的:

1
2
3
4
5
# 通过``包裹命令
var1=`date`

# 通过$()包裹命令
var2=$(date)

此处有一点需要说明的是,通过上面方式执行的命令,本质是在shell脚本运行的终端下创建了一个子shell来运行。与此相类似的还有通过指定具体路径来运行shell脚本,本质上也是在当前的shell下创建子shell来运行脚本的。

有时候,我们想要将程序运行的内容保存到文件中,就说到了输出重定向:>、>>

1
2
3
4
5
# >:将命令的内容输出到文件中,并且覆盖文件中原本的内容
date > log.txt

# >>:将命令的内容追加到文件的末尾
date >> log.txt

如果我们希望将文件的内容输入到命令中,则需要用到输入重定向:<、<<

1
2
# wc:统计文本中的行数、字符数、字节数
wc < run.sh

比较特殊的是<<,被称为内联输入重定向,直接将命令行的内容作为数据输入给命令

1
2
3
4
# EOF只是起文本标示作用,可以用任意字符替代,但是首尾必须一样
wc << EOF
who is the smart boy
EOF

有时候,我们需要将一个命令的运行结果作为输入发送给另一个命令处理,这个时候就会用到管道命令:|

1
2
3
4
5
# 格式:
命令1 | 命令2 | 命令3 ...

# 比如
cat /etc/passwd | sort | more

在shell中如果需要进行算数运算的话,有三种方式:expr、[]、bc,其中expr在shell中存在算数符号不兼容的情况,因此在bash中,可以使用[][]本身也只在bash程序中支持,但是它和expr一样,只支持整数类的运算,不支持浮点数的运算:

1
2
var1=$[ 1 + 2 ]
var2=$[ $var1 * 2 ]

如果有浮点数运算需要的时候,则需要调用外部命令:bc,它是bash内置的一个计算器,通过管道命令,我们可以直接将相关运算直接提交给bc,然后将结果返回:

1
2
3
4
5
# 格式:
variable=$(echo "options; expressions" | bc)

# scale表示保留小数点后4位,bc程序默认保留0位
var1=$(echo "scale=4;4+5" | bc)

同样的,也可以利用重定向的命令将要计算的输入发送给bc程序进行计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
var1=1
var2=2

# EOF内部的变量不管引用还是修改,都不用加$。EOF外部的变量当引用时需要加$
var3=$(bc <<EOF
scale=4

# $var1是直接调用了上面的用户变量的值,在shell中,它只会将变量的值传递到运算的位置,然后bc去执行得出结果
var4=$var1*$var2
var5=$var1+$var2

# 它会依次打印出所有非赋值表达式计算的结果
var6=var4+var5
var4*var5
EOF
)

echo "the var3 is $var3"

在shell中,shell运行的每一个命令都会记录一个退出状态码,并且用一个环境变量记录最近一次命令执行的退出状态码:$?

1
2
3
4
5
6
7
# 非0表示命令执行错误,0表示命令执行成功
echo $?

# 126:命令不可执行
# 127:没找到命令
# 128:无效的退出参数
# 130:Ctrl+c退出了shell

同样,在shell脚本中,我们可以自己指定退出参数:exit 数值,但是退出的参数数值必须在0~255之间,过大的话,则系统自动截断(求余)

1
exit 12

结构化命令

说到结构化命令,其实对应于程序语言中的判断语句循环语句,首先说到判断语句,在shell中判断语句有两类形式,四种状态,先说if的三种状态:

单一状态的判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 格式
if condition
then
statement
fi

# then 可以和if 写在一行,只要中间以;分割
if condition; then
statement
fi

# 比如
if ls $HOME/.ssh
then
echo "you are a good boy"
fi

两种状态的判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 格式
if condition
then
statement
else
statement
fi

# 比如
if ls $HOME/.ssh
then
echo "you are a good boy"
else
echo "lazy boy"
fi

多种状态的判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 格式
if condition; then
statement
elif condition; then
statement
# 也可以省略else
else
statement
fi

# 比如
if ls $HOME/.ssh; then
echo "you are a good boy"
elif ls $HOME/.profile_history; then
echo "lazy boy"
else
echo "pretty boy"
fi

同样,你也可以直接在if命令中进行if的嵌套

1
2
3
4
5
6
7
8
9
10
if ls $HOME/.ssh; then 
echo "you are a good boy"
elif ls $HOME/.profile_history; then
if ls $HOME/.profile_history/wudashuai; then
echo "lazy boy"
else
echo "没文件"
else
echo "pretty boy"
fi

仔细观察的话,你会发现,在shell中的判断条件非常的奇怪,它们是一些shell命令。这个是因为在shell中,if判断条件成立的情况是指这些shell命令的返回状态码为0,如果非0则不会执行if后面的语句。但是这样的判断方式并不能满足复杂的使用环境,因此在bash shell中还有另外一个命令:test,用于辅助判断语句。它已两种状态存在:test condition、[ condition ]:

1
2
3
4
5
6
7
8
9
# test condition的形式, 如果不写condition,则test会返回非0值
if test condition; then
statement
fi

# [ condition ]的形式, 这种方式是最常用的
if [ condition ]; then
statement
fi

介于test condition的形式不常用,所以下文主要记录[ condition ]的形式,首先[ condition ]中可以进行数值、文本、文件比较,进行数值比较的时候,有一点需要注意:它不是使用>、<等的传统比较符号,而是使用django中gt、lt等方式表示比较符号,并且bash shell中的test只能进行整数比较,不支持浮点数的比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# n1 -eq n2: n1等于n2
if [ $n1 -eq $n2 ]; then
echo "n1=n2"
fi

# n1 -ne n2: n1不等于n2
if [ $n1 -ne $n2 ]; then
echo "n1 >= n2"
fi

# n1 -ge n2: n1大于等于n2
if [ $n1 -ge $n2 ]; then
echo "n1 >= n2"
fi

# n1 -gt n2: n1大于n2
if [ $n1 -gt $n2 ]; then
echo "n1 >= n2"
fi

# n1 -le n2: n1小于等于n2
if [ $n1 -le $n2 ]; then
echo "n1 >= n2"
fi

# n1 -lt n2: n1小于n2
if [ $n1 -lt $n2 ]; then
echo "n1 >= n2"
fi

进行文本比较的时候,则是使用>、<等符号了,然而你会发现>、<与shell中的重定向命令重复导致冲突,所以在进行比较的时候就需要对>、<进行转义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# str1 = str2: str1和str2相等
if [ $str1 = $str2 ]; then
echo "str1=str2"
fi

# str1 != str2: str1和str2不相等
if [ $str1 != $str2 ]; then
echo "str1!=str2"
fi

# str1 > str2: str1大于str2
if [ $str1 \> $str2 ]; then
echo "str1=str2"
fi

# str1 < str2: str1小于str2
if [ $str1 \< $str2 ]; then
echo "str1=str2"
fi

# -n str1: str1长度是否非0
if [ -n $str1 ]; then
echo "str1 长度不为0"
fi

# -z str1: str1长度是否为0
if [ -z $str1 ]; then
echo "str1 长度为0"
fi

说到此处,则有一处需要指明,对于if判断条件中,如果存在空变量、未定义变量,则会产生灾难性的影响,为了避免这种情况,应该在不确定变量是否有值的情况下,需要用-n、-z进行一下判断。

然后是shell编程中常用的文件比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# -e file:检查file是否存在
if [ -e $file ]; then
echo "file is exist"
fi

# -d file:检查file是否存在,并且是一个目录
if [ -d $file ]; then
echo "file is a dirtory"
fi

# -f file:检查file是否存在,并且是一个文件
if [ -f $file ]; then
echo "file is a txt"
fi

# -s file:检查file是否存在,并且非空
if [ -s $file ]; then
echo "file not null"
fi

# -r file:检查file是否存在,并且可读
if [ -r $file ]; then
echo "file could be read"
fi

# -w file:检查file是否存在,并且可写
if [ -w $file ]; then
echo "file could be write"
fi

# -x file:检查file是否存在,并且可执行
if [ -x $file ]; then
echo "file could be excute"
fi

# -O file:检查file是否存在,并且属于当前用户
if [ -O $file ]; then
echo "file belongs to $USER"
fi

# -G file:检查file是否存在,并且默认组与当前用户属组相同
if [ -G $file ]; then
echo "file belongs to $HOME"
fi

# file1 -nt file2:检查file1是否比file2新
if [ $file1 -nt $file2 ]; then
echo "file is newer than file2"
fi

# file1 -ot file2:检查file1是否比file2旧
if [ $file1 -ot $file2 ]; then
echo "file is older than file2"
fi

同样的,对于if也可以使用多条件判断:

1
2
3
4
5
6
7
8
9
# &&:与逻辑
if [ condition ] && [condition]; then
statement
fi

# ||:或逻辑
if [ condition ] || [condition]; then
statement
fi

在bash shell针对if -- then中的数值运算,还提供了另外一种模式:(( condition )),在(( condition ))中,>、<等都和python中的比较一样,不需要进行转义;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# var++num:后加
if (( $var1++2 > $var2 )); then
(( var1 = $var1++2 ))
fi

# var--num:后减
if (( $var1--2 > $var2 )); then
(( var1 = $var1++2 ))
fi

# ++var:先增
if (( ++$var1 > $var2 )); then
(( var1 = $var1++2 ))
fi

# --var:先减
if (( --$var1 > $var2 )); then
(( var1 = $var1++2 ))
fi

# !: 逻辑求反
if (( !( $var1 > $var2 ) )); then
(( var1 = $var1++2 ))
fi

# &&:逻辑与
if (( ( $var1--2 > $var2 ) && ( $var1 == 2 ) )); then
(( var1 = $var1++2 ))
fi

# ||:逻辑或
if (( ( $var1--2 > $var2 ) || ( $var1 == 2 ) )); then
(( var1 = $var1++2 ))
fi

# ~:按位求反
if (( $var1--2 > $var2 )); then
(( var1 = ~$var1 ))
fi

# &:按位与
if (( $var1--2 > $var2 )); then
(( var1 = $var1 & $var2 ))
fi

# |:按位或
if (( $var1--2 > $var2 )); then
(( var1 = $var1 | $var2 ))
fi

同样也对字符串比较提供了额外的方式:[[ condition ]],它除了支持原本[ condition ]的运算符外,同时还提供了模式匹配的功能(正则)。

1
2
3
4
# ==:判断是否符合正则
if [[ $var1 == r* ]]; then
(( var1 = $var1++2 ))
fi

然后就是判断语句的另一种格式:case

1
2
3
4
5
6
7
8
9
10
11
12
13
# 格式
case variable in
pattern1 | pattern2 ) command;;
parttern3 ) command;;
*) default command;;
esac

# 比如
case $var1 in
2 | 3 ) echo "\$var1 is 2 or 3";;
4 ) echo "\$var1 is 4";;
* ) echo "\$var1 not in 2,3,4";;
esac