Shell 脚本学习
虽然一直都在用,但有些命令仍是半知半懂的,所以就好好学一下吧。
一些辅助工具:
- 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 "" ]
,这会被判断为假。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论