梅花开 发表于 2023-1-3 22:14:48

主题 2 Shell工具和脚本

主题 2 Shell工具和脚本

Shell 工具和脚本 · the missing semester of your cs education (missing-semester-cn.github.io)
Shell脚本

shell 脚本是一种更加复杂度的工具。

[*]定义变量
在bash中为变量赋值的语法是foo=bar,意为定义变量foo,foo的值为bar。访问变量使用$变量名
$ foo=bar
$ echo "$foo"
bar需要注意的是,Shell中使用空格作为分隔参数的保留字符。
如果将上诉赋值语句写为foo = bar,将不起作用。事实上,这样写并没有将bar赋给foo,而是用=和bar作为参数调用foo程序。因为这样Shell会认为你正在执行一个名为foo的命令。
$ foo = bar
-bash: foo: command not found你需要特别注意这类问题,比如如果有带空格的文件名,你需要使用引号将其括起来。

[*]在bash中处理字符串
有两种定义字符串的方法,可以使用双引号定义字符串,也可以使用单引号定义字符串。
$ echo "Hello"
Hello
$ echo 'Hello'
HelloBash中的字符串通过' 和 "分隔符来定义,但是它们的含义并不相同。
以'定义的字符串为原义字符串,其中的变量不会被转义,而 "定义的字符串会将变量值进行替换。
例如:
$ echo "Value is $foo"
Value is bar
$ echo 'Value is $foo'
Value is $foo

[*]定义函数
和其他大多数的编程语言一样,bash也支持if, case, while 和 for 这些控制流关键字。同样地, bash 也支持函数,它可以接受参数并基于参数进行操作。
下面这个函数是一个例子,它会创建一个文件夹并使用cd进入该文件夹。
$ cat mcd.sh
mcd(){
        mkdir -p "$1"
        cd "$1"
}这里 $1 是脚本的第一个参数的意思
source 脚本名,这将会在Shell中加载脚本并运行。
$ source mcd.sh
$ mcd test
$ 如上,在执行了source mcd.sh之后,看似无事发生,但实际上Shel中已经定义了mcd函数。我们给mcd传递一个参数test,这个参数被用于作为创建的目录名(即$1),然后Shell自动切换到了test目录里。整个过程就是,我们创建了文件夹并进入其中。

[*]保留字
在bash中,许多$开头的东西一般都是被保留的(指留作特定用途)
$1 是脚本的第一个参数的意思。与其他脚本语言不同的是,bash使用了很多特殊的变量来表示参数、错误代码和相关变量。下面列举其中一些变量,更完整的列表可以参考 这里。
形式释义$0脚本名$1~$9脚本的参数, $1 是第一个参数,依此类推$@所有参数$#参数个数$?前一个命令的返回值$$当前脚本的进程识别码!!完整的上一条命令,包括参数。常见应用:当你因为权限不足执行命令失败时,可以使用 sudo !!再尝试一次。$_上一条命令的最后一个参数,如果你正在使用的是交互式 shell,你可以通过按下 Esc 之后键入 . 来获取这个值。有一些保留字可以直接在Shell中使用,例如$?可以获取上一条命令的错误代码(返回值),再比如$_会返回上一条命令的最后一个参数。
例如:
$ mkdir test
$ cd $_
$ 如上,我们无需在写一次test,使用$_访问该参数,它就会被替换成test,现在我们进入到test目录中了。
这样的例子有很多,再例如!!,它返回完整的上一条命令,包括参数。常见应用:当你因为权限不足执行命令失败时,可以使用 sudo !!再尝试一次。
$ mkdir /mnt/new
mkdir: cannot create directory ‘/mnt/new’: Permission denied
$ sudo !!
sudo mkdir /mnt/new
$ rmdir /mnt/new
rmdir: failed to remove '/mnt/new': Permission denied
$ sudo !!
sudo rmdir /mnt/new
$

[*]标准错误流
如果你的程序出错了,你想输出错误但不想污染标准输出,那么你可以写进这个流。

[*]错误代码
还有一种叫做错误代码$?(error code)的东西,是一种告诉你整个运行过程结果如何的方式。
$ echo "Hello"
Hello
$ echo $?
0这里显示echo "Hello" 运行的错误代码为0,0是因为一切正常,没有出现问题。
这种退出码和如C语言里代表的意思一样。
0代表一切正常,没有出现错误。
$ grep foobar mcd.sh
$ echo $?
1如上,我们尝试着在mcd.sh脚本中查找foobar字符串,而它不存在,所以grep什么都没输出。但是通过反馈一个1的错误代码,它让我们知道这件事没有成功。
此外,true的错误代码始终是0;false的错误代码则是1。
$ true
$ echo $?
0
$ false
$ echo $?
1

[*]逻辑运算符
下面bash要做的是执行第一个命令,如果第一个命令失败,再去执行第二个(短路运算法则)。因为它尝试做一个逻辑或,如果第一个命令没有0错误码,就会去执行第二个命令
$ false || echo "Oops fail"
Oops fail相似地,如果我们把false换成true,那么将不会执行第二个命令,因为第一个命令已经返回一个0错误码了,第二个命令将会被短路。
$ true || echo "Oops fail"
$ 相似的,我们使用与运算符&&,它仅当第一个命令执行无错误时,才会执行第二个部分。如果第一个命令失败,那么第二个命令就不会被执行。
$ true && echo "Things went well"
Things went well
$ false && echo "This will not print"
$ 使用;号连接的代码,无论你执行什么,都可以通过。在同一行使用分号来连接命令,如下,它始终会被打印出来。
$ false ; echo "This will always print"
This will always print

[*]把命令的输出存到变量里
这里我们获取pwd命令的输出,它会打印出我们当前的工作路径,然后把其存入foo变量中。然后我们询问变量foo的值,我们就可以看到这个字符串
$ foo=$(pwd)
$ echo $foo
/home/lighthouse/missing-semester/tools更广泛地来说,我们可以通过一个叫做命令替换的东西,把它放进任意字符串中。并且因为我们使用的不是单引号,所以这串东西会被展开。
$ echo "We are in $(pwd)"
We are in /home/lighthouse/missing-semester/tools

[*]过程替换
另一个比较好用知名度更低的东西叫做过程替换。和之前的命令替换是类似的,例如
$ cat <(ls) <(ls ..)
mcd.sh
test
tools第三行:有一个$(date)的参数,date打印出当前的时间。
第五行:$0代表着当前运行的脚本的名称,$#代表给定的参数个数,$$是这个命令的进程ID,一般缩写为PID。
第七行:$@可以展开成所有参数,比如有三个参数,你可以键入$1 $2 $3,如果你不知道有多少个参数,也可以直接键入$@。这里我们通过这种方式将所有参数放在这里,然后这些参数被传给for循环,for循环会创建一个file变量,依次地用这些参数赋值给file变量。
第八行:我们运行grep命令,它会在一堆文件里搜索一个子串。这里我们在文件里搜索字符串foobar,文件变量file将会展开为赋给它的值。
之前说过,如果我们在意程序的输出的话,我们可以把它重定向到某处(比如到一个文件里面保存下来,或者连接组合)。但有时候情况恰恰相反,例如有时候我们只想知道某个脚本的错误代码是什么,例如这里想知道grep能不能成功查找。我们并不在意程序的运行结果,因此我们甚至能直接扔掉整个输出,包括标准输出和标准错误流。这里我们做的就是把两个输出重定向到/dev/null,/dev/null是UNIX系统的一种特殊设备,输入到它的内容会被丢弃(就是说你可以随意乱写乱画,然后所有的内容都会被丢掉)。
这里的>代表重定向输出流,2>代表重定向标准错误流(因为这两个流是分立的,所以你要告诉bash去操作哪一个)。
所以这里我们执行命令,去检查文件有没有foobar字符串,如果有的话,返回一个0错误代码,如果没有返回一个非0错误代码。
第十一行:我们获取前一个命令的错误代码($?),然后是一个比较运算符-ne(代表不等于Non Equal)
其他编程序语言中有像=和≠,bash里有很多预设的比较运算(可以使用命令man test查看),这主要是为了你用Shell的时候,有很多东西要去测试。比如我们现在正在对比两个数,看它们是否相同。
如果文件中没有foobar,前一个命令将会返回一个非零错误代码。
第十二行:我们将会如果前一个命令返回一个非0错误代码,我们将会输出一句话File xxx does not have any foobar, adding one
第十三行:使用>>往对应文件中追加一行注释# foobar
现在我们来运行这个脚本,当前目录下有一些文件,我们将这些文件作为参数传给example.sh,检查是否有foobar。
#!/bin/bash

echo "Start program at $(date)" # Date will be substituted

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

for file in "$@";do
      grep foobar "$file" > /dev/null 2> /dev/null
      # When pattern is not found,grep has exit status
      # We redirect STDOUT and STDERR to a null register ..
      if [[ "$?" -ne 0 ]]; then
                echo "File $file does not have any foobar, adding one"
                echo "# foobar" >> "$file"
      fi      
done我们在文件hello.txt和mcd.sh中没有找到foobar字符串,因此脚本分别给这两个文件添加了一个# foobar 注释
$ ls
example.shhello.txtmcd.sh
$ ./example.sh hello.txt mcd.sh
Start program at Sun Dec 25 23:06:13 CST 2022
Running program ./example.sh with 2 arguments with pid 2570038
File hello.txt does not have any foobar, adding one
File mcd.sh does not have any foobar, adding one

[*]通配符
如果我们不想一个一个查找文件,可以使用通配符来进行匹配。
比如这里*匹配任意字符,这里将会显示出所有含有任意字符,并以.sh结尾的文件
$ cat hello.txt
hello,this is a txt file
# foobar
$ cat mcd.sh
mcd(){
        mkdir -p "$1"
        cd "$1"
}
# foobar现在如果我只想找有一个而不是两个特定字符的项,可以使用?,?匹配一个字符
$ ls
example.shhello.txtimage.pngmcd.shproject1project2test
$ ls *.sh
example.shmcd.sh现在我们得到了匹配的目录project1和project2
src是匹配的目录下的子项
总而言之,通配符非常强大,你也可以组合它们。
一个常用模式是花括号{}。
比如目录下有一个image.png图片,我们想转变该图像的格式,一般的做法是convert image.png image.jpg,但是你也可以键入convert image.{png,jpg},它会展开成上面的那行。
又如:
$ ls
example.shhello.txtimage.pngmcd.shproject1project2project42test
$ ls project?
project1:
src

project2:
src如上所述,我们可以touch一串foo,所有的foo都会被展开。
你也可以进行多层操作,建立笛卡尔系:
$ cat
页: [1]
查看完整版本: 主题 2 Shell工具和脚本