跳转至

Shell 与脚本

常用 Shell

Bash

Bash(Bourne Again Shell)是Linux系统默认的shell,也是使用最广泛的shell。它提供了丰富的功能和良好的兼容性。

Zsh

Zsh(Z Shell)是一个功能强大的shell,提供了更好的自动补全、主题支持和插件系统。许多开发者选择使用Zsh作为他们的默认shell。

其他Shell

  • sh:原始的Bourne Shell
  • csh/tcsh:C Shell系列
  • fish:友好的交互式shell
  • dash:轻量级的POSIX兼容shell

Shell 基础

环境变量

Shell使用环境变量来存储系统配置信息:

# 查看环境变量
echo $PATH
echo $HOME
echo $USER

# 设置环境变量
export MY_VAR="value"

# 查看所有环境变量
env

命令历史

Shell会记录命令历史,方便重复使用:

# 查看历史命令
history

# 搜索历史命令
Ctrl+R

# 执行历史命令
!!          # 执行上一个命令
!n          # 执行第n个历史命令
!string     # 执行最近以string开头的命令

通配符

Shell支持通配符来匹配文件名:

  • *:匹配任意字符
  • ?:匹配单个字符
  • [abc]:匹配a、b或c
  • [a-z]:匹配a到z的任意字符
  • {a,b}:匹配a或b

编写和执行 Shell 脚本

创建Shell脚本

创建一个简单的Shell脚本:

#!/bin/bash
# 这是一个注释
echo "Hello, World!"
echo "当前用户: $USER"
echo "当前目录: $(pwd)"

脚本权限

给脚本添加执行权限:

chmod +x script.sh

执行脚本

# 方法1:直接执行(需要执行权限)
./script.sh

# 方法2:使用bash解释器
bash script.sh

# 方法3:使用sh解释器
sh script.sh

Shebang

脚本的第一行通常包含shebang,指定解释器:

#!/bin/bash     # 使用bash
#!/bin/sh       # 使用sh
#!/usr/bin/env python3  # 使用python3

常用脚本工具和技巧

我们初步认识了一些常用的命令。接下来我们会对它们进行一定的扩展。

  • 系统命令
  • sudo命令用于提权。
  • poweroffshutdown两个命令用于关机。
  • reboot命令用于重启电脑。
  • whoami命令用于查看自己是哪个用户。
  • which命令用于查找可执行文件的路径。
  • ps命令用于显示当前运行的进程。
    • -e:显示所有进程。
    • -f:以全格式显示进程信息,包括父进程ID、用户等。
    • -l:以长格式显示进程信息。
    • -u:显示指定用户的进程。
    • -p:显示指定进程ID的进程。
    • -o:自定义输出格式。
    • -H:以树形结构显示进程之间的关系。
    • -j:以作业控制格式显示进程信息。
    • -x:显示所有进程,包括没有控制终端的进程。
  • kill命令用于终止进程。
  • fg命令可以将后台运行的任务调回前台,这个命令可以恢复被Ctrl+Z挂起的任务。
  • bg命令可以将任务放到后台运行。
  • 列出和查找类
  • pwd命令用于显示当前工作目录的绝对路径。
  • ls命令用于列出目录中的文件和子目录。
    • -l:以长格式列出文件和目录的详细信息。
    • -a:列出所有文件和目录,包括隐藏文件。
    • -h:以人类可读的格式显示文件大小。
    • -R:递归地列出子目录中的文件和目录。
    • -t:按修改时间排序。
  • tree命令用于以树形结构显示目录中的文件和子目录。
  • find命令用于在目录中查找文件和目录。
  • 文件系统操作类
  • cd命令用于切换当前工作目录。
  • mkdir命令用于创建新目录。
    • -p:递归地创建多级目录,如果上级目录不存在则一并创建。
    • -v:显示创建目录的详细信息。
    • -m:设置新目录的权限。
  • touch命令用于创建新文件或更新现有文件的修改时间。
    • -a:只更新访问时间。
    • -m:只更新修改时间。
    • -c:如果文件不存在则不创建。
    • -t:设置文件的时间戳。
    • -r:使用指定文件的时间戳。
  • rm命令用于删除文件或目录。
    • -r:递归地删除目录及其内容。
    • -f:强制删除文件或目录,不提示确认。
    • -i:在删除前提示确认。
    • -v:显示删除的详细信息。
  • rmdir命令用于删除空目录。
  • cp命令用于复制文件或目录。
    • -r:递归地复制目录及其内容。
    • -f:强制覆盖目标文件。
    • -i:在覆盖前提示确认。
    • -v:显示复制的详细信息。
    • -u:只在源文件比目标文件新时才进行复制。
  • mv命令用于移动或重命名文件或目录。
    • -f:强制覆盖目标文件。
    • -i:在覆盖前提示确认。
    • -v:显示移动的详细信息。
    • -u:只在源文件比目标文件新时才进行移动。
  • ln命令用于创建链接。
    • -s:创建软链接(符号链接)。
    • -f:强制覆盖目标链接。
    • -i:在覆盖前提示确认。
    • -v:显示链接的详细信息。
    • -T:将目标视为一个普通文件,而不是目录。
  • tar命令用于打包和解包文件。
    • -c:创建一个新的归档文件。
    • -x:从归档文件中提取文件。
    • -f:指定归档文件的名称。
    • -v:显示详细的操作信息。
    • -z:使用 gzip 压缩或解压缩归档文件。
    • -j:使用 bzip2 压缩或解压缩归档文件。
    • -J:使用 xz 压缩或解压缩归档文件。
    • -p:保留文件的权限和时间戳。
    • -C:切换到指定目录后再进行打包或解包。
  • 文本处理类
  • head命令用于显示文件的前几行。
  • tail命令用于显示文件的后几行。
  • cat命令用于连接文件并打印到标准输出。
    • -n:为每一行添加行号。
    • -b:为非空行添加行号。
    • -s:压缩连续的空行。
    • -E:在每行末尾显示$符号。
    • -T:将制表符显示为^I
    • -v:显示不可见字符。
    • -A:显示所有不可见字符,包括空格和制表符。
    • -e:等同于-vE,显示不可见字符并在行末添加$符号。
  • echo命令用于在终端输出文本。
    • -n:不在输出末尾添加换行符。
    • -e:启用转义字符的解释,例如\n表示换行,\t表示制表符。
    • -E:禁用转义字符的解释。
    • -c:不输出任何内容。
    • -C:将输出内容转换为大写字母。
    • -l:将输出内容转换为小写字母。
    • -a:将输出内容转换为首字母大写字母。
    • -s:将输出内容转换为首字母小写字母。
    • -p:将输出内容转换为首字母大写字母,并将其他字母转换为小写字母。

警告

导啊,咱们生产环境为啥执行 dpkg 会说 command not found 呀?

我之前干了啥?清了一下工作路径垃圾,好像是 sudo rm -rf /

什么叫相对路径要加个点?

警告:除非你知道你在输入什么,否则任何情况下均不要带上sudo执行删除命令!

命令联动

在 Linux 中,重定向、管道、变量和进程替换是四种把“数据”从一条命令挪到另一条命令(或文件)的核心手段。它们常被混用,但机理各不相同。

重定向

重定向只认识真正的文件(或文件描述符),有两种:> 把标准输出定向到文件(写);<把文件内容定向到标准输入(读)。

echo "Hello, World!" > hello.txt   # 新建或覆盖文件
cat < hello.txt                    # 把文件当输入

管道

管道 | 在内核里创建一条匿名管道,让左边进程的标准输出直接成为右边进程的标准输入,两边同时运行。

ls | grep "file"          # 边 ls 边 grep,流式处理

Here-String

<<< word是 Bash 的 here-string 语法,shell 会先把 word 的扩展结果写进一个临时文件(或匿名管道),再把该临时对象作为标准输入递给命令。因此它正好弥补了重定向只能读文件的不足。

grep "file" <<< "$(ls)"   # 等价于 ls | grep "file",但没用管道

Here-String和管道有一定的区别。管道是流式的,边产生边消费;Here-String必须等整个字符串生成完才能开始消费。

进程替换

进程替换是一种"把命令输出/输入伪装成文件名"的 Bash 特性。<(cmd) 把命令的标准输出绑定到一个命名管道(或 /dev/fd/N),返回一个可读文件名;>(cmd) 则把命令的标准输入绑定到一个命名管道,返回一个可写文件名。对任何"只能读文件"的工具(diffcatsort…)来说,这就像凭空多了两个临时文件。

diff <(cmd1) <(cmd2)      # 比较两条命令的输出,而无需临时落盘
sort >(uniq > result.txt) # 把排序结果直接丢给 uniq

变量与命令替换

$(cmd) 是"命令替换",shell 会等待该命令执行结束,把它的全部标准输出当成一段文本收回来,可以赋给变量,也可以直接嵌入命令行。

out=$(ls)                 # 把 ls 的输出存进变量
grep "file" <<< "$out"    # 这里用 here-string 消费变量
diff <(echo "$out") <(ls) # 用进程替换再比一次

$(cmd) 本身不是管道,也不是重定向。它只是"把命令输出变成字符串"的一种手段。

机制 数据形态 左侧何时开始 右侧何时开始
cmd1 \| cmd2 管道字节流 立即 立即
cmd < file 已有文件 立即 -
cmd <<< "$str" 临时文件 字符串生成完后 字符串生成完后
cmd <(cmd1) 命名管道/FD 立即 立即
str=$(cmd1); cmd2 <<< "$str" 变量→临时文件 cmd1 结束后 cmd1 结束后

掌握这四种手法后,你就可以根据各种实际条件,灵活选择最简洁、最高效的写法。

思考题

  1. sl这个玩具软件仅有200行源代码,却能让你在终端里看到一列火车呼啸而过。请你阅读它的源代码,并简要描述它的实现原理;如果希望把该软件改成系统服务,开机就自动跑一列火车,你需要解决哪些问题?试着实践一下。

  2. 对比aptpacman的实现。为什么后者更容易实现滚动更新和回滚?怎么验证?

  3. binsbinusr/bin这三个东西都是什么?为什么会有这么多类似的目录?它们之间有什么区别?为什么直到2020年仍然有发行版保持着/bin/usr/bin的分离?能否把它们合并?如果可以,怎么做?

  4. chmod 777 /到底给敌人开了多少额外的后门?试着统计一个正常的系统中所有777文件分别被多少个不同的进程打开过。

  5. 管道和Here-String的内存和延迟有多少?为什么后者会OOM,而管道不会?Here-String的实现原理是什么?如果要改进它,你会怎么做?

  6. 试着在虚拟机里安装Arch Linux和NixOS;然后,试着将Arch的安装步骤翻译成configuration.nix,并试着生成ISO。对比两者的异同,然后说明声明式系统在可维护性上的优势。但是为什么即使这样,Arch用户仍然比NixOS用户多?