
前言
虽然一直都在用,但有些命令仍是半知半懂的,所以就好好学一下吧。
一些辅助工具:
- shellcheck - Shell 脚本静态检查工具,主流编辑器都有插件。类似 ESLint 的工具。
- zx - Google 出品,用 JavaScript 写 Shell 脚本。
本文大部分内容来自阮一峰老师的 Bash 脚本教程。
一、Shell 命令格式
$ command [ arg1 ... [ argN ] ]
其中 command 是一个具体的命令或者一个可执行文件,arg1... argN 是传递给命令的参数,是可选的。
命令与参数,参数与参数之间通过「一个空格」隔开。若有「多个空格」,多余空格会被自动忽略,作用相当于一个空格。
$ ls -l
其中 ls 是命令,-l 是参数。有些参数是命令的配置项,它们一般以一个「短横线」开头,比如上面的 -l。通常配置项参数有短形式和长形式两种形式,比如 -l 是短形式,--list 是长形式。两种写法作用完全相同,短形式便于输入,长形式可读性、语义更好。
通常命令都是一行的,可有些命令较长,写成多行有利于阅读和编辑,只要在每行结尾处加上反斜杠 \ 可以,Shell 会将下一行跟当前行一起解析。
$ echo Hello World
# 等同于
$ echo Hello \
World
二、命令的组合与继发
命令组合符 &&,前一个命令执行成功,才会接着执行第二个命令。
$ command1 && command2
命令组合符 ||,前一个命令执行失败,才会接着执行第二个命令。
$ command1 || command2
命令结束符 ;(分号),前一个命令执行结束后(无论成功与否),接着执行第二个命令。命令结束符可使得一行中放置多个命令。
$ clear; ls
管道符 |,前一个命令的输出作为第二个命令的输入。
$ command1 | command2
# 相当于
$ command1 > tempfile
$ command2 < tempfile
$ rm tempfile
三、引号
- 单引号:单引号用于保留字符的字面含义,各种特殊字符在单引号里面,都会变为普通字符。
- 双引号:比单引号宽松,大部分特殊字符在双引号里面,都会失去特殊含义,变成普通字符。但是,三个特殊字符除外:美元符号(
$)、反引号(`)和反斜杠(\)。这三个字符在双引号之中,依然有特殊含义,会被 Bash 自动扩展。
$ echo '$USER'
$USER
$ echo "$USER"
frankie
换行符在双引号之中,会失去特殊含义,Bash 不再将其解释为命令的结束,只是作为普通的换行符。所以可以利用双引号,在命令行输入多行文本。
$ echo "hello
world"
hello
world
echo 发音 [ˈekō](才发现原来一直读错了,惭愧)。其参数 -e 会解析引号中的特殊字符(比如换行符 \n)。若在 CLI 中直接输入 echo 命令 \n 也会解析为换行符,而不是普通的 \n 字符串。
$ echo -e "Hello\nShell"
Hello
Shell
四、子命令扩展
$(...) 可以扩展成另一个命令的运行结果,该命令的所有输出都会作为返回值。还有另一种较老的语法,子命令放在反引号之中,也可以扩展成命令的运行结果。
$ echo $(date)
2022年 6月27日 星期一 00时31分14秒 CST
$ echo `date`
2022年 6月27日 星期一 00时32分01秒 CST
五、读取变量
- 在变量名前加上
$,比如$SHELL。 - 读取变量时,变量名可以使用花括号
{}包围,比如$SHELL可以写成${SHELL}。 - 如果变量的值本身也是变量,可以使用
${!varname}语法,读取最终的值。(好像不太对,待进一步验证)
六、算术运算
- 除法运算符的返回结果总是为「整数」,比如
$(( 5 / 2 ))的结果为2,而不是2.5。 -
$(( ... ))的圆括号之中,不需要在变量名之前加上$,不过加上也不报错。 - 如果
$((...))里面使用不存在的变量,也会当作0处理。 -
$[...]是以前的语法,也可以做整数运算,不建议使用。
小数运算,需借助 bc 命令,其中 scale 表示小数位,ibase 和 obase 进行其他进制数运算。比如:
$ var1=3
$ var2=6
$ result=$(echo "scale=2; $var1 / $var2" | bc)
$ echo $result
.50
七、目录堆栈
cd - 命令可以返回前一次的目录。默认情况下,只记录上一次所在的目录。
$ cd ~/Desktop/
$ cd -
~
八、脚本
8.1 Shebang 行
脚本的第一行通常是指定解释器,即这个脚本必须通过什么解释器执行。这一行以 #! 字符开头,这个字符称为 Shebang,所以这一行就叫做 Shebang 行。
#! 后面就是脚本解释器的位置,Bash 脚本的解释器一般是 /bin/sh 或 /bin/bash。
#!/bin/sh
# 或者
#!/bin/bash
#! 与脚本解释器之间有没有空格,都是可以的。
如果 Bash 解释器不放在目录 /bin,脚本就无法执行了。为了保险,可以写成下面这样。
#!/usr/bin/env bash
上面命令使用 env 命令(这个命令总是在 /usr/bin 目录),返回 Bash 可执行文件的位置。env 命令的详细介绍,请看后文。
Shebang 行不是必需的,但是建议加上这行。如果缺少该行,就需要手动将脚本传给解释器。
举例来说,脚本是 script.sh,有 Shebang 行的时候,可以直接调用执行。
$ ./script.sh
上面例子中,script.sh 是脚本文件名。脚本通常使用 .sh 后缀名,不过这不是必需的。
如果没有 Shebang 行,就只能手动将脚本传给解释器来执行。
$ /bin/sh ./script.sh
# 或者
$ bash ./script.sh
8.2 执行权限和路径
前面说过,只要指定了 Shebang 行的脚本,可以直接执行。这有一个前提条件,就是脚本需要有执行权限。可以使用下面的命令,赋予脚本执行权限。
给所有用户执行权限
$ chmod +x script.sh
给所有用户读权限和执行权限
$ chmod +rx script.sh
# 或者
$ chmod 755 script.sh
只给脚本拥有者读权限和执行权限
$ chmod u+rx script.sh
脚本的权限通常设为 755(拥有者有所有权限,其他人有读和执行权限)或者 700(只有拥有者可以执行)。
除了执行权限,脚本调用时,一般需要指定脚本的路径(比如 path/script.sh)。如果将脚本放在环境变量 $PATH 指定的目录中,就不需要指定路径了。因为 Bash 会自动到这些目录中,寻找是否存在同名的可执行文件。
建议在主目录新建一个 ~/bin 子目录,专门存放可执行脚本,然后把 ~/bin 加入 $PATH。
export PATH=$PATH:~/bin
上面命令改变环境变量 $PATH,将 ~/bin 添加到 $PATH 的末尾。可以将这一行加到 ~/.zshrc 文件里面,然后重新加载一次 .zshrc,这个配置就可以生效了。
$ source ~/.zshrc
以后不管在什么目录,直接输入脚本文件名,脚本就会执行。
$ script.sh
上面命令没有指定脚本路径,因为 script.sh 在 $PATH 指定的目录中。
上面的配置文件,取决于你当前所用的 Shell。比如我这里是 zsh,配置文件为
~/.zshrc,如果你是 bash,可能是~/.bash_profile、~/.bashrc等。
九、条件判断
if 关键字后面跟的是一个命令。这个命令可以是 test 命令,也可以是其他命令。命令的返回值为 0 表示判断成立,否则表示不成立。
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi
判断条件 commands 可以是一条命令,这条命令执行成功(返回值为 0),就意味着判断条件成立。
但更多地是使用 test 命令,语法如下:
# 写法一
test expression
# 写法二
[ expression ]
# 写法三
[[ expression ]]
以上三种形式是等价的,第三种形式支持正则判断,前两种不支持。需要注意的是,后两种写法中 [ 和 ] 与内部命令之间必须要有「空格」。因为 [ 是 test 命令的简写形式,因此它后面必须要有空格。举个例子,使用 if 语句判断一个文件是否存在:
# 写法一
if test -e /tmp/foo.txt ; then
echo "Found foo.txt"
fi
# 写法二
if [ -e /tmp/foo.txt ] ; then
echo "Found foo.txt"
fi
# 写法三
if [[ -e /tmp/foo.txt ]] ; then
echo "Found foo.txt"
fi
9.1 文件判断
以下表达式用来判断文件状态:
-
[ -a file ]:如果file存在,则为true。 -
[ -b file ]:如果file存在,并且是一个块(设备)文件,则为true。 -
[ -c file ]:如果file存在,并且是一个字符(设备)文件,则为true。 -
[ -d file ]:如果file存在,并且是目录,则为true。 -
[ -e file ]:如果file存在,则为true。 -
[ -f file ]:如果file存在,并且是一个普通文件,则为true。 -
[ -g file ]:如果file存在,并且设置了组 ID,则为true。 -
[ -G file ]:如果file存在,并且属于有效的组 ID,则为true。 -
[ -h file ]:如果file存在,并且是符号链接(软链接),则为true。 -
[ -k file ]:如果file存在,并且设置了它的 sticky bit,则为true。 -
[ -L file ]:如果file存在,并且是一个符号链接(软链接),则为true。 -
[ -N file ]:如果file存在,并且自上次读取后已被修改,则为true。 -
[ -O file ]:如果file存在,并且属于有效的用户 ID,则为true。 -
[ -p file ]:如果file存在,并且是一个命名管道,则为true。 -
[ -r file ]:如果file存在,并且可读(当前用户有可读权限),则为true。 -
[ -s file ]:如果file存在,并且其长度大于零,则为true。 -
[ -S file ]:如果file存在,并且是一个网络 socket,则为true。 -
[ -t fd ]:如果fd是一个文件描述符,并且重定向到终端,则为true。 这可以用来判断是否重定向了标准输入/输出/错误。 -
[ -u file ]:如果file存在,并且设置了 setuid 位,则为true。 -
[ -w file ]:如果file存在,并且可写(当前用户拥有可写权限),则为true。 -
[ -x file ]:如果file存在,并且可执行(当前用户拥有可执行/搜索权限),则为true。 -
[ file1 -nt file2 ]:如果file1比file2的更新时间最近,或者file2存在而file1不存在,则为true。 -
[ file1 -ot file2 ]:如果file1比file2的更新时间更旧,或者file2存在而file1不存在,则为true。 -
[ file1 -ef file2 ]:如果file1和file2引用相同的设备和 inode 编号,则为true。
if [ -f "$FILE" ]; then
echo "$FILE is a regular file."
fi
上面代码中,$FILE 要放在双引号之中,这样可以防止变量 $FILE 为空,从而出错。因为 $FILE 如果为空,这时 [ -e $FILE ] 就变成 [ -e ],这会被判断为真。而 $FILE 放在双引号之中,[ -e "$FILE" ] 就变成 [ -e "" ],这会被判断为假。
未完待续...