跳转至

Shell 进阶与命令行工具

lec1 中我们已经熟悉了 Linux 的基本操作和一些 shell 的基础语法。在这一节中,我们将继续学习 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}},2{0..6..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
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] [1] hello world

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

1
2
3
show_args 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 来观察命令替换的结果。

文件名展开

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

user@debian:~$ ls *.txt
file10.txt  file1.txt  file2.txt  file3.txt  file4.txt  file5.txt  file6.txt  file7.txt  file8.txt  file9.txt

或是删除 file1.txtfile9.txt

1
2
3
user@debian:~$ rm file?.txt
user@debian:~$ ls *.txt
file10.txt

使用时要小心!如果有时候你不确定会展开成什么,可以先用 echo 命令来查看展开的结果。单个 * 会展开为当前目录下的所有非隐藏文件。如果你想要展开隐藏文件,可以使用 .*

glob

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

变量

数组与关联数组

环境变量

子 shell

shebang

重定向

控制结构

条件判断

test 命令

循环

选择

命令行工具

grep

find

sed

cut

xargs

评论