日常使用 Linux 时,我们常常沿用一套已经熟悉多年的操作流程:一次创建一个目录,或者用 ps 和 grep 反复查找进程号。这些做法当然能用,但在多台服务器上维护脚本时,额外步骤会不断累积,也更容易产生笔误。
本文介绍 5 个更简洁的命令行技巧:用 mkdir -p 和花括号展开创建目录树,用 heredoc 一次写入多行文件内容,用 tar -C 直接解压到目标目录,用 pgrep/pkill 按进程名处理任务,以及用 pidwait 等待进程结束。文中示例在 Ubuntu 26.04 LTS 和 Rocky Linux 10 上测试,通常也适用于其他主流 Linux 发行版,除非某一节另有说明。
Shell 特性与命令选项的区别
这些技巧可以分为两类:Shell 特性和命令选项。理解两者的差异,有助于判断它们在交互式终端和脚本中的行为。
第一类是 Bash 在启动命令之前处理的特性,例如花括号展开和 heredoc。以花括号展开为例,Bash 会先把花括号转换成一组路径,再交给 mkdir;mkdir 本身并不会看到花括号。由于这些特性依赖具体的 Shell,切换到其他 Shell 或使用不同解释器运行脚本时,行为可能不同。
第二类是工具自身实现的选项,例如 tar -C 以及 pgrep、pkill 的进程匹配参数。它们不依赖 Shell,因此无论由哪个 Shell 启动,行为通常保持一致。
1. 用 mkdir -p 和花括号展开创建嵌套目录
部署新应用时,我们经常要一次性建立多个目录。假设要为用户 tecmint 部署一个名为 myapp 的应用,分别保存可执行文件、配置、日志、数据和脚本。
传统做法是逐条执行多个 mkdir。更快的做法是把 mkdir -p 和 Bash 花括号展开结合起来,用一条命令创建整棵目录树:
mkdir -pv ~/myapp/{bin,config/{nginx,systemd},logs,data/{uploads,cache,backups},scripts}
-p 会在需要时创建缺失的父目录,-v 会输出每个已创建目录。这样既完成创建,又能立即检查结果。
输出:
mkdir: created directory '/home/tecmint/myapp'
mkdir: created directory '/home/tecmint/myapp/bin'
mkdir: created directory '/home/tecmint/myapp/config'
mkdir: created directory '/home/tecmint/myapp/config/nginx'
mkdir: created directory '/home/tecmint/myapp/config/systemd'
mkdir: created directory '/home/tecmint/myapp/logs'
mkdir: created directory '/home/tecmint/myapp/data'
mkdir: created directory '/home/tecmint/myapp/data/uploads'
mkdir: created directory '/home/tecmint/myapp/data/cache'
mkdir: created directory '/home/tecmint/myapp/data/backups'
mkdir: created directory '/home/tecmint/myapp/scripts'
不同实现的提示文字和顺序可能不同,但最终目录结构相同。
花括号的展开规则如下:
a,b,c会按逗号拆成多个路径。例如logs,scripts会展开为logs和scripts。- 嵌套写法
config/{nginx,systemd}会在同一个父目录下生成多个路径。 - 逗号后不要加空格。写成
bin, logs会让 Bash 无法按预期识别表达式,花括号可能原样传给命令。
查看目录树
可用 tree 检查结构。精简安装的 Linux 系统可能没有这个工具,请先安装。
Ubuntu 和 Debian:
sudo apt install -y tree
Rocky Linux、AlmaLinux 和 RHEL:
sudo dnf install -y tree
安装后运行:
tree ~/myapp
输出:
/home/tecmint/myapp
├── bin
├── config
│ ├── nginx
│ └── systemd
├── data
│ ├── backups
│ ├── cache
│ └── uploads
├── logs
└── scripts
10 directories, 0 files
创建前预览展开结果
较长的花括号表达式很容易写错,一个位置不对的逗号就可能生成意外路径。由于 Bash 在调用命令前就会完成展开,可以用 echo 预览,不会改变文件系统:
echo ~/myapp/{bin,config/{nginx,systemd},logs}
输出:
/home/tecmint/myapp/bin /home/tecmint/myapp/config/nginx /home/tecmint/myapp/config/systemd /home/tecmint/myapp/logs
/bin/sh 脚本不支持花括号展开
花括号展开是 Bash 特性,不是 POSIX Shell 标准。Ubuntu 和 Debian 上的 /bin/sh 通常指向 dash,不支持 Bash 风格的花括号展开。
因此,以 #!/bin/sh 开头的脚本可能把 bin,config 原样传给 mkdir,而不是拆成多个目录名。如果脚本依赖这类展开,应使用 Bash shebang:
#!/bin/bash
如果必须兼容 POSIX Shell,就应改用多个路径参数或循环。
2. 用 cat heredoc 一次创建带内容的文件
目录准备好后,下一步通常是创建配置文件。手动创建、打开编辑器、输入并保存适合偶尔操作;配置多台服务器或自动化部署时,这种方式就变得繁琐。
heredoc(here document)可以把多行文本直接传给命令,并用指定标记结束输入。配合 cat 和输出重定向,就能一次创建文件并写入内容:
cat << 'EOF' > ~/myapp/config/app.env
APP_NAME=myapp
APP_PORT=8000
LOG_DIR=/home/tecmint/myapp/logs
EOF
检查内容:
cat ~/myapp/config/app.env
输出:
APP_NAME=myapp
APP_PORT=8000
LOG_DIR=/home/tecmint/myapp/logs
命令各部分的作用:
<< 'EOF'开始 heredoc,EOF是自定义结束标记,最后一行必须完全匹配。- 给
EOF加引号会阻止 Bash 展开正文中的变量和命令替换。例如PATH=$HOME/bin会按字面写入。创建配置文件时,这通常是更安全的默认做法。 > ~/myapp/config/app.env会把内容写入文件并替换原内容;改用>>则追加。- 结束标记
EOF必须独占一行,前后不能有多余空格,除非使用明确允许缩进的 heredoc 写法。
在 heredoc 中展开变量
有时需要让 Bash 写入动态值,例如当前用户、主机名或部署日期。此时不要给结束标记加引号,Bash 会在把文本交给 cat 前完成变量和命令替换:
cat << EOF > ~/myapp/DEPLOYED.txt
Deployed by: $USER
Host: $(hostname)
Date: $(date +%F)
EOF
读取文件:
cat ~/myapp/DEPLOYED.txt
示例输出:
Deployed by: tecmint
Host: server1
Date: 2026-10-27
实际结果取决于执行命令时的用户、主机名和日期。
用 sudo tee 写入 root 所有的文件
heredoc 也适合创建 /etc 下的系统配置文件。常见错误是以为给命令加上 sudo,输出重定向也会获得同样的权限。
普通用户执行下面的命令时,因为没有目标目录写权限,会失败:
sudo cat << 'EOF' > /etc/sysctl.d/90-tecmint.conf
vm.swappiness = 10
EOF
可能出现类似错误:
bash: /etc/sysctl.d/90-tecmint.conf: Permission denied
原因是 Shell 会在执行 sudo cat 之前处理 > 重定向,并尝试用普通用户权限打开目标文件。应在提权状态下由 tee 打开和写入文件:
sudo tee /etc/sysctl.d/90-tecmint.conf > /dev/null << 'EOF'
vm.swappiness = 10
EOF
> /dev/null 会隐藏 tee 的正常输出,避免把配置内容打印到终端。若要追加内容,使用 sudo tee -a。
3. 用 tar -C 将压缩包解压到另一个目录
应用目录和配置文件准备好后,下一步通常是安装以 tar 包分发的软件。常见做法是先移动压缩包、切换目录,再执行解压;也可能先在 ~/Downloads 解压,再把目录移走。两种流程都有多余步骤。
tar 的 -C 选项会在解压前切换到指定目录。这样压缩包可以留在下载位置,内容直接进入目标位置。
Go 官方安装方式就是一个实用示例,可以把 Linux 压缩包直接解压到 /usr/local:
wget https://go.dev/dl/go1.25.3.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.25.3.linux-amd64.tar.gz
注意:请把
go1.25.3替换为官方网站当前列出的合适版本。
rm -rf /usr/local/go 会删除已有的 Go 安装。Go 项目建议这样做,因为直接在旧版本上覆盖解压可能留下过时文件。
解压后,Go 可执行文件位于 /usr/local/go/bin,整个过程不需要切换当前工作目录:
ls /usr/local/go/bin
输出:
go gofmt
让 tar 自动识别压缩类型
传统用法中,不同压缩格式需要不同选项,例如 gzip 用 -z、bzip2 用 -j、xz 用 -J。解压时,GNU tar 通常能自动识别常见格式,不必每次记住对应参数。
下面的命令会把 xz 压缩包直接解压到 /opt:
sudo tar -xf archive.tar.xz -C /opt
只要系统具备相应的解压支持,同一方式也适用于 gzip、bzip2、xz 和 Zstandard 压缩的 tar 包。若 .tar.zst 无法处理,请安装 zstd。
Ubuntu 和 Debian:
sudo apt install zstd
Rocky Linux、AlmaLinux 和 RHEL:
sudo dnf install zstd
tar -C 的常见错误
目标目录必须事先存在,因为 tar -C 只会切换到该目录,不会创建它。目录缺失时会报错:
tar: /opt/node: Cannot open: No such file or directory
tar: Error is not recoverable: exiting now
把 mkdir -p 与 tar -C 结合即可避免这个问题。另一个常见情况是压缩包内带有版本化的顶层目录,例如 node-v24.11.0-linux-x64/。如果希望该目录中的文件直接落到目标目录,可用 --strip-components=1 去掉第一层路径:
sudo mkdir -p /opt/node
sudo tar -xf node-v24.11.0-linux-x64.tar.xz -C /opt/node --strip-components=1
注意:压缩包文件名只是示例。请从 Node.js 官方下载页选择合适版本和架构。
解压陌生压缩包前,先查看内部结构:
tar -tf archive.tar.xz | head
这会列出压缩包中的前几个路径,帮助你判断是否需要 --strip-components=1。去掉过多层级可能错误地压平目录结构,或丢失有用的路径层级。
4. 用 pgrep 和 pkill 按名称查找、终止进程
测试服务器、卡住的脚本或失去响应的应用,最终都可能需要终止。传统方式是运行 ps -A | grep -i apache2,找到 PID 后再手动交给 kill。这样虽然可行,但步骤更多,而且 grep 匹配到自身命令行时还可能造成干扰。
更简单的方式是用 pgrep 查找进程,再用 pkill 按名称或完整命令行发送信号。
可以先启动一个安全的测试进程。Python 自带的 HTTP 服务在 Ubuntu 和 Rocky Linux 上都可用,末尾的 & 让它转到后台:
python3 -m http.server 8000 &
用 pgrep 查找进程。-a 会同时显示 PID 和完整命令行,更容易确认目标:
pgrep -a python3
示例输出:
4821 python3 -m http.server 8000
你的 PID 会不同。确认目标后,可直接用 pkill 发送信号,无需复制 PID:
pkill -e -f http.server
示例输出:
python3 killed (pid 4821)
-f 允许匹配完整命令行,因此即使进程名只是 python3,也能找到这个 HTTP 服务。-e 会显示被发送信号进程的名称和 PID。
常用的 pgrep、pkill 选项:
-f匹配完整命令行,而不只是进程名。适合多个程序共用同一可执行文件的场景,但模式必须足够精确。-x要求进程名完全匹配。例如pkill -x ssh只匹配名为ssh的进程,不会匹配sshd。-u tecmint只搜索tecmint用户拥有的进程,可降低共享服务器上的误匹配风险。-HUP或--signal HUP发送SIGHUP,而不是默认的SIGTERM。SIGTERM请求进程正常退出,SIGKILL(-9)则会强制终止,进程没有机会清理。
用 systemctl 停止系统服务,而不是 pkill
一些旧示例会用 pkill apache2 停止 Web 服务器。但对于 systemd 管理的服务,推荐使用 systemctl stop。
直接终止服务进程可能引发问题。如果服务单元配置了重启策略,systemd 可能再次启动它;否则服务可能进入失败状态,或让 systemd 记录的状态与实际意图不一致。
Ubuntu 和 Debian 上 Apache 服务名是 apache2:
sudo systemctl stop apache2
Rocky Linux、AlmaLinux 和 RHEL 上服务名是 httpd:
sudo systemctl stop httpd
pkill 更适合你明确要直接管理的进程,例如自己的测试服务器、卡住的脚本,或另一个终端中不应继续运行的程序。systemd 服务应交给 systemctl 正常停止。
5. 用 pidwait 等待进程结束
最后一个技巧建立在 pgrep 和 pkill 之上,但解决的是另一个问题:等待进程结束后再执行下一条命令。例如,你可能希望长时间运行的备份结束后才开始软件包升级。
常见替代方案是用 while 循环反复调用 pgrep,并用 sleep 暂停。procps-ng 提供的 pidwait 能等待匹配进程结束,写法更简单。
等待 rsync 结束后输出完成消息:
pidwait -x rsync && echo Backup finished at $(date +%T)
-x 要求精确匹配进程名,&& 确保只有 pidwait 成功退出时才打印消息。
需要注意,pidwait 只会等待命令启动时已经找到的进程。如果当时没有匹配进程,它会立即返回,而不会等待未来出现的 rsync。另外,如果存在多个同名进程,仅按进程名匹配也可能产生歧义。
pidwait 是否可用取决于已安装的 procps-ng 版本和发行版打包情况。检查命令:
command -v pidwait
如果没有该命令,可以使用轮询循环:
while pgrep -x rsync > /dev/null; do
sleep 5
done
echo Backup finished at $(date +%T)
循环每 5 秒检查一次,直到没有名为 rsync 的进程。与 pidwait 一样,如果启动时没有匹配进程,循环也不会等待未来任务。
它只检查进程名,不检查具体备份是否成功。因此,在结果很关键时,应使用更精确的进程匹配或明确的任务状态机制。
在脚本中安全使用这些命令
把上述技巧放进多台服务器上的脚本时,以下习惯能降低风险。
pkill -f 最大的风险是匹配模式过宽。像 pkill -f python 这样的模式会终止当前用户启动的所有 Python 程序。应先用 pgrep -a 和相同模式预览,并加上 -u 限定到任务所属用户。
在脚本中使用 heredoc 时,除非确实需要变量展开,否则应给结束标记加引号,例如 << 'EOF'。未加引号的 heredoc 如果包含本应写入配置文件的 $,例如密码或 Nginx 的 $host,会被静默替换为空字符串,最终得到一个看似正常但无法工作的配置。
最后,mkdir -p 的 -m 权限选项只作用于最后创建的目录,父目录仍使用默认权限。如果需要一次创建整条路径并指定权限和所有者,下面的写法更可预测:
sudo install -d -m 750 -o tecmint -g tecmint /path/to/dir
总结
这 5 个技巧分别替代了一类重复操作:用 mkdir -p 和花括号展开建立目录树,用 heredoc 配合 cat 或 sudo tee 写入文件,用 tar -C 把压缩包解压到目标目录,用 pgrep/pkill 按名称管理进程,以及用 pidwait 在进程结束后继续执行。合理使用它们,可以让脚本更短,也能减少笔误。










