神兵利刃: Linux shell 编程之赋值、函数、条件判断、通配符、命令替换

“这是我参与更文挑战的第19天,活动详情查看: 更文挑战

大多数shell都有自己的一套脚本语言,包括变量、控制流和自己的语法。

创建命令流程(pipelines)、将结果保存到文件、从标准输入中读取输入,这些都是shell脚本中的原生操作,这让它比通用的脚本语言更易用。

本文我们会专注于bash脚本,因为它最流行,应用更为广泛。

赋值、取值

在 bash 中为变量赋值的语法是 foo=bar 。用 $foo 来访问变量中存储的数值。 注意中间不能有空格,否则解释器会调用程序 foo 并将 =bar 作为其参数。

foo=bar
echo "$foo"
# 打印 bar
echo '$foo'
# 打印 $foo
复制代码

Bash 中的字符串通过 单引号和双引号来定义,但它们含义并不完全相同。以单引号 ' 定义的字符串为原义字符串,其中的变量不会被转义,而 双引号"定义的字符串将变量值进行替换。

函数

bash 也支持 ifcasewhilefor 这些控制流关键字。同样 bash 也支持函数,它可以接受参数,并基于参数进行操作。譬如:

mcd () {
    mkdir -p "$1"
}
mcd 'hello/world'
复制代码

以上 $1 是脚本的第一个参数,将创建 hello/world 这两个有上下层级关系的目录。

bash 使用很多特殊的变量来表示参数、错误代码和相关变量。以下是一些常见的变量。

  • $0 – 脚本名
  • $1$9 – 脚本的参数。 $1 是第一个参数,依此类推。
  • $@ – 所有参数
  • $# – 参数个数
  • $? – 前一个命令的返回值
  • $$ – 当前脚本的进程识别码
  • !! – 完整的上一条命令,包括参数。常见应用:当你因为权限不足执行命令失败时,可以使用 sudo !!再尝试一次。
  • $_ – 上一条命令的最后一个参数。如果你正在使用的是交互式shell,你可以通过按下 Esc 之后键入 . 来获取这个值。

更完整的列表可以参考

文件内容如下,取名为 funWithParam.sh,执行 sh funWithParam.sh,可以看到效果。

#!/bin/bash

funWithParam(){
    echo "第一个参数为 $1 !"
    echo "第二个参数为 $2 !"
    echo "第十个参数为 $10 !"
    echo "第十个参数为 ${10} !"
    echo "参数总数有 $# 个!"
    echo "作为一个字符串输出所有参数 $* !"
    echo "作为一个字符串输出所有参数 $@ !"
    echo "当前脚本的进程识别码 $$"
    echo "前一个命令的返回值 $?"
    echo "上一条命令的最后一个参数 $_"
}

funWithParam 1 2 3 4 5 6 7 8 9 10
复制代码

条件判断:与和或,比较

命令通常使用 STDOUT来返回输出值,使用STDERR 来返回错误及错误码,便于脚本以更加友好的方式报告错误。

返回码或退出状态是脚本/命令之间交流执行状态的方式。返回值0表示正常执行,其他所有非0的返回值都表示有错误发生。

返回码可以配合搭配 &&|| 使用,用来进行条件判断。程序 true 的返回码永远是 0false 的返回码永远是 1

它们都属于短路运算符(short-circuiting),同一行多个命令可以用 ; 分隔

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
复制代码

我们看一个复杂的例子,通过它来看循环和条件语句。

文件取名为 testGrep.sh,执行 sh testGrep.sh 1 2 3 ,文件内容如下,

#!/bin/bash

echo "Starting program at $(date)" # date会被替换成日期和时间

echo "Running program $0 with $# arguments with pid $$"

for file in "$@"; do
    grep foobar "$file" > /dev/null 2> /dev/null
    # 如果模式没有找到,则grep退出状态为 1
    # 我们将标准输出流和标准错误流重定向到Null,因为我们并不关心这些信息
    if [[ $? -ne 0 ]]; then
        echo "File $file does not have any foobar, adding one"
        echo "# foobar" >> "$file"
    fi
done
复制代码

其会再当前目录下创建 file1 file2 file3 这三个文件,内容为 # foobar,终端输出效果如下:

➜  shellLearn  sh testGrep.sh file1 file2 file3
Starting program at 2021年 6月19日 星期六 09时57分09秒 CST
Running program grep.sh with 3 arguments with pid 58825
File file1 does not have any foobar, adding one
File file2 does not have any foobar, adding one
File file3 does not have any foobar, adding one
复制代码

在条件语句中,我们比较 $? 是否等于 0,不等于 0 则执行 if 条件内的逻辑。 在bash中进行比较时,尽量使用双方括号 [[ ]] 而不是单方括号 [ ],这样会降低犯错的几率,尽管这样并不能兼容 sh。

通配(globbing)- 花括号、通配符

  • 通配符 – 当你想要利用通配符进行匹配时,你可以分别使用 ?* 来匹配一个或任意个字符。

    例如,对于文件foo, foo1, foo2, foo10 和 bar, rm foo?这条命令会删除foo1 和 foo2 ,而rm foo* 则会删除除了bar之外的所有文件。

  • 花括号 {} – 当你有一系列的指令,其中包含一段公共子串时,可以用花括号来自动展开这些命令。

    这在批量移动或转换文件时非常方便。

convert image.{png,jpg}
# 会展开为
convert image.png image.jpg

cp /path/to/project/{foo,bar,baz}.sh /newpath
# 会展开为
cp /path/to/project/foo.sh /path/to/project/bar.sh /path/to/project/baz.sh /newpath

# 也可以结合通配使用
mv *{.py,.sh} folder
# 会移动所有 *.py 和 *.sh 文件
复制代码

命令替换 (command substitution)、进程替换(process substitution)

当您通过 $( CMD ) 这样的方式来执行CMD 这个命令时,它的输出结果会替换掉 $( CMD ) 。例如,如果执行 for file in $(ls) ,shell首先将调用ls ,然后遍历得到的这些返回值。

for file in $(ls); do
    echo "$file"
done
复制代码

还有一个冷门的类似特性是 进程替换(process substitution), <( CMD ) 会执行 CMD 并将结果输出到一个临时文件中,并将 <( CMD ) 替换成临时文件名。这在我们希望返回值通过文件而不是STDIN传递时很有用。

例如, diff <(ls foo) <(ls bar) 会显示文件夹 foobar 中文件的区别。

mkdir foo bar

# 下面命令会创建foo/a, foo/b, ... foo/h, bar/a, bar/b, ... bar/h这些文件
touch {foo,bar}/{a..h}
touch foo/x bar/y
# 比较文件夹 foo 和 bar 中包含文件的不同
diff <(ls foo) <(ls bar)
# 输出
# < x
# ---
# > y
复制代码

脚本检查

编写 bash 脚本有时候会很别扭和反直觉。可以利用ShellCheck类似的工具帮你定位 shell 脚本中的错误。

参考致谢

系列其他文章见

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享