Shell 脚本简明教程

Shell 脚本简明教程

适用于 bash。

可用命令

Linux 系统中可用的命令除了 Shell 自带的之外,都作为可执行文件存储在 /bin, /usr/bin, /usr/local/bin 目录下。 准确的说,位于变量 $PATH 中的路径下的可执行文件,都可以如同命令一样执行。 如果用户自身还创建了 ~/.local/bin 的话,这个路径也会被添加到 $PATH 变量中。 其他的路径也可以添加进去,但是如果太过杂乱,就很不利于系统的长期使用。

按照 Linux 路径规范,大多数包管理器都会将应用程序安装在上述的三个目录中(排除 /bin,这是系统命令,一般在安装后不会改变)。

alias

可以给命令设置别名,常常用于简化过于长的命令。例如,默认情况下 ls 命令不会输出带颜色的输出,要输出颜色,需要携带参数 --color=auto(在任何情况下都携带颜色则需要 --color=always,大多数 GNU 工具都有此参数)。 在你的 ~/.bashrc 中,通常都有这样的语句:

1
alias ls='ls --color=auto'

这就是声明别名的语法, alias 别名=原始命令。原始命令之所以加单引号,是因为中间存在空格,为了避免 Shell 解析错误,用单引号把它变成一个词。

你可以直接执行 alias 以查看当前声明的所有别名。

管道与重定向

操作符 | 是管道操作符,用于将一个命令的输出导向另一个命令的输入。 学习过 C 语言或其他编程语言的话,都应该知道一个进程拥有三个标准输入输出流(句柄): stdin, stdout, stderr(不仅 Linux、Windows 也如此); 而管道符就是将上一个命令的 stdout 与下一个命令的 stdin 连接起来。

例如,当 ls 命令输出过多,超过了一个屏幕,我们需要用一个 pager 来分页以方便阅读,则可以使用:

1
ls --color=always | less -R

要用 Vim 编辑一下:

1
ls | vim -

vim 需要参数 - 以命令它从 stdin 读取内容。

参数和 stdin, stdout 这两个流无关。把一个命令的输出作为另一个命令的参数这项特性不被 Shell 原生支持, 但是你可以使用名为 xargs 的一个工具。

管道操作符是 Unix 工具功能专一理念的技术基础,它就是将多个工具联合起来使用的手段。

另外还有两个流重定向符: <>。 前者是输入重定向,后者是输出重定向,可以把进程的 stdin/stdout 与文件连接起来。 这个可以看作一个 “箭头”,把一些内容导向了命令:

1
program <stdin.txt >stdout.txt 2>stderr.txt

2> 是把标准错误流重定向。 stdin stdout stderr 的编号分别是 0 1 2。 可以在 /proc/${pid}/fd 下查看、这个目录中存储了目标进程所有打开的输入输出流。

我们可以当场编写一个小程序体验一下:

1
2
3
4
5
6
7
#include <stdio.h>
int main(void) {
FILE *f = fopen("hello.txt", "wb");
getchar();
fclose(f);
return 0;
}

在运行之后,进程会被分配一个 PID,用 ps -ef | grep 'a.out' 搜索,可以通过可执行文件的名称找到它。 这里我运行程序时的 PID 为 4268,去到对应的路径 /proc/4268

1
2
3
4
5
6
7
8
$ ll /proc/4268/fd
total 0
dr-x------ 2 zom zom 0 Jul 20 11:07 ./
dr-xr-xr-x 9 zom zom 0 Jul 20 11:07 ../
lrwx------ 1 zom zom 64 Jul 20 11:08 0 -> /dev/pts/4
lrwx------ 1 zom zom 64 Jul 20 11:08 1 -> /dev/pts/4
lrwx------ 1 zom zom 64 Jul 20 11:08 2 -> /dev/pts/4
l-wx------ 1 zom zom 64 Jul 20 11:08 3 -> /home/zombie110year/hello.txt

变量

Shell 中的变量都是在执行之前由 Shell 解释器 “展开” 的,没有类型系统,可以视作都是字符串。

变量创建

1
2
var='Variable'
echo $var

创建变量时,直接使用 <name>=<value> 的语法,但在使用变量时就必须加上 $ 前缀。 注意 = 两侧不能存在空格,否则 Shell 会把此语句解析为 <命令> <参数>

很多 Shell 脚本中会在变量创建的语句之前使用 export 修饰符,这个修饰符是为了将变量导出当前作用域。 另外还有一个 local 修饰符,则是为了限制作用域(在函数中)。

详见 脚本文件函数

变量引用

在 Shell 脚本中,需要使用 $ 作为前缀,否则会将变量名当作普通的字符串。

1
2
echo $PATH
echo PATH
  • 前者输出 PATH 变量的内容
  • 后者输出 PATH 四个字母

除此之外,还可以用 ${name} 的语法。

这里不得不提到三种特殊的引号: "" 双引号、 '' 单引号和 `` 反引号。

双引号中可以引用变量,常常用做变量的格式化输出,转义序列也会正常工作; 而单引号则是纯文本,在里面的任何字符都会保持原样。

你可以试试

1
2
echo "Path 变量为 $PATH"
echo 'Path 变量为 $PATH'

然后是反引号,当一个程序输出的结果需要赋值给一个变量时使用,与它效果相同的还有括号 $( )

1
2
files=`ls`
files_=$( ls )
1
2
3
4
for i in ${files}
do
echo $i
done

变量删除

可以使用 unset 命令删除一个变量。

算数运算

Shell 中的变量没有数字类型,不能直接运算。 但是可以使用扩展语法 let

1
2
3
4
i=0
let i+=100
echo $i
# 100

可以带 $ 前缀也可以省略, let 命令默认除了数字符号之外都是变量,但注意赋值号两侧不能有空格。 支持加减乘除运算符 +-*/ 以及模运算 %,以及对应的赋值运算符。

或者在 $(( )) 括号中:

1
2
echo $(( 100+200 ))
# 300

运行的结果当然也可以赋值给一个变量。

Shell 变量

Shell 预设了一些变量,有着重要意义:

1
$?      # 上一条命令的返回值

控制流程

命令分隔符

你可以把多条命令都写在一行里,只要使用分号分隔就行了。 分号和回车符的效果是一样的:

1
2
3
4
5
cmd1; cmd2; cmd3
#
cmd1
cmd2
cmd3

还有两种分隔符,就是 &&||, 它们参与流程控制,类似于 if-else 语句。

  • &&:当前一条命令的返回值为 0 时(true),执行后面的命令
  • ||:当前一条命令的返回值不为 0 时(false),执行后面的命令,否则跳过。
1
[[ -e $file ]] && echo "存在" || echo "不存在"

条件判断

要进行流程控制,就必须先解决条件判断的问题。 Shell 中的条件表达式用 0 和其他任意值作为布尔条件。 需要注意, 与大多数编程语言不同, 是 0 为 true, 其他值为 false。 这是因为按照规范,一个出错的命令会返回非 0 值,而正常结束的任务会返回 0 。

要测试一个表达式的值,需要用到 test 命令。不过,更通常的做法是使用 [[ ]] 表达式(注意两侧留有空格):

表达式 含义
[[ $a -eq $b ]] 等于
[[ $a -ne $b ]] 不等于
[[ $a -ge $b ]] 大于等于
[[ $a -le $b ]] 小于等于
[[ $a -gt $b ]] 大于
[[ $a -lt $b ]] 小于
[[ ! $a ]]
[[ $a -o $b ]]
[[ $a -a $b ]]

另外还有一系列文件测试符

表达式 含义
[[ -b $a ]] 块设备文件
[[ -c $a ]] 字符设备文件
[[ -d $a ]] 目录
[[ -f $a ]] 普通文件
[[ -g $a ]] 文件设置了 SGID 位
[[ -k $a ]] 文件设置了 Sticky Bit
[[ -p $a ]] 命名管道
[[ -u $a ]] 文件设置了 SUID 位
[[ -r $a ]] 可读
[[ -w $a ]] 可写
[[ -x $a ]] 可执行
[[ -s $a ]] 空文件
[[ -e $a ]] 文件存在
[[ -S $a ]] Socket 文件
[[ -L $a ]] 符号链接

分支结构

分支结构可以使用语句 if 和 case,前者就是常见的 if-else 语句,后者则类似于 C 语言中的 switch:

1
2
3
4
5
6
if [[ -e $file ]]
then
echo "$file 存在"
else
echo "$file 不存在"
fi

else 分支可以省略,末尾以 fi 标志语句块的结束( if 倒过来 )。 你也可以把换行符都替换成分号,把上面的语句写道同一行。

可以用 elif 简化 else if:

1
2
3
if [[ 0 -eq 2 ]]; then echo "0 == 2";
elif [[ 0 -eq 1 ]]; then echo "0 == 1";
else echo "0 != 1, 2"; fi

另一种条件分支是 case 语句,类似于 C 语言的 switch:

1
2
3
4
5
6
7
8
9
10
11
case $var in
var1)
echo "var1"
;;
var2)
echo "var2"
;;
*)
echo "other"
;;
esac

通过匹配变量 var 的值,执行对应的分支。 ;; 类似于 break

循环结构

有三种循环, while, untilfor

1
2
3
4
while [[ $true ]]
do
echo "true"
done
1
2
3
4
until [[ $false ]]
do
echo "false"
done

while 和 true 相反,前者是条件为真时执行,后者则是条件为假时执行。

for 循环可以遍历一个集合,例如:

1
2
3
4
for i in {0..10}
do
echo $i
done

这个充当 “集合” 的对象,可以是数组,也可以一个由空格分隔的值; 因此,也可以用来遍历 ls 得到的文件:

1
2
3
4
5
for i in `ls`
do
[[ -f $i ]] && echo "$i 是文件"
[[ -d $i ]] && echo "$i 是目录"
done

列表

可以用 (v1 v2 v3) 这样的语法来表示一个列表,每一项用空格分隔。 列表中可以引用变量。

列表生成式

你可以在 Shell 中以 {...} 这样的语法生成列表。 在其他语句执行之前,此列表将会被展开成用空格分隔的值。

1
2
3
4
for i in {a,g,h,i}
do
trueecho $i
done

也可以用在一个字符串的部分中,例如:

1
2
3
4
for i in hello{world,zom}
do
trueecho $i
done
1
2
helloworld
hellozom

可以用逗号分隔其中的每一部分,也可以用 .. 来生成连续的区间。

1
2
3
4
for i in {0..10}
do
trueecho $i
done
1
2
3
4
for i in {a..z}
do
trueecho $i
done

函数

1
2
3
4
5
6
function name() {
for i in $*
do
echo "Hello $i"
done
}

如果要在函数中创建变量,需要用 local 修饰符修饰, 以确定变量的作用域位于函数内。

参数处理

变量 $0, $1 … 对应了 argv[0], argv[1]

也可以用 $* 表示所有参数(用空格分隔)。

返回值与结果

函数的返回值是最后一个命令的返回值,用于流程控制。 函数的结果是向 stdout 输出的内容,可赋值给变量。

调用方法

1
name arg1 arg2 arg3 ...

脚本文件

类似于函数,但是

  1. 脚本中的变量作用域只存在脚本之中,如果要导出到外界,需要用 export 修饰符修饰。
  2. 脚本文件可以用 source script.sh(接受变量的导出)、bash script.sh(新开 bash 进程运行,不会接受导出的变量) 执行,如果要用类似于可执行文件的方法执行,需要编写 shebang: #! /usr/bin/bash,并授予可执行权限 chmod +x script.sh。运行效果等同于 bash script.sh