本文将会介绍以 bash 作为脚本语言的一些基本操作,以及几种常用的 shell 工具。

目录如下:

我们可以在 bash 中为某个变量赋值,然后使用 echo 将其输出,但需要注意输出的格式,需要在该变量之前添加一个$符号。

需要注意的是:等号两边不能含有空格,变量名不能以数字开头,不能使用标点符号,不能使用 bash 中的关键字。仅允许使用字母、数字、下划线。

1
2
3
4
carol@ubuntu-carol:~$ foo=bar
carol@ubuntu-carol:~$ echo $foo
bar
carol@ubuntu-carol:~$ 

此外,还需要注意单引号和双引号的区别,如下所示。

双引号中的语句是会被解释执行的,而单引号不可以。

1
2
3
4
5
6
7
carol@ubuntu-carol:~$ echo $foo
bar
carol@ubuntu-carol:~$ echo "Value is $foo"
Value is bar
carol@ubuntu-carol:~$ echo 'Value is $foo'
Value is $foo
carol@ubuntu-carol:~$ 

这里编写一个 bash 脚本,其功能是:用于创建一个指定名称的目录,然后进入到该目录中。

这里的"$1"相当于占位符,接收一个目录的名称。

1
2
3
4
mcd() {
    mkdir -p "$1"
    cd "$1"
}
1
2
3
4
5
carol@ubuntu-carol:~/learnShell$ source mcd.sh 
carol@ubuntu-carol:~/learnShell$ mcd test
carol@ubuntu-carol:~/learnShell/test$ pwd
/home/carol/learnShell/test
carol@ubuntu-carol:~/learnShell/test$ 

此外,对于$_的使用,它所指的是上一个命令中的最后一个参数,或者说当前 shell 环境中的最后一个参数。

对于以下环境来说,当前 shell 环境中的最后一个参数是mkdir a b中的b。因此,下一句cd $_中的$_指的就是b

1
2
3
4
5
6
7
8
9
carol@ubuntu-carol:~/learnShell$ ls
hello2.txt  hello.txt  ls.txt  mcd.sh  number.txt
carol@ubuntu-carol:~/learnShell$ mkdir file
carol@ubuntu-carol:~/learnShell$ cd $_
carol@ubuntu-carol:~/learnShell/file$ 
carol@ubuntu-carol:~/learnShell/file$ cd ..
carol@ubuntu-carol:~/learnShell$ mkdir a b
carol@ubuntu-carol:~/learnShell$ cd $_
carol@ubuntu-carol:~/learnShell/b$ 

当我们执行grep foobar mcd.sh时,也就是尝试通过grepmcd.sh脚本中寻找foorbar时,由于匹配不到相应的foobar,因此在echo $?之后,会返回一个错误信息1(事实上,在错误时会返回一个在 1~255 内的整数)。如果上一步没有执行错误,说明执行成功,则会返回信息0。例如,在执行echo "hello"后,由于没有执行错误,因此返回的是0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
carol@ubuntu-carol:~/learnShell$ ls
hello2.txt  hello.txt  ls.txt  mcd.sh  number.txt
carol@ubuntu-carol:~/learnShell$ echo "hello"
hello
carol@ubuntu-carol:~/learnShell$ echo $?
0
carol@ubuntu-carol:~/learnShell$ grep foobar mcd.sh 
carol@ubuntu-carol:~/learnShell$ echo $?
1
carol@ubuntu-carol:~/learnShell$ $?

这很好理解,如果我们在mcd.sh脚本中寻找是否有字符串与dir匹配,那么此时是匹配成功的,然后执行echo $?会返回0,如下所示:

1
2
3
4
5
carol@ubuntu-carol:~/learnShell$ grep dir mcd.sh 
    mkdir -p "$1"
carol@ubuntu-carol:~/learnShell$ echo $?
0
carol@ubuntu-carol:~/learnShell$ 

当然,我们也可以通过truefalse来判断echo $?的输出,如下所示:

1
2
3
4
5
6
7
carol@ubuntu-carol:~/learnShell$ true
carol@ubuntu-carol:~/learnShell$ echo $?
0
carol@ubuntu-carol:~/learnShell$ false
carol@ubuntu-carol:~/learnShell$ echo $?
1
carol@ubuntu-carol:~/learnShell$ 

甚至还可以结合逻辑运算符,如下所示:

注意短路运算。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
carol@ubuntu-carol:~/learnShell$ false || echo "hello"
hello
carol@ubuntu-carol:~/learnShell$ echo $?
0
carol@ubuntu-carol:~/learnShell$ false && echo "hello"
carol@ubuntu-carol:~/learnShell$ echo $?
1
carol@ubuntu-carol:~/learnShell$ true || echo "hello"
carol@ubuntu-carol:~/learnShell$ echo $?
0
carol@ubuntu-carol:~/learnShell$ true && echo "hello"
hello
carol@ubuntu-carol:~/learnShell$ echo $?
0
carol@ubuntu-carol:~/learnShell$ 

我们可以在 shell 中使用变量去承接一个命令的输出。不对,准确的说,应该是使用变量foo作为pwd命令的一个别名。如下所示:

1
2
3
4
carol@ubuntu-carol:~/learnShell$ foo=$(pwd)
carol@ubuntu-carol:~/learnShell$ echo $foo
/home/carol/learnShell
carol@ubuntu-carol:~/learnShell$ 

使用重定向符号<并搭配命令行语句ls,来列出当前目录下以及父级目录下所有的文件,如下所示:

需要注意的是:<(之间不能有空格,你应该将它们看作是同一个语句。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
carol@ubuntu-carol:~/learnShell$ cat <(ls) <(ls ..)
hello2.txt
hello.txt
ls.txt
mcd.sh
number.txt
Desktop
Documents
Downloads
learnC
learnShell
Music
Pictures
Public
snap
Templates
Videos
carol@ubuntu-carol:~/learnShell$ 

下面这段脚本用于遍历我们提供的参数,使用grep搜索字符串foobar,如果没有找到的话,则将其作为注释追加到文件中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
echo "Startint program at $(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"
    if
done
1
2
3
4
import sys

for arg in reversed(sys.argv[1:]):
    print(arg)

执行完./example.sh mcd.sh script.py example.sh语句后可以发现,mcd.sh中没有foobar语句,因此程序会在mcd.sh中的末尾添加# foobar语句。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
carol@ubuntu-carol:~/learnShell$ chmod +777 example.sh 
carol@ubuntu-carol:~/learnShell$ ./example.sh mcd.sh script.py example.sh 
Startint program at Thu 26 Nov 2020 09:30:12 PM CST
Running program ./example.sh with 3 arguments with pid 7787
File mcd.sh does not have any foobar, adding one
File script.py does not have any foobar, adding one
carol@ubuntu-carol:~/learnShell$ cat mcd.sh 
mcd() {
    mkdir -p "$1"
    cd "$1"
}
# foobar
carol@ubuntu-carol:~/learnShell$

接下来就是一些通配符的使用了,其中*用于匹配多个字符,而?用于匹配一个字符。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
carol@ubuntu-carol:~/learnShell$ tree
.
├── example.sh
├── hello2.txt
├── hello.txt
├── ls.txt
├── mcd.sh
├── number.txt
├── project1
│   └── src
├── project2
│   └── src
├── project42
│   └── src
└── script.py

6 directories, 7 files
carol@ubuntu-carol:~/learnShell$ ls project*
project1:
src

project2:
src

project42:
src
carol@ubuntu-carol:~/learnShell$ ls project?
project1:
src

project2:
src
carol@ubuntu-carol:~/learnShell$ 

对于花括号的使用,这个知识点我之前没接触过,在这里记录一下。

也就是说,当你有一系列命令的时候,其中包含一段公共的字串时,可以使用花括号来自动展开这些命令。如下所示:

1
2
3
4
5
6
7
carol@ubuntu-carol:~/learnShell$ ls
example.sh  hello2.txt  hello.txt  image.jpg  image.png  ls.txt  mcd.sh  number.txt  project1  project2  project42  script.py
carol@ubuntu-carol:~/learnShell$ cp image.{jpg,png} /home/carol/
carol@ubuntu-carol:~/learnShell$ cd /home/carol/
carol@ubuntu-carol:~$ ls
Desktop  Documents  Downloads  image.jpg  image.png  learnC  learnShell  Music  Pictures  Public  snap  Templates  Videos
carol@ubuntu-carol:~$ 

其中:语句cp image.{jpg,png} /home/carol/被自动展开为:

1
cp image.jpg image.png /home/carol/

需要注意的是:image.{jpg,png}中的逗号两边不能含有空格。

此外,如果想要使用例如touch创建多个含有公共前缀文件名的文件的话,则也可以使用{ },例如:

1
2
3
4
5
6
7
carol@ubuntu-carol:~/learnShell/project1/src$ touch foo{1,2,3}
carol@ubuntu-carol:~/learnShell/project1/src$ ls -l
total 0
-rw-rw-r-- 1 carol carol 0 Nov 28 16:02 foo1
-rw-rw-r-- 1 carol carol 0 Nov 28 16:02 foo2
-rw-rw-r-- 1 carol carol 0 Nov 28 16:02 foo3
carol@ubuntu-carol:~/learnShell/project1/src$ 

如果在逗号两侧含有空格的话,则会发生意想不到的结果,这显然不是我们的目的。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
carol@ubuntu-carol:~/learnShell/project1/src$ touch foo{4, 5, 6}
carol@ubuntu-carol:~/learnShell/project1/src$ ls -l
total 0
-rw-rw-r-- 1 carol carol 0 Nov 28 16:04 5,
-rw-rw-r-- 1 carol carol 0 Nov 28 16:04 6}
-rw-rw-r-- 1 carol carol 0 Nov 28 16:02 foo1
-rw-rw-r-- 1 carol carol 0 Nov 28 16:02 foo2
-rw-rw-r-- 1 carol carol 0 Nov 28 16:02 foo3
-rw-rw-r-- 1 carol carol 0 Nov 28 16:04 foo{4,
carol@ubuntu-carol:~/learnShell/project1/src$

甚至可以在多个目录下创建多个文件,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
carol@ubuntu-carol:~/learnShell/project1/src$ mkdir foo bar
carol@ubuntu-carol:~/learnShell/project1/src$ ls
bar  foo
carol@ubuntu-carol:~/learnShell/project1/src$ touch {foo,bar}/{a..j}
carol@ubuntu-carol:~/learnShell/project1/src$ tree
.
├── bar
│   ├── a
│   ├── b
│   ├── c
│   ├── d
│   ├── e
│   ├── f
│   ├── g
│   ├── h
│   ├── i
│   └── j
└── foo
    ├── a
    ├── b
    ├── c
    ├── d
    ├── e
    ├── f
    ├── g
    ├── h
    ├── i
    └── j

2 directories, 20 files
carol@ubuntu-carol:~/learnShell/project1/src$ 

这里的touch {foo,bar}/{a..j}命令会被展开成touch foo/a foo/b foo/c ... foo/j bar/a bar/b bar/c ... bar/j。中间的目录我用...给省略了。

然后我们可以使用diff命令查看ls输出的不同,例如:

1
2
3
4
5
6
carol@ubuntu-carol:~/learnShell/project1/src$ touch foo/x foo/y
carol@ubuntu-carol:~/learnShell/project1/src$ diff <(ls foo) <(ls bar)
11,12d10
< x
< y
carol@ubuntu-carol:~/learnShell/project1/src$ 

可以看到,在foo目录下和bar目录下的文件中,只有xy文件不同。

在 Linxu 中,可以使用man查看一个某个命令的使用说明,但是man page页面介绍的太罗嗦了。因此,这里有一个实用的工具 tldr,即「Too long; Don’t Read.」,可以简化地介绍某个命令,并给出一些事例。

使用find命令在当前目录下,查找名为src的目录,例如:

1
2
3
4
5
6
7
carol@ubuntu-carol:~/learnShell$ ls
example.sh  hello2.txt  hello.txt  image.jpg  image.png  ls.txt  mcd.sh  number.txt  project1  project2  project42  script.py
carol@ubuntu-carol:~/learnShell$ find . -name src -type d
./project2/src
./project42/src
./project1/src
carol@ubuntu-carol:~/learnShell$ 

对于find命令的其它功能,例如查找当前目录下所有以.tmp结尾的文件,并执行删除操作,如下:

1
find . -name '*.tmp' -exec rm {} \;

也就是说,在执行find命令后,使用参数-exec再跟上另外一个命令,可以实现多个命令组合的效果,例如:找到所有的.png文件,然后将它们都转换成.jpg格式,如下:

1
find . -name '*png' -exec convert {} {.}.jpg \;

之前可以使用grep进行文件中的字符串的查找,也可以使用工具fzf进行查找,如下所示:

1
carol@ubuntu-carol:~/learnShell$ cat example.sh | fzf

输入完上述命令后,则会进入编辑模式。然后,你可以在该模式下输入你想要查找的字符串。

image.png

对于可视化目录来说,也有几个比较实用的工具,如 treebrootnnn 等。