第10章 文件处理

本章目标:掌握UNIX/Linux下文件、目录常用工具:查看文件列表、修改时间戳、临时文件、查找文件、xargs、磁盘空间、文件对比。

10.1 列出文件

echo 通配展示文件

  • 作用:shell先把通配符*展开成文件名列表,echo只是打印文本;不校验文件是否真实存在
1
2
echo /bin/*sh
# 输出:/bin/ash /bin/bash /bin/csh ……

⚠没有匹配文件时,shell不会展开,直接原样输出*字符串。

ls 命令

  • 作用:专门处理文件,校验文件是否存在,输出文件信息。

行为区别:输出到终端默认多栏;输出到管道/重定向,自动变成一行一个文件名

选项 说明
-1 数字1,强制单栏输出
-a 显示全部,包含.开头隐藏文件
-d 只看目录本身信息,不列出目录里面内容
-F 给不同类型文件末尾标记符号(/目录、*可执行、@软链接)
-l 小写L,长格式:权限、所有者、大小、修改时间
-i 打印inode号
-L 跟随软链接,显示链接指向的真实文件信息
-r 反转排序
-R 递归列出所有子目录
-S 按文件大小从大到小排序(GNU)
-t 按修改时间,新的放前面

示例

1
2
3
ls -l /bin/*sh      # 长格式查看shell程序
ls -d .*            # 查看隐藏文件,不打印目录内部
ls -a               # 显示当前目录所有,含隐藏文件

ls -l 长格式输出字段拆解

1
-rwxr-xr-x 1 root root 110048 Jul 17 2002 /bin/ash
  1. 第1字符:文件类型 -普通文件,d目录,l软链接
  2. 接下来9位:rwxr‑xr‑x 文件权限
  3. 链接计数
  4. 文件所有者
  5. 文件所属用户组
  6. 文件字节大小
  7. 修改时间
  8. 文件名

坑:文件名内部包含换行、tab特殊字符,ls终端会显示问号;通过管道od -a才能看到真实字节。所以不要解析 ls 的输出用于脚本处理文件名!

创建一个名字带换行的测试文件

shell 中 $'\n' 代表换行字符。文件名:one+ 换行 +two

1
touch $'one\ntwo'

现在目录里存在一个文件,文件名不是字符串one\ntwo,是:

1
2
one
two

直接运行 ls,终端输出看到问号

1
ls one*two

终端输出(交互式终端,stdout 指向屏幕)

1
one?two

ls 输出交给管道,用 od‑a 看真实字节

1
ls one*two | od -a

输出类似:

1
0000000   o   n   e  nl   t   w   o  nl

10.2 使用touch更新修改时间

作用两件事:

  1. 文件不存在:创建空文件
  2. 文件已存在:更新文件的时间戳(访问/修改时间),不会改动文件内容

创建空文件多种方式对比

1
2
> some-file          # 会清空已有文件!危险
touch some-file      # 安全,存在就仅更新时间;不存在创建空文件

touch选项

选项 作用
-m 只更新修改时间(默认)
-a 只更新访问时间
-t CCYYMMDDhhmm[.SS] 指定时间戳
-r ref_file 复制参考文件的时间戳到目标文件

示例

1
2
3
4
5
# 设置指定时间:1976‑07‑04 00:00
touch -t 197607040000.00 US-bicentennial

# 把birthday文件时间,复制为US‑bicentennial的时间
touch -r US-bicentennial birthday

小知识:UNIX时间戳epoch,起点1970‑01‑01 00:00 UTC;32位时间到2038溢出;现代系统64位时间。GNU touch -d可以解析人类可读日期,规避POSIX时间格式限制。

10.3 临时性文件的建立与使用

/tmp:开机经常清空;/var/tmp:重启数据保留;属于公共共享目录,写脚本要考虑安全问题。

方案1:$$ 进程ID生成临时文件(旧方案,不安全)

$$:shell内置变量,代表当前脚本进程PID。

1
2
3
4
5
umask 077                  # 设置掩码:其他用户不能读写这个临时文件
# 拼接临时文件路径,PID作为文件名后缀
TMPFILE=${TMPDIR-/tmp}/myprog.$$
# trap:脚本无论正常/异常退出,执行 rm -f $TMPFILE,自动清理临时文件
trap 'rm -f $TMPFILE' EXIT

安全缺陷:文件名可以被猜测,存在攻击风险。

${TMPDIR-/tmp} 拆解
1
${变量名-默认值}

- 是这个语法的固定符号,不是减号!
格式:${TMPDIR - /tmp}(书写时中间不能有空格!必须写成 ${TMPDIR-/tmp}

规则

  1. 如果环境变量 TMPDIR 已经定义、且不为空:就取变量本身的值。
  2. 如果 TMPDIR 没定义,或者是空值:就使用 - 后面的字符串 /tmp 作为返回结果。

案例 1:没有设置 TMPDIR

1
2
unset TMPDIR          # 删除这个环境变量,模拟不存在
echo ${TMPDIR-/tmp}

输出:/tmp

变量不存在,使用-后面的默认值 /tmp

案例 2:设置 TMPDIR=/home/xxx/mytmp

1
2
export TMPDIR=/home/zhangsan/mytmp
echo ${TMPDIR-/tmp}

输出:/home/zhangsan/mytmp

变量存在,直接拿 TMPDIR 本身的值,后面‑/tmp完全被忽略。

完整拼接

1
TMPFILE=${TMPDIR-/tmp}/myprog.$$

容易混淆的几个shell替换语法(必考,shell脚本学习指南原文知识点)

语法 作用
${VAR‑word} VAR未定义/空,返回word;有值返回VAR;不会修改变量本身
${VAR=word} VAR未定义/空,返回word,并且把word赋值给VAR变量
${VAR:-word} ${VAR‑word}几乎一样;就算VAR=““空字符串,也使用默认(更严格)
${VAR:=word} VAR为空/未定义,赋值word并返回

方案2:mktemp(推荐,安全,不可预测文件名)

作用:生成难以猜测的临时文件名/临时目录;自动设置权限,其他人无法访问。 模板中大写X会被随机字符替换;建议不少于12个X

1
2
3
4
5
# 创建临时文件
TMPFILE=$(mktemp /tmp/myprog.XXXXXXXXXXXX) || exit 1

# -d 创建临时目录
SCRATCHDIR=$(mktemp -d -t myprog.XXXXXXXXXXXX) || exit 1

注意:HP‑UX原生mktemp实现有安全缺陷,建议替换OpenBSD版本。

方案3:/dev/urandom /dev/random

二者是 Linux 下特殊字符设备文件,读取它,会吐出原始二进制随机字节。

  • /dev/random:熵池不足就阻塞卡住不动,脚本不要用。
  • /dev/urandom:永不阻塞,日常脚本优先选它

⚠注意:输出是原始二进制,里面会有各种不可打印字节、NUL、换行,不能直接拿来当文件名,需要做编码转换,转成可见字符(十六进制 /base64)。

⚠重要注意事项

  1. 一定要加 head -c N 限制读取字节数量! /dev/urandom 是无限数据流,不加 head 会源源不断读数据,脚本卡死。-c 16代表读取 16 字节,16 字节随机度已经很高。
  2. 不要直接把 /dev/urandom 原始二进制字节用作文件名! 二进制里面可能出现/,而/在 linux 文件名中是非法字符,会直接报错。 必须转成十六进制或者 base64 可见字符串之后,再做文件名。

方式 1:od 转十六进制

od -x:把二进制字节转成十六进制字符串。

1
2
# 读取16字节随机数据,转16进制
head -c 16 /dev/urandom | od -x
  • head -c 16:只读取 16 个字节,不要读无穷无尽的数据流!必须加,不然程序卡死一直读

输出示例:

1
2
0000000 2741 62d5 840c fa20 76bc 9023 188f 0934
0000020

再搭配tr删掉空格换行,拼接成随机字符串:

1
2
3
4
tr -d ' \n'
#      ↑↑
#      │└── \n 换行
#      └──── 空格
1
2
RAND_STR=$(head -c 16 /dev/urandom | od -x | tr -d ' \n')
echo $RAND_STR

tr '/+' '_-'命令详解

  • 第一组(左边):/+ 两个字符
  • 第二组(右边):_- 两个字符

对应关系:

  1. 遇见字符 / → 替换成下划线 _
  2. 遇见字符 + → 替换成横杠 -

10.4 寻找文件

10.4.1 locate 快速查找

locate:依靠预生成数据库,不用遍历磁盘,查找速度飞快;数据库一般由cron定时任务updatedb更新。

1
2
3
locate gcc-3.3.tar
# 使用通配符,一定要引号括起来,避免shell提前展开
locate '*gcc-3.3*.tar*'

⚠缺点:只能查到数据库生成之前存在的文件;刚新建文件locate找不到;安全版本slocate会过滤无权限访问的文件。

10.4.2 type:查看命令真实身份

shell内置命令,识别命令:外部程序、别名、shell函数。

1
2
3
type gcc
type type
type mypwd

10.4.3 find命令(最强大,磁盘实时遍历)

find语法:find [查找起始目录] [测试条件] [操作]

  • 特点:自动递归子目录;不会忽略.开头隐藏文件;输出不排序;
  • 逻辑:挨个遍历目录下每一个文件,执行测试条件;条件成立执行操作;默认操作打印文件名。
常用选项 说明 示例
-name 'pattern' 匹配文件名,通配符一定要引号保护,防止shell提前展开 find . -name '*.txt' 查找当前目录下所有后缀为txt的文件
-type f/d/l f普通文件;d目录;l软链接 find . -type f 只找普通文件,排除目录、软链接
-user username 匹配文件所有者 find /home -user root 查找home下属主为root的文件
-group groupname 匹配文件所属用户组 find . -group devel 查找组为devel的文件
-perm mode 权限匹配; ‑perm -002代表其他人可写 find . -perm -002 查找其他用户拥有写权限的文件
-size n[c/k] 文件大小;c字节,kKB; +大于,小于 find . -size +1024k 查找大于1MB的文件
-mtime n 文件修改时间,单位天; ‑n少于n天;+n大于n天 find . -mtime -7 查找7天之内修改过的文件
-atime n 文件访问时间(被读取) find . -atime +30 查找超过30天没有访问过的文件
-ctime n inode属性变更时间(权限、属主变化) find . -ctime -1 1天内inode信息发生变动的文件
-ls 输出类似ls‑li的详细信息 find . -type f -ls 找到文件并打印详细元数据
-prune 裁剪目录,不向下递归进入该目录 find . -path ./temp -prune -o -print 跳过temp目录,不进去搜索
-follow 遍历的时候跟随软链接,访问链接指向真实文件 find . -type l -follow 找到软链接并且追踪到真实文件

时间正负号规则: -n:少于n天;+n:大于n天;不带符号正好n天。

布尔运算符:

  • -a AND(默认,多个条件写一起就是and)
  • -o OR
  • ! NOT 取反

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 当前目录所有文件,递归
find .

# 查找所有*.html普通文件
find . -name '*.html' -type f

# 查找7天内修改过的文件
find . -mtime -7

# 查找其他人可写的文件
find . -perm -002

# 逻辑或:空文件 或者 一年以上没访问
find . -size 0 -o -atime +365

# !取反:查找不属于当前用户的文件
find $HOME/. ! -user $USER

find搭配while read处理文件:注意文件名含换行特殊字符风险;GNU find提供‑print0,搭配xargs ‑0安全处理任意文件名。

find -print0 安全处理特殊文件名

小记忆技巧

  • -print0:print 输出,使用 0 字节 (NUL) 做分隔符,所以叫 print0,连写。
  • -0:xargs 接收 0 字节分隔的数据。
1
2
# 安全列出所有文件,兼容带空格、换行、tab奇葩文件名
find . -type f -print0 | xargs -0 ls -l

10.5 执行命令 xargs

解决什么痛点

shell命令行有最大长度限制ARG_MAX;当$(find …)替换输出文件太多,报错 Argument list too long。 xargs:读取标准输入,把输入分批组装参数,调用后面的命令。

基础示例

1
2
# 在所有头文件搜索字符串
find /usr/include -type f | xargs grep POSIX_OPEN_MAX /dev/null

/dev/null小技巧:保证grep至少两个参数,每一条匹配行都会打印文件名;没有输入的时候xargs不会执行命令。

GNU扩展:xargs -0 配合find -print0安全处理带空格、换行文件名

1
find . -print0 | xargs -0 ls -l

对比:

  • $(find …):一次性全部参数,参数过长报错;文件名换行容易破坏解析
  • find | xargs:自动分批;特殊文件名有坑;
  • find -print0 | xargs -0:最安全,GNU工具集支持。
xargs 命令详解

xargs 通俗讲解

先看整条命令:

1
find /usr/include -type f | xargs grep POSIX_OPEN_MAX /dev/null

一、管道 | 作用

find 运行完,把找到的一堆文件名,一行一个输出到标准输出;
管道 |,把 find 的输出,直接送给右边 xargs标准输入

⚠重点:xargs 不会把管道过来的内容,当做后面命令的标准输入
xargs:读取管道过来的文本,把文本拆分成一个个参数,追加到后面命令的参数列表上,然后执行这个命令

二、拆解上面这条命令

  1. find /usr/include -type f 遍历 /usr/include,找出所有普通文件,输出:
1
2
3
4
/usr/include/stdio.h
/usr/include/stdlib.h
/usr/include/limits.h
……许许多多文件名……

每一个文件名占一行。

  1. 管道把上面一大串文件名全部交给 xargs。 xargs拿到这一堆文件名,做两件事:
    ① 把一行行的内容,拆分成一份份文件名;
    把这些文件名,当做参数追加给 grep 命令,然后调用grep。

原始写的命令:

1
xargs grep POSIX_OPEN_MAX /dev/null
  • grep 固定参数:POSIX_OPEN_MAX/dev/null
  • xargs把管道读到的文件名,追加到末尾

三、xargs 会实际分批执行类似下面的命令

假设find输出非常多文件,系统命令行装不下全部,xargs就会分组分批调用grep

第一批:

1
grep POSIX_OPEN_MAX /dev/null /usr/include/stdio.h /usr/include/stdlib.h ……

用完这一批名额,再执行第二批:

1
grep POSIX_OPEN_MAX /dev/null /usr/include/a.h /usr/include/b.h ……

这就是所谓:输入分组,调用后面的命令 xargs自动根据系统ARG_MAX限制,把长长的文件名列表切分成多组,多次运行grep,避免 Argument list too long 参数过长报错。

四、/dev/null 小技巧解释

grep语法:

1
grep 关键词  文件1 文件2 文件3
  • 多个文件参数的时候,匹配行会打印出文件名:匹配内容
  • 如果只给grep一个文件参数,grep就不打印文件名,只输出匹配内容,分不清来自哪个文件。

我们传给grep:/dev/null + 一堆头文件。 /dev/null是一个永远空的虚拟文件,它不会产生任何匹配;但它占了一个参数位置。 于是grep永远至少有两个文件参数,每一条匹配结果都会带上文件名

grep 有个行为:

  1. grep 关键字 单个文件:只给 1 个文件,匹配输出的时候,不打印文件名,只输出匹配到的行
1
2
3
4
# 只传1个文件
grep POSIX_OPEN_MAX /usr/include/stdio.h
#输出(没有文件名):
#define _POSIX_OPEN_MAX 16
  1. grep 关键字 文件 A 文件 B:传≥2 个文件,输出格式是 文件名:匹配行,带上来源文件名。
1
2
3
4
#传两个文件:/dev/null + stdio.h
grep POSIX_OPEN_MAX /dev/null /usr/include/stdio.h
#输出(带上文件名)
/usr/include/stdio.h:#define _POSIX_OPEN_MAX 16

模拟:

1
grep POSIX_OPEN_MAX /dev/null stdio.h stdlib.h

五、对比不用xargs的写法(有坑)

1
grep POSIX_OPEN_MAX $(find /usr/include -type f)

$(find …) 命令替换:把find全部输出,一次性全部变成grep参数。

  • 文件数量少:正常工作。
  • 文件数量巨大:参数列表超出系统上限,直接报错 Argument list too long

xargs 就是用来解决这个痛点:自动分批,多次执行命令。

六、举个极简小实验,方便你肉眼看懂xargs在干什么

准备3个文件

1
2
touch 1.txt 2.txt 3.txt
ls *.txt

六、测试1

1
find . -name '*.txt' | xargs echo ">>>files:"

find输出:

1
2
3
./1.txt
./2.txt
./3.txt

xargs读取,把文件名追加到echo后面,实际执行:

1
echo ">>>files:" ./1.txt ./2.txt ./3.txt

屏幕输出:

1
>>>files: ./1.txt ./2.txt ./3.txt

管道进来的内容,变成命令的参数,不是命令输入。

七、容易混淆重点(非常多人踩坑)

  1. cat file | xargs command 管道内容 → 变成command的命令行参数

  2. cat file | command 管道内容 → 变成command的标准输入

八、对比两条命令,看差别

1
2
3
4
5
# ① xargs版本:把abc当做echo的参数
echo "abc" | xargs echo "val:"

# ② 普通管道:abc进入echo标准输入(echo不读标准输入,所以abc不输出)
echo "abc" | echo "val:"

运行输出:

1
2
val: abc
val:

九、风险:文件名带空格、换行

普通 find | xargs 按换行切割;
如果文件名内部自带换行/空格,会切错。
安全版本:find -print0 | xargs -0,用NUL字节分割,彻底避开换行问题。

十、小结

  1. xargs读取管道输入,把输入内容拆分成参数,追加到后面命令的末尾,执行命令
  2. 文件太多的时候,xargs自动分成多组,多次运行命令,规避参数长度超限。
  3. 普通管道 cmd1 | cmd2:数据给cmd2做标准输入
    cmd1 | xargs cmd2:数据变成cmd2的命令行参数
  4. 处理任意奇葩文件名,务必使用 -print0 + -0 配对。

10.6 文件系统的空间信息

df 查看文件系统整体磁盘占用

disk free,查看各个挂载分区总、已用、可用空间。

选项 说明
‑k 输出单位KB
‑h human‑readable,人类可读单位(G/M),GNU
‑l 只看本地磁盘,排除网络文件系统
‑i 查看inode使用情况
1
2
3
df -k
df -h
df -i /tmp

du 统计目录占用空间

disk usage:统计指定目录树实际占用磁盘块大小

选项 说明
‑k 单位KB
‑s 只输出总汇总,不输出子目录每一项
‑h 人类可读格式(GNU)
1
2
3
4
5
# 汇总/var/log总大小
du -s -k /var/log

# 找出home目录占用磁盘最多用户,从大到小排序
du -s -k /home/users/* | sort -k1nr | less

注意:du统计会遇到权限不足报Permission denied,统计结果不完全;硬链接不会重复统计。

10.7 比较文件

cmp:二进制比较

  • 作用:逐字节对比两个文件,找到第一个不一样的位置就停止。
  • 退出码:0相同;1不同;>1出错。
1
2
3
cmp file1 file2
cmp -s file1 file2    # -s静默,不输出,只看$?退出码
echo $?

diff:文本文件差异

  • 作用:对比两个文本,输出行级别差异;输出格式可给patch工具使用。

规则:diff 旧文件 新文件 输出标记:

  • < 代表 左边第一个文件(旧文件 test.1) 的行
  • > 代表 右边第二个文件(新文件 test.2) 的行
  • a add 新增;d delete 删除;c change 修改

第一步:创建两个测试文件

test.1(旧文件)

1
2
3
4
苹果
香蕉
橙子
葡萄

test.2(新文件)

1
2
3
4
5
苹果
香蕉
芒果
葡萄
西瓜

文件差异说明

  1. 旧文件第 3 行:橙子 → 修改成了 芒果(change 修改 c)
  2. 新文件末尾多了一行:西瓜(add 新增 a)

终端执行命令

1
diff test.1 test.2

diff 输出结果

1
2
3
4
5
6
3c3
< 橙子
---
> 芒果
4a5
> 西瓜

逐行拆解输出

1
3c3
  • 左边3:旧文件 test.1 的第 3 行
  • c = change 修改
  • 右边3:新文件 test.2 的第 3 行

含义:旧文件第 3 行发生修改,对应新文件第 3 行

1
< 橙子

< 代表这一行来自左边旧文件 test.1,旧内容是:橙子

1
---

分割线,把旧内容、新内容隔开

1
> 芒果

> 代表这一行来自右边新文件 test.2,新内容是:芒果

1
4a5
  • 4:旧文件的第 4 行之后
  • a = add 新增
  • 5:新文件第 5 行

含义:在旧文件第 4 行的后面,新文件增加了第 5 行

1
> 西瓜

> 新增的内容来自新文件:西瓜

再演示一个删除 d 的例子

新建文件: test.old

1
2
3
张三
李四
王五

test.new

1
2
张三
王五

删掉了李四这一行

运行

1
diff test.old test.new

输出:

1
2
2d1
< 李四

解析:

  • 2d1:旧文件第 2 行被删除,对应新文件第 1 行之后
  • < 李四:被删掉的那一行,来自左边旧文件

patch 打补丁演示(承接第一个橙子芒果西瓜例子)

把 diff 输出保存成补丁文件

1
2
diff test.1 test.2 > test.patch
cat test.patch

test.patch 内容就是刚才 diff 输出全部文本。

执行 patch,把旧文件 test.1 更新成新文件内容

1
2
patch < test.patch
cat test.1

执行完之后 test.1 内容就变成和 test.2 一模一样:

1
2
3
4
5
苹果
香蕉
芒果
葡萄
西瓜

diff -c 上下文模式(context 差异)

1
2
diff -c test.1 test.2 > test.diff
patch < test.diff

diff -c test.1 test.2 上下文格式输出

*** 后面:旧文件 test.1---后面:新文件test.2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
*** test.1	2026‑09‑16 10:00:00.000000000 +0800
--- test.2	2026‑09‑16 10:00:10.000000000 +0800
***************
*** 1,4 ****
  苹果
  香蕉
! 橙子
  葡萄
--- 1,5 ----
  苹果
  香蕉
! 芒果
  葡萄
+ 西瓜
符号 含义
! 这一行发生修改 change
+ 新文件新增的行 add
旧文件要删掉的行 delete
空格 没有改动的上下文行

文件校验和 md5sum / sha256sum

作用:根据全部文件字节算出哈希指纹;快速判断两个文件内容是否完全一样;下载文件校验完整性。

1
md5sum /bin/*

示例脚本:利用awk统计md5,找出内容完全一样的文件。

1
2
3
4
5
6
7
md5sum "$@" /dev/null 2>/dev/null |
awk '{
    count[$1]++
    if(count[$1]==1) first[$1]=$0
    if(count[$1]==2) print first[$1]
    if(count[$1]>1) print $0
}'

GPG数字签名

md5/sha只能校验文件有没有被无意损坏;数字签名可以验证文件来源身份,确认文件没有被恶意篡改

  • 私钥:签名用,只有发布者持有
  • 公钥:所有人用来验证签名。

简单示例

1
2
# 验证签名文件
gpg coreutils-5.0.tar.gz.sig

10.8 小结

  1. ls查看文件;touch创建空文件、修改时间戳。
  2. 临时文件:优先mktemp;不要直接用$$生成文件名,有安全风险;脚本退出用trap清理临时文件。
  3. 查找文件:
    • locate:查数据库,速度快,非实时;
    • find:实时遍历磁盘,功能强大;处理特殊文件名务必使用‑print0 + xargs ‑0
  4. xargs解决命令行参数过长问题。
  5. df看分区整体磁盘;du统计目录树占用大小。
  6. 文件对比:cmp二进制对比;diff文本差异;patch应用补丁;md5sum做校验;gpg做数字签名验证来源。

重要坑汇总

  1. shell通配符没有匹配文件时,不会展开,保留原样字符串。
  2. find默认递归,‑name模式一定要用引号,防止shell提前展开。
  3. 文件名包含空格、换行,普通管道会解析错乱,GNU工具用‑print0 / xargs ‑0
  4. 临时文件放在公共目录/tmp,要设置umask,防止其他用户篡改。