跳转至

Shell 进阶

lec1 中我们已经熟悉了 Linux 的基本操作和一些 shell 的基础语法。在这一节中,我们将继续学习 shell 的进阶用法和一些常用的命令行工具。

函数

我们今天要接触的第一个话题就是 shell 函数。你当然可以像其他语言一样定义函数,然后在需要的时候调用它。

试试用函数实现 A+B 吧!

1
2
3
4
5
function a_plus_b() {
    echo $(($1 + $2))
}

a_plus_b 1 2  # 输出 3

或者,你也可以省略 function 关键字,它们的效果是一样的。

1
2
3
4
5
a_plus_b() {
    echo $(($1 + $2))
}

a_plus_b 4 5  # 输出 9

注意到了吗,你可以像使用任何正常命令一样使用函数。函数的参数就是 $1 $2 $3 ...。试试在函数内部 $@ $# $0 这些变量的值吧!

表达式展开

表达式展开是一个更加庞大和复杂的话题——例如,解析一条 shell 命令要经历七种展开,你都知道么?

  • brace expansion
  • tilde expansion
  • parameter and variable expansion
  • command substitution
  • arithmetic expansion
  • word splitting
  • filename expansion

在这里把这些展开全部介绍一遍是不现实的,我们只会介绍一些常用的语法,你可以在 GNU bash 文档 查看完整的介绍。

所以字面上来说,其实我们在之前介绍过的变量展开也是一种表达式展开。这些基础的展开方式也会伴随我们使用 shell 的一生。在看一些好玩的东西之前,我们先来看一些别的东西。我们需要这样一个函数,方便观察一些东西。

1
2
3
show_args() {
    echo "[$#] $@"
}

函数 show_args 做的事应当很好理解,它会输出传入的参数个数和参数本身。我们来看看这个函数的输出。

1
2
3
4
5
6
user@debian:~$ show_args hello world
[2] hello world
user@debian:~$ show_args "hello world"
[1] hello world
user@debian:~$ show_args hello\ world
[1] hello world

当然,这些似乎都是我们已经知道的东西。但是下面这样呢:

1
2
3
4
5
6
user@debian:~$ show_args he"ll"o "w"o'r'l"d"
[2] hello world
user@debian:~$ show_args hello" "world
[1] hello world
user@debian:~$ show_args hell"o w"orld
[1] hello world

没错,在 shell 中,引号实际上也可以出现在(几乎)任何地方。上面的 hello" "world 中将空格用引号括起来,并且引号前后都没有空格,所以这整个字符串是一个参数。在第三条命令中也是一样的道理。这实际上是非常有用的功能,例如平时在操作路径时,如果路径中有空格,我们可以用引号括起来,可能像这样:

1
2
3
4
5
# 这些命令都是等价的
cd ~/Library/Application\ Support
cd ~/Library/"Application Support"
cd ~"/Library/Application Support"  # 注意 ~ 不能放在引号内
cd ~/Library/Application" "Support

我们在 lec1 中研究过,无法在单引号字符串中使用 \' 来转义单引号。对于这个问题,我们可以通过拆分字符串来解决。在下面这个例子中,单引号 \' 位于左右两部分引号内容的中间,它自身不包含在单引号字符串中。

user@debian:~$ show_args 'It'\''s MyGO!!!!!'
[1] It's MyGO!!!!!

在有了 show_args 函数和知道引号的用法之后,我们来看看花括号展开吧!

花括号展开

user@debian:~$ show_args {1..5}
[5] 1 2 3 4 5
user@debian:~$ show_args {z..a}
[26] z y x w v u t s r q p o n m l k j i h g f e d c b a
user@debian:~$ show_args i{08..18..2}n
[6] i08n i10n i12n i14n i16n i18n
user@debian:~$ show_args {,s,t,k,m,h}{a,e,i,o,u}
[30] a e i o u sa se si so su ta te ti to tu ka ke ki ko ku ma me mi mo mu ha he hi ho hu
user@debian:~$ show_args "c++"{98,03,1{1,4,7,{x..z}},{20..26..3}}
[11] c++98 c++03 c++11 c++14 c++17 c++1x c++1y c++1z c++20 c++23 c++26

花括号展开是一种非常有用的展开方式,它可以帮助我们快速生成一些特定格式的字符串。你也可以自己尝试一下,看看你能不能生成一些有趣的字符串。

有没有其他的使用场景呢?当然是有的。例如,你想要创建一些文件,但是文件名都是类似的,只有一部分不同。你可以这样:

# 创建 file1.txt 到 file10.txt
touch file{1..10}.txt

# 创建 2020 年到 2024 年的 1 月到 12 月的目录
mkdir -p {2020..2024}/{01..12}

# 将 .zshrc 和 .vimrc 复制到 my-server
scp .{zsh,vim}rc my-server:

# 将 my_binary 移动到 /usr/local/bin/my_binary
mv {,/usr/local/bin/}my_binary

命令替换

命令替换是另一种非常有用的展开方式。它可以帮助我们将一个命令的输出作为 shell 命令的一部分。例如:

1
2
3
4
5
6
7
8
user@debian:~$ show_args $(echo hello world)
[2] hello world
user@debian:~$ show_args $(show_args hello world)
[3] [2] hello world
user@debian:~$ show_args "$(show_args hello world)"
[1] [2] hello world
user@debian:~$ show_args "$(show_args "hello world")"
[1] [1] hello world

上面这四条命令经过命令替换后,实际上成为:

1
2
3
4
show_args hello world
show_args [2] hello world
show_args "[2] hello world"
show_args "[1] hello world"

这有什么用呢?一个应用场景是,你想要查看某个可执行文件的信息:

user@debian:~$ which ls echo
/usr/bin/ls
/usr/bin/echo
user@debian:~$ file $(which ls echo)
/usr/bin/ls:   ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=9f127c37a4c459cf01639f6ded2fcf11a49d3da9, for GNU/Linux 3.7.0, stripped
/usr/bin/echo: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=9be5659ffc854a3b33bfacc1523a40ab2760cc78, for GNU/Linux 3.7.0, stripped
user@debian:~$ ls -al $(which ls file which echo)
-rwxr-xr-x 1 root root  68408 Sep 20  2022 /usr/bin/echo
-rwxr-xr-x 1 root root  67920 Jan 29  2023 /usr/bin/file
-rwxr-xr-x 1 root root 200440 Sep 20  2022 /usr/bin/ls
lrwxrwxrwx 1 root root     23 Jul 29  2023 /usr/bin/which -> /etc/alternatives/which

别忘了,我们可以用 show_args 来观察命令替换的结果。上面的命令展开后就成为:

file /usr/bin/ls /usr/bin/echo
ls -al /usr/bin/ls /usr/bin/file /usr/bin/which /usr/bin/echo

文件名展开

文件名展开是也许是最符合我们直觉的展开方式,它可以快速匹配文件名。例如我们想要查看当前目录下所有的 .txt 文件:

user@debian:~$ ls *.txt
z1.txt  z10.txt  z2.txt  z3.txt  z4.txt  z5.txt  z6.txt  z7.txt  z8.txt  z9.txt

这会展开为 ls z1.txt z10.txt z2.txt z3.txt z4.txt z5.txt z6.txt z7.txt z8.txt z9.txt。或是删除当前目录下所有形如 IMG_*.jpgIMG_*.jpeg 的文件:

user@debian:~$ rm IMG_*.jp{,e}g

这里会首先执行花括号展开,成为 rm IMG_*.jpg IMG_*.jpeg,然后再执行文件名展开,也就是 rm 后面跟着所有匹配的文件名。但是,对于这种使用场景要小心!如果有时候你不确定会展开成什么,可以先用 echo 命令来查看展开的结果。单个 * 会展开为当前目录下的所有非隐藏文件。如果你想要展开隐藏文件,可以使用 .*

glob

文件名展开的规则叫做 glob,它是一种简单的正则表达式。重要的是,实际上 glob 的规则是可以自定义的:你可以使用 shopt 来查看和修改 glob 的行为。更详细的介绍请查看 GNU bash 的文档。

重定向与管道

在学了 C 语言后我们应当知道,每个程序都有三个 I/O 流:标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。这些到底是什么呢?简单来说,标准输入是程序可以从中读取数据的地方,标准输出是程序可以将数据输出其中的地方,标准错误是程序用来输出错误信息的地方。一般来说,当我们在 shell 中运行一个程序时,它的标准输入就被设置为键盘,标准输出和标准错误则被设置为终端,也就是我们的屏幕。比如 cat 命令。试试看!

1
2
3
4
5
user@debian:~$ cat
hello
hello
cat
cat

cat 命令中,我们输入一行,它就输出一行。这个过程就相当于 cat 命令从标准输入读取数据,然后将数据输出到标准输出。

子 shell

我们在 lec1 中提到了嵌套 shell,也就是在一个 shell 中运行另一个 shell。

shebang

变量

我们在 lec1 中提到了 shell 变量环境变量。说到底它们到底有什么区别?

数组与关联数组

环境变量

复合语句

实际上 shell 中有许多种复合语句,我们会在后面看到一些其他的。但现在我们要看的是下面这样的语句:

{ echo hello; echo world; }

这是一种组合命令(grouping command)。它会将花括号内的命令作为一个整体来执行,类似于函数。这样的语句在某些场景下非常有用,例如你想要将一组命令的输出重定向到一个文件:

{ echo hello; echo world; } > output.txt

对于 bash 来说,最后的分号是必须的。

控制结构

条件判断

test 命令

循环

选择

文件描述符

协程

Homework

简答题

  1. 设计一条命令以验证:在 shell 中,&&|| 的优先级是相同的。

编程题

评论