Series · Linux · Chapter 8

Linux 文件操作深入解析

深入讲解 Unix 管道模型:stdin/stdout/stderr 与文件描述符、各种重定向写法、grep/awk/sed/cut/sort/uniq/xargs 工具链、命名管道与进程替换,配大量可直接套用的日志一行命令。

在命令行上拉开效率差距的,从来不是会多少命令,而是能不能把命令"拼起来"——把一堆小工具串成一条清晰的数据流。管道符 | 正是 Unix 哲学的体现:每个工具只做一件事并把它做好(grep 只过滤、awk 只提取字段、sort 只排序),然后通过管道把它们组合成一条可读、可调试、可维护的流水线。本文从数据流模型讲起——stdinstdoutstderr 以及它们背后的文件描述符——再系统过一遍各种重定向写法(>>><2>2>&1&>),然后把文本处理工具链(grepawksedcuttrsortuniqxargstee)一次讲透,最后补上两个大多数入门教程跳过的话题:命名管道(FIFO)和进程替换。读完之后,你应该能把很多"得写个脚本才行"的小需求,用一两行可读的命令搞定,也能更轻松地看懂别人写的 one-liner。

数据流模型:stdin、stdout、stderr

管道数据流:stdin / stdout / stderr

每个 Linux 进程一启动就自带三个打开的文件描述符。理解了它们,本文后面的内容才会真正立得住。

FD默认去向典型用途
stdin0终端键盘进程从这里读输入
stdout1终端屏幕正常结果往这里写
stderr2终端屏幕诊断信息、警告、错误往这里写

不那么显然的一点是 stdout 和 stderr 是分开的。它们默认都通向终端,所以交互式使用时看起来没区别——但内核把它们放在两个不同的文件描述符上。这种分离正是管道安全的根本原因:

  • 管道符 | 只传递 fd 1(stdout)。写到 fd 2 上的错误信息,不会污染流入下一阶段的数据。
  • 你可以把正常输出存进文件,同时让错误信息留在终端实时刷新;反过来也可以。
  • 脚本可以平时一句话不说,只有在出错时才通过 stderr 报警。

一个最小演示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ ls /etc /nonexistent
ls: cannot access '/nonexistent': No such file or directory   # stderr
/etc:                                                          # stdout
adduser.conf
...

$ ls /etc /nonexistent > out.txt
ls: cannot access '/nonexistent': No such file or directory   # stderr 仍然显示在终端
$ cat out.txt
/etc:
adduser.conf
...

只有 stdout 被存进了 out.txt,stderr 直接走到终端,因为我们没有重定向它。

文件描述符就是几个整数

文件描述符是内核给打开文件分配的句柄。在每个进程里,fd 0 / 1 / 2 留给三个标准流,之后每次 open() 拿到的就是 fd 3、4、5……。可以通过 /proc 看一个活进程的所有 fd:

1
2
3
4
5
$ ls -l /proc/$$/fd          # $$ 是当前 shell 的 PID
lrwx------ 1 kchen kchen 64 ... 0 -> /dev/pts/0
lrwx------ 1 kchen kchen 64 ... 1 -> /dev/pts/0
lrwx------ 1 kchen kchen 64 ... 2 -> /dev/pts/0
lr-x------ 1 kchen kchen 64 ... 3 -> /var/log/syslog

shell 里写的每一个重定向、每一个管道,归根结底都是 在程序启动前重新接线这几个小整数> 的意思是"exec 之前把 fd 1 替换成这个文件";| 的意思是"exec 之前把生产者的 fd 1 和消费者的 fd 0 换成内核管道的两端"。

重定向:把文件描述符接到哪里去

重定向:每个 fd 的去向

上图把日常会用到的六种写法摆在一起,下面把全套写法过一遍。

stdout:>>>

1
2
3
4
echo "hello" > out.txt        # 截断后写入
echo "world" >> out.txt       # 追加,保留原内容
ls -l > filelist.txt          # 保存目录列表
date >> deploy.log            # 给日志加一行时间戳

> 是破坏性的——shell 会在命令真正运行之前就把目标文件清空。如果命令随后失败了,原内容已经丢了。拿不准的时候用 >> 然后再清理,或者先写到一个临时路径里。

stderr:2>2>>

1
2
make 2> build-errors.log              # 只把错误写文件,正常输出留在屏幕
./long-job 2>> errors.log             # 多次运行的错误都追加到一起

一种常见组合是"丢掉无关输出,只留错误":

1
./scan-disks > /dev/null 2>> scan-errors.log

同时处理两路

正确的写法有两种,还有一种被广为流传的错写法。

1
2
3
4
5
6
# 现代写法(bash / zsh 推荐):
cmd &> out.log
cmd &>> out.log         # 追加模式

# 传统写法,老 shell 也兼容:
cmd > out.log 2>&1

传统写法有个顺序陷阱,每个人都会踩一次:

1
2
3
4
5
6
7
# 错的写法:先把 fd 2 复制到当前 fd 1(也就是终端)的位置,
# 然后才把 fd 1 改向文件。结果 stderr 留在终端,stdout 进了文件——
# 跟想要的完全反过来。
cmd 2>&1 > out.log

# 对的写法:先把 fd 1 改到文件,再把 fd 2 指向"fd 1 现在指向的位置"。
cmd > out.log 2>&1

从左往右读,记住 2>&1 的语义是"让 fd 2 指向 fd 1 此刻指向的地方"——而不是"把 fd 2 合并进 fd 1"。

丢弃输出:/dev/null

/dev/null 是内核里的"垃圾桶"。写进去的数据被全部丢弃,从它读则立刻得到 EOF。

1
2
3
4
cmd > /dev/null              # 静默正常输出
cmd 2> /dev/null             # 只静默错误
cmd &> /dev/null              # 全部静默
cmd > /dev/null 2>&1 &        # 完全静默地放后台跑

典型用途:cron 里的脚本(出错时才发邮件)、能力探测(command -v jq > /dev/null)、单纯关心"命令是不是 0 退出"的判断。

stdin:<、here-doc、here-string

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
sort < names.txt              # 用文件喂 stdin

# Here-document:多行字面量作为 stdin
cat <<EOF > /etc/motd
Welcome to $(hostname)
Today is $(date '+%Y-%m-%d')
EOF

# Here-string:单行字面量作为 stdin
grep -E "ERROR|WARN" <<< "$line"

Here-doc 是把配置文件内嵌进脚本、把多行 SQL 喂给 psql、不开新文件就拼小模板的标准方式。默认会做变量展开;如果想要 body 里的内容原样保留,把分隔符引起来:<<'EOF'

管道符:组合的精髓

管道是 Unix 里最简单的进程间通信原语,也是本文剩下内容能成立的前提。producer | consumer 同时做了三件事:

  1. 向内核申请一条匿名管道——一段内存里的小环形缓冲,有读端和写端两个。
  2. fork 出生产者,把它的 fd 1 通过 dup2 接到管道的写端。
  3. fork 出消费者,把它的 fd 0 通过 dup2 接到管道的读端。

两个进程随后 并发运行:生产者写、消费者读,缓冲满或空时由内核阻塞其中一方。整个过程不在硬盘上落任何临时文件。

一个标准例子:

1
2
$ cat access.log | grep "404" | wc -l
137
  • cat access.log——把日志内容流向自己的 stdout
  • grep "404"——从 stdin 读,只保留匹配行
  • wc -l——从 stdin 读,输出行数

这种风格之所以好,理由有三个:

  • 没有中间文件,数据全在内存里流动,不需要清理。
  • 流式,生产者刚写出第一行,消费者就开始处理,不必等整个文件读完。
  • 可组合,每一步都能独立测试、独立替换。

“无意义地用 cat”(UUOC)

上面的例子可以更直接地写:

1
grep "404" access.log | wc -l

绝大多数过滤工具都接受文件名参数,省掉那个 cat 既快一点也清楚一点。只有在你确实需要"流"——比如要拼多个文件、或为了让管道从上到下读起来更顺——才用 cat file | 开头。

tee 调试管道

tee 像水管的三通:把 stdin 写进若干文件 同时 也写到 stdout,下游阶段照样能拿到数据。

1
grep "404" access.log | tee 404.log | wc -l

现在你既得到了行数,又把所有匹配行存进了 404.log 方便事后查看。当一条管道行为不对时,往中间塞一个 tee /tmp/stage-N.txt,跑一次,再 head /tmp/stage-N.txt 看看那一段到底流的是什么。

tee -a 是追加而不是截断。tee 还有一个常见用法:在普通用户的 shell 里以 root 身份写文件——

1
echo "127.0.0.1 example.local" | sudo tee -a /etc/hosts

(直接写 sudo echo ... >> /etc/hosts 是不行的,因为重定向是由你当前的 shell 完成的,而那个 shell 不是 root。)

文本处理工具链

六个工具就能覆盖绝大多数日志和文本处理场景。把每个工具的 用途 想清楚,就不会动不动想去掏 Python 了。

grep:过滤行

grep 留下匹配模式的行,必须掌握的参数:

参数含义
-i忽略大小写
-v反向匹配(保留 匹配的行)
-n显示行号
-E扩展正则(`
-F固定字符串(不当正则用,处理纯文本时更安全也更快)
-r / -R递归子目录
-l只输出包含匹配的文件名
-c只输出匹配行数
-A N / -B N / -C N显示匹配后 / 前 / 前后各 N 行
-o只输出匹配的部分,每个匹配一行

实战组合:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 多关键词、忽略大小写、带 3 行上下文
grep -iEC 3 "error|fail|timeout" /var/log/syslog

# 全工程找 import requests 的 Python 文件
grep -rln --include='*.py' "^import requests" .

# 匹配 ERROR 但排除 ERROR_HANDLED
grep "ERROR" app.log | grep -v "ERROR_HANDLED"

# 抽出日志里所有 URL 并去重
grep -oE 'https?://[^ "]+' access.log | sort -u

-F 值得单独提一下:当要搜的是一个含有正则特殊字符的字面字符串(路径、IP、堆栈片段),grep -F 比手动转义更安全也更快。

awk:列与聚合

awk 是一门小型编程语言,模型是"对每一行,按字段切开后执行一段动作"。只要你的数据有 ,要做投影、按列筛选、按列聚合,就该想到它。

心智模型:

  • 默认分隔符是"任意空白",-F 可改。
  • $1$2 是字段引用,$0 是整行;NF 是字段数,NR 是行号。
  • 程序由若干 pattern { action } 构成,两部分都可省略。
  • BEGIN { ... } 在读入前执行一次,END { ... } 在读完后执行一次。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 投影:从 Nginx access log 里取 IP 和状态码
awk '{ print $1, $9 }' access.log

# 数值条件过滤:只看 5xx
awk '$9 >= 500 { print }' access.log

# 聚合:按 IP 计数
awk '{ count[$1]++ } END { for (ip in count) print count[ip], ip }' access.log \
  | sort -nr | head

# 自定义分隔符:解析 /etc/passwd
awk -F: '{ print $1, $7 }' /etc/passwd

# 求平均(假设第 10 列是响应时间,单位毫秒)
awk '{ sum += $10; n++ } END { if (n) print sum / n }' access.log

# 多条件:5xx 且响应时间超过 1 秒
awk '$9 >= 500 && $10 > 1000 { print $1, $7, $9, $10 }' access.log

count[$key]++ ... END { for (k in count) print count[k], k } 这个聚合套路是核心生产力:每次想写 Python 来"按某字段统计"之前,先看看一行 awk 是不是就够了。

sed:流编辑

sed 是非交互式编辑器。日常 95% 的场景是替换和删除。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 替换(每行只替换第一处)
sed 's/foo/bar/'      file.txt
sed 's/foo/bar/g'     file.txt    # 每行所有匹配
sed -E 's|/old/path|/new/path|g'  file.txt   # 用 | 当分隔符,省得转义斜杠

# 删除行
sed '/^$/d'           file.txt    # 空行
sed '/^#/d'           file.txt    # 注释行
sed '1,10d'           file.txt    # 前 10 行
sed '$d'              file.txt    # 最后一行

# 只打印某段(-n 抑制默认输出)
sed -n '10,20p' file.txt

# 按行号插入 / 追加
sed '1i\#!/usr/bin/env bash'  script.sh
sed '$a\# end of file'        script.sh

# 原地修改并保留 .bak 备份
sed -i.bak 's/listen 80;/listen 8080;/' /etc/nginx/nginx.conf

两个能省时间的小提醒:

  • 替换的分隔符 不一定/。当模式里本来就有斜杠时,sed 's|a/b|c/d|'sed 's/a\/b/c\/d/' 可读得多。
  • sed -i 在 GNU sed 和 BSD sed(macOS)上行为不同。sed -i.bak '...' 在两边都能用——总是写一个带后缀的备份,是值得养成的习惯。

cut、tr:用不上 awk/sed 时的轻量选手

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# cut:按分隔符取列
cut -d',' -f1,3        data.csv         # CSV 第 1 和第 3 列
cut -d':' -f1,7        /etc/passwd      # 用户名和 shell

# cut:按字符位置(定宽数据)
cut -c1-8              file.txt         # 每行前 8 个字符

# tr:字符级替换 / 删除
echo "HELLO" | tr 'A-Z' 'a-z'           # 转小写
echo "a b c" | tr ' ' '\n'              # 空格变换行
echo "abc123" | tr -d '0-9'             # 删数字
tr -s ' ' < file                        # 把多个空格压成一个

分隔符固定、只想取列时用 cut,比 awk 更省字。涉及字符级别的事(大小写、按字符切分、去掉某类字符)就用 tr

sort、uniq:排序与分组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
sort file.txt              # 字典序
sort -n file.txt           # 数字序(这样 "10" 才会排在 "9" 后面)
sort -h file.txt           # 人类可读的容量(1K、2M、3G)
sort -r file.txt           # 反向
sort -k2,2 file.txt        # 按第 2 列(按空白切)
sort -t',' -k3,3n data.csv # CSV 第 3 列,按数字
sort -u file.txt           # 一遍排序+去重

uniq           file        # 合并相邻重复
uniq -c        file        # 统计相邻重复出现次数
uniq -d        file        # 只显示有重复的行
uniq -u        file        # 只显示没有重复的行

关于 uniq 最重要的一句话:它只能处理相邻的重复。所以你几乎总要写成 sort | uniq -c | sort -nr 才能得到一份按频次排好的报表——第一个 sort 让重复挨在一起,uniq -c 计数,第二个 sort -nr 按数字倒序排。

实战:Nginx 日志分流

下面这几条命令值得每个值班的人背下来。假设 access.log 是标准的 combined 格式(第 1 列是客户端 IP,第 7 列是路径,第 9 列是状态码)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 访问最多的 10 个客户端 IP
awk '{ print $1 }' access.log | sort | uniq -c | sort -nr | head

# 访问最多的 10 个 URL
awk '{ print $7 }' access.log | sort | uniq -c | sort -nr | head

# 状态码分布
awk '{ print $9 }' access.log | sort | uniq -c | sort -nr

# 最近 1 小时的所有 5xx(假设日志时间戳是可排序的)
grep "$(date -d '1 hour ago' '+%d/%b/%Y:%H')" access.log \
  | awk '$9 >= 500'

# 哪些 IP 在打哪些 4xx 路径?
awk '$9 >= 400 && $9 < 500 { print $1, $9, $7 }' access.log \
  | sort | uniq -c | sort -nr | head -20

grep | awk | sort | uniq 流水线

上图把第三条命令拆开了:每一阶段都在 收窄或聚合 数据,到右端就只剩屏幕能装下的一份排好序的统计了。任何中间位置都可以塞个 tee 进去看具体形态。

xargs:当下游需要的是参数而不是 stdin

管道传的是 stdin。但很多最想串进来的工具——rmcpmvchmodgit checkout——接收的是 参数,不是 stdin。xargs 就是这两种接口之间的桥。

1
find . -name "*.tmp" | xargs rm        # 删除当前目录下所有 *.tmp

没有 xargs 的话,find ... | rm 什么也不做:rm 根本不读 stdin。

空格与换行的坑

find | xargs 这种朴素写法,一旦文件名里有空格或换行(是的,文件名里出现换行是合法的)就会出错。xargs 默认按空白分词,于是 my file.txt 会被拆成两个参数。永远把 find -print0xargs -0 配对使用:

1
find . -name "*.tmp" -print0 | xargs -0 rm

-print0 用 NUL 字节做分隔符;-0xargs 也按 NUL 切。NUL 是唯一不可能出现在文件名里的字节,所以这种组合稳如磐石。

更简洁也常常更优雅的做法是干脆不用 xargs,让 find 自己调命令:

1
2
find . -name "*.tmp" -exec rm {} +     # 批量调用,等价于 xargs -0
find . -name "*.tmp" -delete           # 删除有内置写法

-exec ... {} + 会把参数尽可能塞进一次调用里;-exec ... {} \; 是每个文件单独调一次(慢一些,但适用于"每次调用都要独立"的场景)。

常用参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 用占位符
find . -name "*.log" | xargs -I{} cp {} {}.bak

# 限制每次调用的参数个数(避免 argv 太长)
find . -name "*.json" -print0 | xargs -0 -n 100 jq -c .

# 并行:跑 N 个进程
find . -name "*.json" -print0 | xargs -0 -P 8 -n 1 jq -c . > /dev/null

# 每次调用前交互确认(小心模式)
find . -name "*.tmp" -print0 | xargs -0 -p rm

-P 是把"扫一遍所有 JSON 文件做语法检查"从 30 秒变成 4 秒(8 核机器上)的关键。配合 -n 1 让每次调用只接一个文件,否则一个慢的会拖累整批。

命名管道(FIFO)

匿名管道 vs 命名管道(FIFO)

| 创建的是 匿名管道——存在于内核内存里,文件系统上没有名字,生产者和消费者退出后就消失。两端必须在同一条命令行上。

命名管道(FIFO)是文件系统上有名字的管道。用 mkfifo 创建之后,任意两个进程——可能在不同终端、不同时刻启动——都能挂上去通信。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ mkfifo /tmp/jobs
$ ls -l /tmp/jobs
prw-r--r-- 1 kchen kchen 0 ... /tmp/jobs    # 开头那个 'p' 表示这是 FIFO

# 消费者(终端 A):阻塞读 FIFO,逐行处理
while read job; do
    echo "[worker] processing $job"
    sleep 1
done < /tmp/jobs

# 生产者(终端 B):把任务丢进 FIFO
echo "task-1" > /tmp/jobs
echo "task-2" > /tmp/jobs

第一次用会被两条语义绊住:

  • 对 FIFO 的 read 会阻塞,直到有人写。同样,write 也会阻塞,直到有人读(内核管道缓冲很小,一般 64 KiB)。
  • 写还在但读全部退出时,写方会收到 SIGPIPE。读还在但写全部退出时,下一次 read 会返回 EOF。

这种阻塞行为正是它的 用处:FIFO 就是一个零配置的微型任务队列或信号通道。常见用法包括:脚本之间的简单信号、给一拨突发的生产者前面挡一个长时间运行的消费者、把多个短命进程的日志汇聚到一个轮转器。

用完 rm /tmp/jobs 清理掉就行——FIFO 就是个文件。

进程替换:<(cmd)>(cmd)

进程替换:<(cmd) 把命令的 stdout 暴露成一个文件名

进程替换回答了一个问题:“如果我想把命令的输出喂给一个只接受文件名参数的工具,怎么办?”

1
diff <(sort file1) <(sort file2)

bash 在背后做了这样一件事:<(sort file1) 展开成一个路径——通常是 /dev/fd/63——读这个路径就能得到内层命令的 stdout。diff 把它当作一个普通文件打开,完全意识不到对面是个进程。

写成等价的临时文件版本,对比一下就明白价值在哪:

1
2
3
4
sort file1 > /tmp/a
sort file2 > /tmp/b
diff /tmp/a /tmp/b
rm /tmp/a /tmp/b

bash 替你做了所有 bookkeeping,而且内层命令是并发跑的,生产者和消费者在重叠执行而不是串行。

输出形式 >(cmd) 也存在——展开成的路径写入时会变成内层命令的 stdin。实战中少见,但能让人写出这样的模式:

1
2
# 用 tee 把一份流分给两个并发消费者
some_command | tee >(gzip > out.gz) >(sha256sum > out.sha256) > /dev/null

进程替换是 bash / zsh / ksh 的特性,纯 POSIX sh 没有。要写可移植脚本就退回到临时文件加 trap "rm -f $tmp" EXIT 的写法。

实战:批量文件操作

几个运维里反复出现的套路。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 递归设置 web 目录权限:文件 644,目录 755
find /var/www/html -type f -exec chmod 644 {} +
find /var/www/html -type d -exec chmod 755 {} +

# 压缩所有 7 天前的日志
find /var/log -name "*.log" -mtime +7 -exec gzip {} +

# 删除空文件(往往是写入失败留下的残骸)
find /tmp -type f -empty -delete

# 用 shell 参数展开把 img_*.jpg 批量改名为 photo_*.jpg
for f in img_*.jpg; do
    mv -- "$f" "${f/img_/photo_}"
done

# 用 rename(1)(Debian/Ubuntu 上叫 perl-rename)做同样的事
rename 's/^img_/photo_/' img_*.jpg

# 找大于 100 MB 的文件并按大小倒序列出
find / -xdev -type f -size +100M -exec du -h {} + 2>/dev/null | sort -hr | head

mv 后面那个 -- 是个值得养成的小习惯:它告诉命令"参数列表结束了",这样以 - 开头的文件名就不会被当成选项。

安全与最佳实践

几条避免最常见管道事故的规则。

1. 永远不要解析 ls 的输出。 ls 是给人看的,输出格式不是稳定的交换格式。空格、换行、glob 字符、locale 相关的日期格式都会让朴素的解析挂掉。

1
2
3
4
5
6
7
# 错的
ls *.log | xargs rm

# 对的
find . -maxdepth 1 -name "*.log" -print0 | xargs -0 rm
# 或者直接:
rm -- *.log

2. 破坏性命令先预览。-delete 换成 -print(或把 rm 换成 echo rm),文件清单看着没问题再真删:

1
2
find . -name "*.tmp" -print     # 预览
find . -name "*.tmp" -delete    # 实删

3. 所有变量展开都加引号。 这是"在我机器上是好的"问题最大的源头:

1
2
3
dir="my reports"
rm -rf $dir       # 错:被展开成 "rm -rf my reports",把 'my' 和 'reports' 都删了
rm -rf -- "$dir"  # 对

4. 脚本里加 set -euo pipefail

1
2
#!/usr/bin/env bash
set -euo pipefail   # -e: 出错就退出;-u: 用未定义变量就退出;-o pipefail: 管道里任一阶段失败整条算失败

不加 pipefail 的话,管道的退出码是 最后一个 阶段的退出码。中间阶段悄悄失败(grep 没匹配到、sort 写不下盘)会被掩盖。加了 pipefail,第一个非零退出会传播到外面。

5. 不要 eval,更不要把外部输入直接塞进 shell 命令。 来自外部的值要么先做正则校验([[ "$x" =~ ^[A-Za-z0-9._-]+$ ]]),要么作为参数传给一个不调用 shell 的工具。

总结

到这里,你应该有了一张完整的图,而不是一堆零散的命令:

  • 数据流模型:三个流(stdin/stdout/stderr)映射到三个文件描述符(0/1/2)。管道只传 stdout。
  • 重定向> >> < 2> 2>&1 &> << <<<,六种写法覆盖所有场景,并且你理解 为什么 2>&1 >file 是错的。
  • 工具链grep 过滤、awk 投影与聚合、sed 编辑、cut/tr 简单场景、sort | uniq -c | sort -nr 出频次榜。
  • 参数桥接xargs(以及 find -exec ... {} +)打通 stdin 风格和 argv 风格的工具,并且记得用 NUL 分隔来兜住带空格的文件名。
  • 两个少见但好用的模式:命名管道用于跨进程会合;进程替换用于"我需要文件名,但手头是命令"。
  • 安全:变量加引号、删之前先预览、永不解析 ls,每个脚本顶上都写 set -euo pipefail

扩展阅读

  • The Art of Command Line —— 一份精挑细选的命令行惯用法清单。
  • man bash,重点看 REDIRECTIONPipelinesProcess Substitution 三节。
  • info coreutils —— grepsortcuttruniq 等的权威参考。

这是 Linux 系列的最后一篇。前面七篇给了你模型——进程、文件、权限、磁盘、服务、软件包、用户——这一篇给的是 黏合剂。真实的运维工作大半就是 应用这层黏合剂:把"我得知道 X"翻译成一行命令,把"对这里所有文件做某操作"翻译成一句安全的 find ... -exec,把 shell 当成你手头最通用的可编程工具来用。

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub