Home Shell (4)
Post
Cancel

Shell (4)

[toc]

阮一峰老师写的教程,这里做个精简笔记。

1 循环

Bash 提供三种循环语法 forwhileuntiluntilwhile 的反逻辑版。

while 循环,只要满足 condition 条件就一直执行。

1
2
3
while condition; do
  commands
done

until 循环,只要不满足 condition 条件就一直执行。

1
2
3
until condition; do
  commands
done

for in 循环,遍历列表的所有项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for variable in list; do
  commands
done

## 例子1
for i in word1 word2 word3; do
  echo $i
done

## 例子2
for i in *.png; do
  ls -l $i
done

## in list 的部分可以省略,这时 list 默认等于脚本的所有参数 $@。但是,为了可读性,最好还是不要省略,参考下面的例子。
for filename; do
  echo "$filename"
done
# 等同于
for filename in "$@" ; do
  echo "$filename"
done

for 循环,支持 C 语言写法。

1
2
3
4
5
6
7
for (( expression1; expression2; expression3 )); do
  commands
done

for (( i=0; i<5; i=i+1 )); do
  echo $i
done

select 结构,用于生成简单菜单,与 for in 语法一致。

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
select name [in list]; do
  commands
done

## 例子
select brand in Samsung Sony iphone symphony Walton
do
  echo "You have chosen $brand"
done

$ ./select.sh
1) Samsung
2) Sony
3) iphone
4) symphony
5) Walton
#?

## select 与 case 结合使用
select os in Ubuntu LinuxMint Windows8 Windows10 WindowsXP
do
  case $os in
    "Ubuntu"|"LinuxMint")
      echo "I also use $os."
    ;;
    "Windows8" | "Windows10" | "WindowsXP")
      echo "Why don't you try Linux?"
    ;;
    *)
      echo "Invalid entry."
      break
    ;;
  esac
done

2 函数

函数与别名的区别:别名一般用于封装简单的单行命令,函数则用来封装复制的多行命令。

函数总是在当前 Shell 执行,这是跟脚本的重大区别,Bash 执行脚本时会新建一个子 Shell 来执行。如果函数与脚本同名,函数会优先被执行。但函数的优先级不如别名,存在同名的函数和别名时,别名会优先被执行。优先级:别名 > 函数 > 脚本。

函数的定义语法,两种语法等价:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 第一种
fn() {
  # codes
}

# 第二种
function fn() {
  # codes
}

# 删除函数
$ unset -f functionName

# 查看当前 Shell 已定义的函数
$ declare -f
$ declare -f functionName
$ declare -F # 只输出函数名,不包含函数体

2.1 参数变量

函数体内可以使用参数变量,获取函数参数。函数的参数变量,与脚本参数变量是一致的。

  • $1~$9:函数的第一个到第9个的参数。
  • $0:函数所在的脚本名。
  • $#:函数的参数总数。
  • $@:函数的全部参数,参数之间使用空格分隔。
  • $*:函数的全部参数,参数之间使用变量 $IFS 值的第一个字符分隔,默认为空格,但是可以自定义。

如果函数的参数多于 9 个,那么第 10 个参数可以用 ${10} 的形式引用,以此类推。

1
2
3
4
5
6
function log_msg {
  echo "[`date '+ %F %T'` ]: $@"
}

$ log_msg "This is sample log message"
[ 2018-08-16 19:56:34 ]: This is sample log message

2.2 变量作用域

Bash 函数体内直接声明的变量,属于全局变量,整个脚本都可以读取。

1
2
3
4
5
6
7
8
9
10
11
12
fn () {
  foo=1
  echo "fn: foo = $foo"
}

fn
echo "global: foo = $foo"

## 执行结果
$ bash test.sh
fn: foo = 1
global: foo = 1

函数里面可以用 local 命令声明局部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
fn () {
  local foo
  foo=1
  echo "fn: foo = $foo"
}

fn
echo "global: foo = $foo"

## 执行结果
$ bash test.sh
fn: foo = 1
global: foo =

3 数组

3.1 数组创建

数组可以采用逐个赋值或者一次性赋值的方式进行创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
## 逐个赋值
$ array[0]=val
$ array[1]=val
$ array[2]=val

## 一次性赋值
$ array=(value1 value2 ... valueN)
# 或
$ array=(
  value1
  value2
  value3
)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ array=(a b c)
$ days=(Sun Mon Tue Wed Thu Fri Sat)

# 可以在每个值前面指定位置
$ array=([2]=c [0]=a [1]=b)
$ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu [5]=Fri [6]=Sat)

# 只为某些值指定位置,hatter是数组的0号位置,duchess是5号位置,alice是6号位置,没有赋值的数组元素的默认值是空字符串
$ names=(hatter [5]=duchess alice)

# 可以使用通配符
$ mp3s=( *.mp3 )

# 声明数组
$ declare -a ARRAYNAME

# 用户输入存入数组
$ read -a dice

3.2 访问数组元素

单个元素,指定索引或者不指定索引返回数组第一个元素。

1
2
3
4
5
# 注意要加上大括号,否则返回的就是 ${array[0]}[i] 了
$ echo ${array[i]}

# 不指定索引,默认返回 array[0]
$ echo $array

所有元素,使用 @ 和 * 可以返回所有元素。

1
2
3
4
5
6
7
8
$ foo=(a b c d e f)
$ echo ${foo[@]}
a b c d e f

# 配合 for 循环进行遍历
for i in "${names[@]}"; do
  echo $i
done

@* 放不放在双引号之中,是有差别的。

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
$ activities=( swimming "water skiing" canoeing "white-water rafting" surfing )

## 直接遍历,数组 activities 实际包含 5 个成员,但是 for...in 循环直接遍历 ${activities[@]},导致返回 7 个结果。
$ for act in ${activities[@]}; \
do \
echo "Activity: $act"; \
done

Activity: swimming
Activity: water
Activity: skiing
Activity: canoeing
Activity: white-water
Activity: rafting
Activity: surfing

## 加引号,把 ${activities[@]} 放在双引号之中可以保证遍历的正确性。
$ for act in "${activities[@]}"; \
do \
echo "Activity: $act"; \
done

Activity: swimming
Activity: water skiing
Activity: canoeing
Activity: white-water rafting
Activity: surfing

${activities[*]} 不放在双引号之中,跟 ${activities[@]} 不放在双引号之中是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ for act in ${activities[*]}; \
do \
echo "Activity: $act"; \
done

Activity: swimming
Activity: water
Activity: skiing
Activity: canoeing
Activity: white-water
Activity: rafting
Activity: surfing

## 放在双引号之中,所有成员就会变成单个字符串返回。
$ for act in "${activities[*]}"; \
do \
echo "Activity: $act"; \
done

Activity: swimming water skiing canoeing white-water rafting surfing

拷贝一个数组的最方便方法,就是写成下面这样。

1
$ hobbies=( "${activities[@]}" )

上面例子中,数组 activities 被拷贝给了另一个数组 hobbies

这种写法也可以用来为新数组添加成员。

1
$ hobbies=( "${activities[@]" diving )

上面例子中,新数组 hobbies 在数组 activities 的所有成员之后,又添加了一个成员。

3.3 数组属性

长度,空的元素不会计入长度中。

1
2
3
4
5
6
7
8
${#array[*]}
${#array[@]}

$ a[100]=foo
$ echo ${#a[*]}
1
$ echo ${#a[@]}
1

索引,返回所有非空元素的位置。

1
2
3
4
5
6
7
8
${!array[@]}
${!array[*]}

$ arr=([5]=a [9]=b [23]=c)
$ echo ${!arr[@]}
5 9 23
$ echo ${!arr[*]}
5 9 23

3.4 数组元素

提取${array[@]:position:length}

1
2
3
4
5
6
7
8
9
$ food=( apples bananas cucumbers dates eggs fajitas grapes )
$ echo ${food[@]:1:1}
bananas
$ echo ${food[@]:1:3}
bananas cucumbers dates

# 如果省略长度参数 length,则返回从指定位置开始的所有成员。
$ echo ${food[@]:4}
eggs fajitas grapes

追加+=

1
2
3
4
5
6
7
$ foo=(a b c)
$ echo ${foo[@]}
a b c

$ foo+=(d e f)
$ echo ${foo[@]}
a b c d e f

删除unset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ foo=(a b c d e f)
$ echo ${foo[@]}
a b c d e f

$ unset foo[2]
$ echo ${foo[@]}
a b d e f
$ echo ${#foo[@]}
5
$ echo ${!foo[@]}
0 1 3 4 5

## foo[2]='' 可以用来隐藏元素,删除和隐藏的区别可以用 null 和 "" 进行理解,一个是没有值,一个是空字符串
$ foo=(a b c d e f)
$ foo[1]=''
$ echo ${foo[@]}
a c d e f
$ echo ${#foo[@]}
6
$ echo ${!foo[@]}
0 1 2 3 4 5

3.4 关联数组

Bash 的新版本支持关联数组。关联数组使用字符串而不是整数作为数组索引。

declare -A 可以声明关联数组。

1
2
3
4
declare -A colors
colors["red"]="#ff0000"
colors["green"]="#00ff00"
colors["blue"]="#0000ff"

关联数组必须用带有 -A 选项的 declare 命令声明创建。相比之下,整数索引的数组,可以直接使用变量名创建数组,关联数组就不行。

访问关联数组成员的方式,几乎与整数索引数组相同。

1
echo ${colors["blue"]}

4 set

Bash 执行脚本时,会创建一个子 Shell。子 Shell 就是脚本的执行环境,Bash 默认给定了这个环境的各种参数。

直接运行 set,会显示所有的环境变量和 Shell 函数。

set 参数:

  • -u:执行脚本时,如果遇到不存在的变量,Bash 默认忽略它。加上 -u 后,脚本遇到不存在的变量就会报错并停止执行。
  • -x:用来在运行结果之前,先输出执行的那一行命令,在行首以 + 号表示执行的命令。
  • -e:脚本只要发生错误,就终止执行。
  • -o pipefail:-e 不适用于管道命令,所谓管道命令,就是多个子命令通过管道运算符(|)组合成为一个大的命令。Bash 会把最后一个子命令的返回值,作为整个命令的返回值。也就是说,只要最后一个子命令不失败,管道命令总是会执行成功,因此它后面命令依然会执行,-e 就失效了。-o pipefail 可以避免这种情况发生,只要一个子管道命令失效,整个管道命令就会失败,脚本就会终止执行。
  • -n:不运行命令,只检查语法是否正确。
  • -f:不对通配符进行文件名扩展。
  • -v:打印 Shell 接收到的每一行输入。

set 的参数一般放在脚本头部,也可以在执行 Bash 脚本时从命令行传入。

1
$ bash -euxo pipefail script.sh

5 shopt

shopt 命令用来调整 Shell 的参数,跟 set 命令的作用很类似。之所以会有这两个类似命令的主要原因是,set 是从 Ksh 继承的,属于 POSIX 规范的一部分,而 shopt 是 Bash 特有的。

直接输入 shopt 可以查看所有参数,以及它们各自打开和关闭的状态。

1
$ shopt

shopt 命令后面跟着参数名,可以查询该参数是否打开。

1
2
$ shopt globstar
globstar  off

上面例子表示 globstar 参数默认是关闭的。

(1)-s

-s 用来打开某个参数。

1
$ shopt -s optionNameHere

(2)-u

-u 用来关闭某个参数。

1
$ shopt -u optionNameHere

举例来说,histappend 这个参数表示退出当前 Shell 时,将操作历史追加到历史文件中。这个参数默认是打开的,如果使用下面的命令将其关闭,那么当前 Shell 的操作历史将替换掉整个历史文件。

1
$ shopt -u histappend

6 Debug 相关环境变量

6.1 LINENO

变量 LINENO 返回它在脚本里面的行号。

1
2
3
#!/bin/bash

echo "This is line $LINENO"

执行上面的脚本 test.sh$LINENO 会返回 3

1
2
$ ./test.sh
This is line 3

6.2 FUNCNAME

变量 FUNCNAME 返回一个数组,内容是当前的函数调用堆栈。该数组的 0 号成员是当前调用的函数,1 号成员是调用当前函数的函数,以此类推。

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

function func1()
{
  echo "func1: FUNCNAME0 is ${FUNCNAME[0]}"
  echo "func1: FUNCNAME1 is ${FUNCNAME[1]}"
  echo "func1: FUNCNAME2 is ${FUNCNAME[2]}"
  func2
}

function func2()
{
  echo "func2: FUNCNAME0 is ${FUNCNAME[0]}"
  echo "func2: FUNCNAME1 is ${FUNCNAME[1]}"
  echo "func2: FUNCNAME2 is ${FUNCNAME[2]}"
}

func1

执行上面的脚本 test.sh,结果如下。

1
2
3
4
5
6
7
$ ./test.sh
func1: FUNCNAME0 is func1
func1: FUNCNAME1 is main
func1: FUNCNAME2 is
func2: FUNCNAME0 is func2
func2: FUNCNAME1 is func1
func2: FUNCNAME2 is main

6.3 BASH_SOURCE

变量 BASH_SOURCE 返回一个数组,内容是当前的脚本调用堆栈。该数组的 0 号成员是当前执行的脚本,1 号成员是调用当前脚本的脚本,以此类推,跟变量 FUNCNAME 是一一对应关系。

下面有两个子脚本 lib1.shlib2.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# lib1.sh
function func1()
{
  echo "func1: BASH_SOURCE0 is ${BASH_SOURCE[0]}"
  echo "func1: BASH_SOURCE1 is ${BASH_SOURCE[1]}"
  echo "func1: BASH_SOURCE2 is ${BASH_SOURCE[2]}"
  func2
}
# lib2.sh
function func2()
{
  echo "func2: BASH_SOURCE0 is ${BASH_SOURCE[0]}"
  echo "func2: BASH_SOURCE1 is ${BASH_SOURCE[1]}"
  echo "func2: BASH_SOURCE2 is ${BASH_SOURCE[2]}"
}

然后,主脚本 main.sh 调用上面两个子脚本。

1
2
3
4
5
6
7
#!/bin/bash
# main.sh

source lib1.sh
source lib2.sh

func1

执行主脚本 main.sh,会得到下面的结果。

1
2
3
4
5
6
7
$ ./main.sh
func1: BASH_SOURCE0 is lib1.sh
func1: BASH_SOURCE1 is ./main.sh
func1: BASH_SOURCE2 is
func2: BASH_SOURCE0 is lib2.sh
func2: BASH_SOURCE1 is lib1.sh
func2: BASH_SOURCE2 is ./main.sh

7 mktemp

Bash 脚本有时需要创建临时文件或临时目录。常见的做法是,在 /tmp 目录里面创建文件或目录,这样做有很多弊端,使用 mktemp 命令是最安全的做法。

使用

直接运行 mktemp 命令,就能生成一个临时文件。

1
2
3
4
5
$ mktemp
/tmp/tmp.4GcsWSG4vj

$ ls -l /tmp/tmp.4GcsWSG4vj
-rw------- 1 ruanyf ruanyf 0 12月 28 12:49 /tmp/tmp.4GcsWSG4vj

上面命令中,mktemp 命令生成的临时文件名是随机的,而且权限是只有用户本人可读写。

Bash 脚本使用 mktemp 命令的用法如下。

1
2
3
4
#!/bin/bash

TMPFILE=$(mktemp)
echo "Our temp file is $TMPFILE"

为了确保临时文件创建成功,mktemp 命令后面最好使用 OR 运算符(||),保证创建失败时退出脚本。

1
2
3
4
#!/bin/bash

TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"

为了保证脚本退出时临时文件被删除,可以使用 trap 命令指定退出时的清除操作。

1
2
3
4
5
6
#!/bin/bash

trap 'rm -f "$TMPFILE"' EXIT

TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"

参数

-d 参数可以创建一个临时目录。

1
2
$ mktemp -d
/tmp/tmp.Wcau5UjmN6

-p 参数可以指定临时文件所在的目录。默认是使用 $TMPDIR 环境变量指定的目录,如果这个变量没设置,那么使用 /tmp 目录。

1
2
$ mktemp -p /home/ruanyf/
/home/ruanyf/tmp.FOKEtvs2H3

-t 参数可以指定临时文件的文件名模板,模板的末尾必须至少包含三个连续的 X 字符,表示随机字符,建议至少使用六个 X。默认的文件名模板是 tmp. 后接十个随机字符。

1
2
$ mktemp -t mytemp.XXXXXXX
/tmp/mytemp.yZ1HgZV

8 trap

trap 命令用来在 Bash 脚本中响应系统信号。

最常见的系统信号就是 SIGINT(中断),即按 Ctrl + C 所产生的信号。trap 命令的 -l 参数,可以列出所有的系统信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ trap -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

trap 的命令格式如下。

1
$ trap [动作] [信号1] [信号2] ...

上面代码中,“动作”是一个 Bash 命令,“信号”常用的有以下几个。

  • HUP:编号1,脚本与所在的终端脱离联系。
  • INT:编号2,用户按下 Ctrl + C,意图让脚本终止运行。
  • QUIT:编号3,用户按下 Ctrl + 斜杠,意图退出脚本。
  • KILL:编号9,该信号用于杀死进程。
  • TERM:编号15,这是kill命令发出的默认信号。
  • EXIT:编号0,这不是系统信号,而是 Bash 脚本特有的信号,不管什么情况,只要退出脚本就会产生。

trap 命令响应 EXIT 信号的写法如下。

1
$ trap 'rm -f "$TMPFILE"' EXIT

上面命令中,脚本遇到 EXIT 信号时,就会执行 rm -f "$TMPFILE"

trap 命令的常见使用场景,就是在 Bash 脚本中指定退出时执行的清理命令。

1
2
3
4
5
6
7
8
9
#!/bin/bash

trap 'rm -f "$TMPFILE"' EXIT

TMPFILE=$(mktemp) || exit 1
ls /etc > $TMPFILE
if grep -qi "kernel" $TMPFILE; then
  echo 'find'
fi

上面代码中,不管是脚本正常执行结束,还是用户按 Ctrl + C 终止,都会产生 EXIT 信号,从而触发删除临时文件。

注意,trap 命令必须放在脚本的开头。否则,它上方的任何命令导致脚本退出,都不会被它捕获。

如果 trap 需要触发多条命令,可以封装一个 Bash 函数。

1
2
3
4
5
6
7
function egress {
  command1
  command2
  command3
}

trap egress EXIT

9 命令提示符

命令提示符有 4 个相关的环境变量。

  • PS1:常规输入的提示符,$ 是普通用户,# 是根用户。
  • PS2:令行折行输入时系统的提示符,默认为 > 。
  • PS3:使用 select 命令时,系统输入菜单的提示符。
  • PS4:默认为 + ,它是使用 Bash 的 -x 参数执行脚本时行首的符号。

内容

  • \a:响铃,计算机发出一记声音。
  • \d:以星期、月、日格式表示当前日期,例如“Mon May 26”。
  • \h:本机的主机名。
  • \H:完整的主机名。
  • \j:运行在当前 Shell 会话的工作数。
  • \l:当前终端设备名。
  • \n:一个换行符。
  • \r:一个回车符。
  • \s:Shell 的名称。
  • \t:24小时制的hours:minutes:seconds格式表示当前时间。
  • \T:12小时制的当前时间。
  • \@:12小时制的AM/PM格式表示当前时间。
  • \A:24小时制的hours:minutes表示当前时间。
  • \u:当前用户名。
  • \v:Shell 的版本号。
  • \V:Shell 的版本号和发布号。
  • \w:当前的工作路径。
  • \W:当前目录名。
  • \!:当前命令在命令历史中的编号。
  • \#:当前 shell 会话中的命令数。
  • \$:普通用户显示为$字符,根用户显示为#字符。
  • \[:非打印字符序列的开始标志。
  • \]:非打印字符序列的结束标志。

前景颜色

  • \[\033[00m\]:颜色恢复

  • \[\033[0;30m\]:黑色
  • \[\033[1;30m\]:深灰色
  • \[\033[0;31m\]:红色
  • \[\033[1;31m\]:浅红色
  • \[\033[0;32m\]:绿色
  • \[\033[1;32m\]:浅绿色
  • \[\033[0;33m\]:棕色
  • \[\033[1;33m\]:黄色
  • \[\033[0;34m\]:蓝色
  • \[\033[1;34m\]:浅蓝色
  • \[\033[0;35m\]:粉红
  • \[\033[1;35m\]:浅粉色
  • \[\033[0;36m\]:青色
  • \[\033[1;36m\]:浅青色
  • \[\033[0;37m\]:浅灰色
  • \[\033[1;37m\]:白色

背景颜色

  • \[\033[0;40m\]:蓝色
  • \[\033[0;41m\]:红色
  • \[\033[0;42m\]:绿色
  • \[\033[0;43m\]:棕色
  • \[\033[1;44m\]:黑色
  • \[\033[1;45m\]:粉红
  • \[\033[1;46m\]:青色
  • \[\033[1;47m\]:浅灰色

License:署名-相同方式共享 3.0

This post is licensed under CC BY 4.0 by the author.