脚本

bash不仅可以直接输入到命令行执行,也可以作为一种脚本语言。作为一种脚本语言,Bash也拥有变量、分支语句、循环语句等。

可以在某个文件里写bash脚本,然后运行它,不必每次都在命令行里输入。将以下代码保存到hello.sh里,.sh后缀代表这是一个bash脚本,当然,使用其他后缀也不影响脚本正常运行。

1
2
#!/bin/bash
echo "Hello World!"

文件的第一行是#!/bin/bash,以#开始的内容是注释,意味着不会被运行。但是在文件的最开始,以#!开头的注释有个特别的名字,它被称作“shebang”,可以提示系统,使用什么程序来解析下面的代码。这里的#!/bin/bash,告诉操作系统使用/bin/bash解释下面的代码,也可以写作#!/bin/sh,因为/bin/sh是一个指向bin/bash的软链接。

在执行之前,请先向这个文件添加执行的权限,因为Linux默认的新文件权限是644,即rw-r--r--,任何人都无法执行。使用chmod +x hello.sh来添加执行权限。

然后,有两种方式可以运行脚本,第一种是输入./hello.sh,这里不能写成hello.sh,因为对于这样的命令,Linux会在PATH变量中查找,而不从当前文件夹中查找。这种方式没有指定运行脚本的解释器,因此系统会读取Shebang,然后使用/bin/bash解释代码

第二种方式就是指定解释器,将文件作为参数传入,输入sh hello.sh,就可以看到一行Hello World!。这种情况显式指定了使用sh解释运行,因此shebang不会生效

变量

在bash中,给变量赋值使用foo=bar请注意,这里的等号前后不能加上任何空格,这是因为bash脚本会用空格来分割命令和命令的参数,如果输入foo = bar,解释器会调用foo命令,并传递=bar

在上一节中已经提到过了,bash中的字符串使用'"包裹,但这两个记号的含义并不相同,前者会保留原始字符串,而后者会进行转义。

1
2
3
4
5
cyrus:~$ foo=bar
cyrus:~$ echo "$foo"
bar
cyrus:~$ echo '$foo'
$foo

这里的$代表引用相应的变量,但是在'字符串内部并不会生效。

引用变量也可以写成${foo},和$foo并无不同,但是${foo}的写法可以更精确地界定变量名的范围,防止歧义。

数组

bash也支持定义数组,下标和其他大多数编程语言一样是从0开始的

1
2
3
cyrus:~$ array=(a o e i u)
cyrus:~$ echo ${array[2]}
e

(())

bash为了方便作为命令行使用,变量都是字符串类型的,因此命令foo=1+1只会将字符串1+1赋值给foo,而不会计算出2,要进行数学运算,应使用(())双括号包裹,在双括号里引用变量是不需要加$的。

1
2
3
4
5
6
7
8
9
10
11
12
13
cyrus:~$ foo=1+1
cyrus:~$ echo $foo
1+1
cyrus:~$ echo $((1+2))
3
cyrus:~$ foo=$((1+2))
cyrus:~$ echo $foo
3
cyrus:~$ ((foo=1+3))
cyrus:~$ echo $foo
4
cyrue:~$ echo $((foo*2))
8

可以使用的运算和C语言的语法是一致的:+-*/(注意是整除,不支持小数)、%=(、==!=<>foo++++foofoo----foo!||&&~|&<<>>

另外还有一个符号#,可以用来进制转换

1
2
3
4
cyrus:~$ echo $((8#123))
83
cyrus:~$ echo $((16#abc))
2748

两条命令分别计算出了 (123)8(123)_8(abc)16(abc)_{16} 的10进制表示。

[]

还有一个语法是[ 表达式 ],但实际上这么说并不准确,因为这其实不是一个语法,[是一个程序的名字,可以试着运行which [,会发现[其实是放在/usr/bin/[的一个程序,它的运行效果和命令test是一样的,具体使用细节可以输入man [查询。

[是一个程序,而]只不过是传入的最后一个参数罢了(这个参数是必须的,否则会报错)。正因如此,必须写成[ 表达式 ],而不是[表达式]。即表达式前后必须加空格

表达式 含义
!表达式 逻辑非
表达式 -a 表达式 逻辑且 And
表达式 -o 表达式 逻辑或 Or
-n 字符串 字符串长度非0 Non-zero
-z 字符串 字符串长度为0 Zero
字符串 = 字符串 字符串相等
字符串 != 字符串 字符串不相等
字符串 > 字符串 字符串大于,需要转义,写成\>而不是>
字符串 < 字符串 字符串小于,同样需要转义,因为<>都有特殊含义
( 表达式 ) 就是括号的意思,表达式分组
整数 -eq 整数 整数相等 EQual to
整数 -ge 整数 整数大于等于 Greater than or Equal to
整数 -gt 整数 整数大于 Greater Than
整数 -le 整数 整数小于等于 Less than or Equal to
整数 -lt 整数 整数小于 Less Than
整数 -nq 整数 整数小于 Not Equal to
-f 文件 文件存在,并且类型为常规文件
-d 文件 文件存在,并且类型为目录

更多用法请输入man [man test查看帮助文档。

还有一个指令是双方括号[[ 表达式 ]],双方括号除了支持以上特性,还支持了更多高级的特性,例如<>()不需要转义等。双方括号表达式不支持所有POSIX系统,但是大多数时候都是可用的,为了避免犯错,建议使用双方括号表达式。

定义函数

在bash中定义一个函数,它的作用是创建一个文件夹并进入,可以这样写:

1
2
3
4
mcd () {
mkdir -p "$1"
cd "$1"
}

这里的$1是一个特殊的变量,代表传入的第一个参数,例如我们定义以上函数后,输入mcd example,就会有以下效果:

1
2
3
4
5
6
cyrus:~$ mcd () {
mkdir -p "$1"
cd "$1"
}
cyrus:~$ mcd example
cyrus:~/example$

像这样的特殊变量还有很多,以下是常用的一些:

  • $0:脚本名
  • $1$9:脚本的参数。$1是第一个参数,依此类推,超过第9个时,应该加上大括号,如${14}是第14个参数。
  • $@:所有参数
  • $#:参数个数
  • $?:前一个命令的返回值
  • $$:当前脚本的进程ID(Process Identification, PID)
  • !! - 完整的上一条命令,包括参数。常见应用:当你因为权限不足执行命令失败(会出现Permission denied)时,可以使用 sudo !!再尝试一次。
  • $_ - 上一条命令的最后一个参数。

更多请参见这里

上面需要特殊解释的是$?,它给出上一个命令的返回值,如果为0,就意味着程序正常退出,如果非0,就代表程序异常退出。truefalse,也是两个程序,其中true永远返回0false永远返回1

这个返回码可以搭配&&||这两个运算符使用,它们都是短路运算符

  • &&其实是逻辑与运算,也可以用来连接两条命令,当前面的命令执行成功时才执行后面的命令。用&&连接多个命令,假如中间发生了错误,就不会继续执行,引发一连串的错误。
  • ||相应的,逻辑或运算是当前面的命令执行失败时才执行后面的命令。可以用于设置一个“Plan B”,当前面的命令执行失败,就执行“Plan B”。
  • ;就是单纯的先后执行两条命令,无论成功与否,两条命令都会执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
false || echo "Oops, fail"
# Oops, fail

true || echo "Will not be printed"
#

true && echo "Things went well"
# Things went well

false && echo "Will not be printed"
#

false ; echo "This will always run"
# This will always run

?*{}

?*分别可以匹配一个任意字符和多个任意字符,而{}可以展开一个字符串列表,如:

1
2
3
4
5
6
7
8
9
10
11
rm foo? # 会删除foo1、foo2、foob,不会删除foo、foo12
rm foo* # 会删除foo1、foo2、foob、foo、foo12,不会删除bar

cp *.png folder # 会复制所有.png后缀的文件

mv {dog,cat}.png folder # 会移动dog.png和cat.png
mv dog.png cat.png folder # 上面那句等价于这个

mkdir *.{png,jpg,jpeg,gif} # 将两种语法组合起来使用

touch {foo,bar}/{a..h} # 会创建foo/a, foo/b, ... foo/h, bar/a, bar/b, ... bar/h这些文件

流程控制语句

if

bash中if的格式是这样的:

1
2
3
4
5
6
7
if 条件
then
命令1
命令2
...
命令n
fi

或者写成一行(适用于在命令行界面下使用)

1
if 条件; then 命令1; 命令2; ...; 命令n; fi

if-else语法,和if-elif-else语法是类似的:

1
2
3
4
5
6
7
8
9
if 条件
then
命令1
命令2
else
命令3
命令4

fi

1
if 条件; then 命令1; 命令2; else 命令3; 命令4; fi

if-elif-else:

1
2
3
4
5
6
7
8
9
if 条件1
then
命令1
elif 条件2
then
命令2
else
命令3
fi

或者

1
if 条件1; then 命令1; elif 条件2; then 命令2; else 命令3; fi

(这么复杂的语句还是写在脚本文件里吧,不建议写成一行了)

具体示例:

1
2
3
4
5
6
7
8
9
10
11
12
compare() {
if (($1 > $2)); then
echo "$1 大于 $2"
elif (($1 < $2)); then
echo "$1 小于 $2"
else
echo "$1 等于 $2"
fi
}
compare 282 14
compare 28 214
compare 123 123

运行脚本,输出

1
2
3
282 大于 14
28 小于 214
123 等于 123

for

for循环语句的语法如下:

1
2
3
4
5
6
7
for i in 列表项1 列表项2 ... 列表项n
do
命令1
命令2
...
命令n
done

写成一行

1
for i in 列表项1 列表项2 ... 列表项n; do 命令1; 命令2; ...; 命令n done

示例

1
2
3
4
5
foo=bar
for i in 1 "2" '3' apple 5 $foo
do
echo "i is $i now."
done

输出

1
2
3
4
5
6
i is 1 now.
i is 2 now.
i is 3 now.
i is apple now.
i is 5 now.
i is bar now.

while

while的语法如下:

1
2
3
4
while 条件
do
命令
done

case

case其实就是其他编程语言中的switch语句,case的语法比较奇怪,结束case语句要使用case这个单词反过来拼写的esac,这个和if->fi是一样的。然后,每个子块里面需要用匹配式) 命令 ;;的格式来写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
echo '请选择 a~e 之间的一个英文字母'
read letter
case $letter in
a)
echo ALPHA
;;
b)
echo BRAVO
;;
c)
echo CHARLIE
;;
d)
echo DELTA
;;
e)
echo ECHO
;;
*)
echo 您的输入不是 a~e 之间的一个英文字母
;;
esac

breakcontinue

当然,bash也是支持breakcontinue的,它们的用法也和大多数编程语言一样。

$()组合命令

$()可以获得一个命令的输出并用它替换,例如

1
2
3
for i in $(ls); do
echo "我有一个文件,它叫做$i"
done

输出

1
2
3
我有一个文件,它叫做Downloads
我有一个文件,它叫做temp
...

find

find指令可以用来查找文件,示例如下

1
2
3
4
5
6
7
8
9
10
11
12
# 查找所有名称为src的文件夹
find . -name src -type d
# 查找所有文件夹路径中包含test的python文件
find . -path '*/test/*.py' -type f
# 查找前一天修改的所有文件
find . -mtime -1
# 查找所有大小在500k至10M的tar.gz文件
find . -size +500k -size -10M -name '*.tar.gz'
# 删除全部扩展名为.tmp 的文件
find . -name '*.tmp' -exec rm {} \;
# 查找全部的 PNG 文件并将其转换为 JPG
find . -name '*.png' -exec convert {} {}.jpg \;

grep

grep命令可以用来从一段文本里面查找想要的字符串,比如

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
cyrus:~$ cat poem.txt
Do not go gentle into that good night

Dylan Thomas

Do not go gentle into that good night,
Old age should burn and rave at close of day;
Rage, rage against the dying of the light.
...

cyrus:~$ grep light < poem.txt # 获取所有含有“light”的行
Rage, rage against the dying of the light.
Because their words had forked no lightning they
Rage, rage against the dying of the light.
...

cyrus:~$ grep eye -C 1 < poem.txt # 获取含有“eye”的行,并同时打印出上下文(Context),往上往下各一行
Grave men, near death, who see with blinding sight
Blind eyes could blaze like meteors and be gay,
Rage, rage against the dying of the light.

cyrus:~$ grep the -v < poem.txt # -v代表反选,获得所有不含“the”的行
Do not go gentle into that good night

Dylan Thomas

Do not go gentle into that good night,
Old age should burn and rave at close of day;
...

alias

alias命令可以创建别名

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
# 创建常用命令的缩写
alias ll="ls -lh"

# 能够少输入很多
alias gs="git status"
alias gc="git commit"
alias v="vim"

# 手误打错命令也没关系
alias sl=ls

# 重新定义一些命令行的默认行为
alias mv="mv -i" # -i prompts before overwrite
alias mkdir="mkdir -p" # -p make parent dirs as needed
alias df="df -h" # -h prints human readable format

# 别名可以组合使用
alias la="ls -A"
alias lla="la -l"

# 在忽略某个别名
\ls
# 或者禁用别名
unalias la

# 获取别名的定义
alias ll
# 会打印 ll='ls -lh'

这些别名并不会持续生效,如果想要保存他们,需要在~/.bashrc里面添加对应的配置,就可以每次启动bash的时候都加载这些配置。

参考链接

https://missing.csail.mit.edu/2020/

https://www.runoob.com/linux/linux-shell.html

本文属于系列文章:一看就懂的Linux Shell的基础使用