Shell 脚本学习

发布于 2023-05-05 21:30:48 字数 9419 浏览 39 评论 0

配图源自 Freepik

虽然一直都在用,但有些命令仍是半知半懂的,所以就好好学一下吧。

一些辅助工具:

  • 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 表示小数位,ibaseobase 进行其他进制数运算。比如:

$ 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 ]:如果 file1file2 的更新时间最近,或者 file2 存在而 file1 不存在,则为 true
  • [ file1 -ot file2 ]:如果 file1file2 的更新时间更旧,或者 file2 存在而 file1 不存在,则为 true
  • [ file1 -ef file2 ]:如果 file1file2 引用相同的设备和 inode 编号,则为 true
if [ -f "$FILE" ]; then
  echo "$FILE is a regular file."
fi

上面代码中,$FILE 要放在双引号之中,这样可以防止变量 $FILE 为空,从而出错。因为 $FILE 如果为空,这时 [ -e $FILE ] 就变成 [ -e ],这会被判断为真。而 $FILE 放在双引号之中,[ -e "$FILE" ] 就变成 [ -e "" ],这会被判断为假。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

0 文章
0 评论
23 人气
更多

推荐作者

懂王

文章 0 评论 0

清秋悲枫

文章 0 评论 0

niceone-tech

文章 0 评论 0

小伙你站住

文章 0 评论 0

刘涛

文章 0 评论 0

南街九尾狐

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文