Shell 文件与目录操作
更新时间:2026-08-28。本文是 Shell 编程主题第④族:文件与目录操作,共 18 个命令。
与 Linux 命令速查第①族 的分工:那边是字典式速查(33 个命令,想不起参数时查);本文把其中最常用的 18 个讲透——每个命令的核心参数、选项组合、以及脚本里最容易踩的坑。读完这篇,日常文件操作基本不用再翻手册。
一、路径与浏览
1.1 pwd 与 cd
pwd 输出当前工作目录。它有个容易被忽略的区别:-P 显示物理路径(解析掉所有软链接),-L 显示逻辑路径(保留你 cd 进来时用的软链接路径)。默认行为是 -L。在部署脚本里判断"当前到底在哪个目录"时,用 pwd -P 更可靠——否则拿到的是链接名而非真实目录,后续拼接路径可能出错。
cd 除了切目录,有几个实用特性:cd - 在最近两个目录间来回切换,排查问题时在两个目录间反复跳转特别省事;cd ~user 直接进指定用户的家目录;CDPATH 环境变量可以设置"搜索基目录",之后 cd project 会先在当前目录找,找不到就去 CDPATH 列的目录里找,类似 $PATH 的机制。脚本里 cd 失败会让后续命令在错误目录下执行,所以要么接 || exit 1,要么用 cd ... && 命令 的形式。
pwd # 逻辑路径(默认)
pwd -P # 物理路径,解析掉软链接
cd /var/log # 切到绝对路径
cd - # 回上一个目录(反复切换很方便)
cd ~alice # 进 alice 的家目录
export CDPATH=~/projects:/srv # 之后 cd proj 会在这两个目录下找1.2 ls — 列目录
ls 的参数看着多,实际围绕三件事:显示什么(隐藏文件、目录本身)、怎么排(时间、大小、倒序)、显示多详细(长格式、人类可读大小、inode)。组合起来用是常态,比如 ls -lhrt 就是"长格式 + 人类可读 + 倒序 + 按时间排",看日志目录时最常用的组合。
几个细节值得留意:-a 会连 .(当前目录)和 ..(上级目录)一起列出,而 -A 列出隐藏文件但排除这两个,日常想要"看全"应该用 -A 而非 -a。-d 单独用没什么意义,但配合 */ 可以只列子目录(ls -d */),或者用 ls -ld dirname 看目录本身的属性而不是它的内容。-i 显示 inode 号,排查硬链接(两个文件名 inode 相同即为硬链接)时会用到。-F 在目录后加 /、可执行文件后加 *,肉眼区分类型很直观。-R 递归列出所有子目录,看目录结构时比反复 cd 快。
ls -lh # 长格式 + 人类可读大小
ls -lhrt # 按时间倒序(最新的在最后)
ls -lS # 按大小排序
ls -A # 含隐藏文件,但排除 . 和 ..
ls -d */ # 只列子目录
ls -i file # 显示 inode 号
ls -F # 目录加 /,可执行文件加 *
ls -R dir/ # 递归列出
ls -lt --time-style=long-iso # 时间显示成 2026-08-28 10:301.3 tree — 树状结构
tree 把目录画成树,看项目骨架一目了然。常用 -L 限制层数(不限制的话,遇到 node_modules 能刷出几千行),-d 只看目录,-I 按模式排除(-I 'node_modules|.git')。-h 显示文件大小,-C 强制彩色输出,-f 显示完整路径前缀(方便复制路径)。它通常需要额外安装,不是系统自带。
tree -L 2 # 只看两层
tree -d -L 2 # 只看目录
tree -I 'node_modules|.git|__pycache__' # 排除噪音目录
tree -h # 带文件大小
tree -f -L 2 # 显示完整路径二、创建与删除
2.1 mkdir 与 rmdir
mkdir 的核心是 -p:没有它,创建多级目录(父目录不存在)会直接报错;有了它,缺多少级就建多少级,且已存在时不报错——这个幂等特性让它在脚本里特别好用,不用先判断目录是否存在。-m 可以在创建的同时指定权限(mkdir -m 700 private),省掉先建后改权限的两步。另外配合 Shell 的大括号扩展能一次建一批:mkdir -p app/{logs,conf,data} 会建出三个子目录。
rmdir 只能删空目录,非空就报错。这个"限制"其实是安全特性——它保证你不会误删有内容的目录。想删非空目录得用 rm -r。rmdir -p a/b/c 会连带删除变成空的父目录。
mkdir -p /data/app/logs # 递归创建,已存在不报错
mkdir -m 700 secrets # 创建即设权限
mkdir -p app/{logs,conf,data} # 一次建三个子目录
mkdir -pv /a/b/c # -v 显示创建过程
rmdir emptydir # 只能删空目录
rmdir -p a/b/c # 删 c,若 b、a 随之变空也一并删2.2 touch — 创建与改时间戳
touch 有两个用途:文件不存在时创建空文件;文件存在时只更新它的时间戳(内容不动)。后者在写"哨兵文件"或触发依赖时间戳的构建工具时很有用(比如 touch *.c 强制 make 重编)。
时间戳分三种,touch 默认同时更新访问时间(atime)和修改时间(mtime):-a 只改访问时间,-m 只改修改时间。想指定具体时间用 -t(格式 [[CC]YY]MMDDhhmm[.ss])或更友好的 -d(touch -d "2 days ago" file)。-r 把另一个文件的时间戳复制过来,批量对齐时间戳时方便。-c(或 --no-create)很实用:文件不存在时不创建,只更新已存在文件的时间——避免在脚本里意外造出空文件。
touch newfile # 创建空文件
touch existing_file # 只更新时间戳,内容不变
touch -c maybe_missing # 不存在就不创建,存在才更新
touch -d "2 days ago" f # 改成两天前
touch -t 202608251200 f # 指定时间 2026-08-25 12:00
touch -r source target # 把 source 的时间戳复制给 target
touch *.c # 强制 make 重新编译2.3 rm — 删除(谨慎)
rm 的选项很简单,但它是 Linux 上最危险的命令之一,值得多说几句。-r(递归)删目录,-f(强制)不提示且忽略不存在的错误,-i 逐个确认。-v 显示删除过程,批量删除时能看到进度。--preserve-root 是现在的默认保护:它会拒绝 rm -rf /,防止自杀式误操作。
真正的风险来自变量拼接:rm -rf $DIR/* 在 $DIR 未赋值时会变成 rm -rf /*。防御写法是用 ${VAR:?}——变量为空时直接报错退出,或者先 [ -n "$VAR" ] || exit 1 校验。另一个习惯是删除前先 ls 看一遍匹配结果,或用 find ... -print 演练。rm 还有个 -- 分隔符:rm -- -weird-name 用来删除以 - 开头的文件名(否则会被当成选项)。
rm file.txt # 删文件
rm -r dir/ # 递归删目录
rm -f file # 强制,不提示、不存在也不报错
rm -i *.log # 逐个确认
rm -v *.tmp # 显示删除了哪些
rm -- -weird-name # 删以 - 开头的文件
# 安全写法:变量必须非空
[ -n "$TARGET" ] || { echo "TARGET 未设置" >&2; exit 1; }
rm -rf "${TARGET:?}/cache"想有个后悔药,可以装
trash-cli(提供trash-put命令),它把文件移到回收站而不是直接删除。生产环境批量删除前,先备份、先演练、先小范围试,这三条习惯比任何命令参数都管用。
三、复制、移动与链接
3.1 cp — 复制
cp 的参数主要解决"复制时要保留什么":默认只复制内容,属主、权限、时间戳都会变。-p 保留权限与时间戳,-a(archive)是"全保留"——等价于 -rpP,连软链接本身也保留(不跟随链接复制内容),备份目录时应该用 -a 而不是 -r。
其他实用选项:-i 覆盖前确认,-n 不覆盖(已有就跳过),-u 只在源文件比目标新或目标不存在时才复制(增量同步很好用),-v 显示过程。-r 递归复制目录。-s 创建软链接而非复制内容,-l 创建硬链接——这两个用来"看起来复制了,实际不占空间"。--parents 会连父目录结构一起建出来(cp --parents a/b/c.txt /backup/ 会在 backup 下建出 a/b/ 再放 c.txt)。cp -t dir src... 把目标目录写前面,配合 xargs 很好用。
cp src.txt dst.txt # 复制文件
cp -r srcdir/ dstdir/ # 递归复制目录
cp -a /data /backup/ # 归档复制,保留全部属性(备份首选)
cp -u src dst # 只在 src 更新时复制(增量)
cp -n src dst # 不覆盖已存在的
cp -i src dst # 覆盖前询问
cp --parents a/b/c.txt /backup/ # 保留目录结构3.2 mv — 移动与重命名
mv 在同一文件系统内只是改目录项(重命名),几乎瞬时完成,跟文件大小无关;跨文件系统则会退化成"复制 + 删除",大文件跨分区移动会很慢,且中途失败可能留下半截文件——这是排查"mv 为什么这么慢"的关键。
选项:-i 覆盖前确认,-n 不覆盖,-u 只在源更新时移动,-v 显示过程,-b 覆盖前先备份目标(生成 ~ 后缀)。-t dir src... 同样支持目标前置。mv 没有 -r,移动目录不需要递归选项——它直接改目录项。
mv a.txt b.txt # 重命名
mv file.txt /tmp/ # 移动
mv -b config.ini /etc/ # 覆盖前备份成 config.ini~
mv -n src dst # 不覆盖
mv *.log logs/ # 批量移动3.3 ln — 软链接与硬链接
这是最容易混淆的一对概念,值得讲清楚。软链接(symbolic link,ln -s)是一个独立的小文件,内容是目标路径——它有自己的 inode,可以跨文件系统,可以指向目录,目标删了它就变成"断链"(ls 里显示为红色闪烁)。硬链接(ln 不加 -s)是同一个 inode 的第二个名字,与原名完全平级,删掉其中一个名字数据仍在,只有所有名字都删掉(链接计数归零)数据才真正释放;硬链接不能跨文件系统、不能指向目录。
实际坑点:用相对路径创建软链接时,链接里存的就是你写的那个相对字符串,一旦把链接文件移到别处,相对路径就失效了。稳妥做法是用绝对路径创建,或保证链接与目标同目录。另一个坑是 ln -s target existing_dir/:如果目标位置已存在同名链接,ln 会在该目录里再建一个(而不是覆盖),于是出现 link/link 这种嵌套。用 -f 强制覆盖,再配合 -n(把已存在的链接当文件处理)避免嵌套:ln -sfn target linkname。-r 可以自动把绝对路径转成相对路径,让整组链接可整体搬迁。
ln -s /data/app current # 软链接(绝对路径,推荐)
ln -sfn /data/app current # 强制覆盖已有链接(-n 防嵌套)
ln -sr /data/app rel_link # 创建相对路径的软链接
ln file.txt hardlink # 硬链接
ls -li # -i 看 inode:硬链接 inode 相同
readlink -f current # 解析出最终真实路径
find . -xtype l # 找出所有断链(指向不存在的链接)四、查找与文件信息
4.1 find — 最强大的查找工具
find 的能力远超"按名字找文件",它的表达式系统可以按类型、大小、时间、权限、属主、链接数等十几种条件筛选,还能对结果直接执行动作。基本结构是 find <路径> <条件> <动作>。
按名字:-name 区分大小写、-iname 不区分;注意模式要加引号,否则 Shell 会先展开通配符。-path 匹配完整路径(排除目录用 -path '*/node_modules' -prune)。-regex 用正则匹配整条路径。
按类型:-type f(普通文件)、d(目录)、l(软链接)、b/c(块/字符设备)、s(套接字)、p(管道)。-empty 找空文件或空目录,-xtype l 专门找断链。
按大小:-size +100M(大于)、-100M(小于)、100M(精确);单位 k/M/G(也支持 c 字节、w 双字节块)。
按时间:-mtime(内容修改)、-atime(访问)、-ctime(状态变更,如改权限)。-n 表示 n 天内,+n 表示超过 n 天。还有按分钟计的 -mmin。-newer file 找比指定文件更新的文件,做增量备份时常用。-newermt "2026-08-01" 直接跟日期字符串。
按权限/属主:-perm 644(精确)、-perm -644(包含这些位)、-perm /u+w(任一匹配);-user/-group 按属主属组筛选。
控制搜索:-maxdepth n/-mindepth n 限制搜索深度,不限制的话 find 会遍历整棵子树,在大目录上很慢。
组合条件:默认多个条件是"与"关系(可显式写 -a);-o 是"或";! 或 -not 是"非"。注意优先级,复杂条件建议用括号(要转义:\( ... \))。
执行动作:-print(默认)、-print0(用 \0 分隔,处理含空格文件名必用)、-delete(直接删除)、-ls(按 ls 格式显示)、-exec cmd {} \;(每个文件执行一次)、-exec cmd {} +(批量传参,快得多)、-ok(等同 -exec 但逐个确认)。
find . -name "*.log" # 按名查找(模式要加引号)
find /var -type f -size +100M # 大于 100M 的文件
find . -type d -name node_modules -prune # 排除 node_modules 目录
find . -mtime -7 # 7 天内修改过的
find . -mmin -30 # 30 分钟内修改过的
find . -newermt "2026-08-01" # 比指定日期新的
find . -empty # 空文件/空目录
find . -xtype l # 断链
find . -perm 644 -user alice # 组合条件(默认 AND)
find . -name "*.tmp" -o -name "*.bak" # OR 条件
find . -maxdepth 2 -type f # 只搜两层
find . -name "*.tmp" -delete # 找到即删
find . -type f -perm /u+x # 有执行位的文件
-exec cmd {} \;与-exec cmd {} +的差别在性能:前者每找到一个文件就启动一次cmd进程,一万个文件就是一万次进程创建;后者把尽可能多的文件名一次性传给cmd,通常只需几次。批量操作一律用+。需要逐个处理的复杂逻辑,用find -print0 | xargs -0。
4.2 locate — 索引式快速查找
locate 查的是预先建好的索引数据库,速度极快(全盘搜索毫秒级),代价是结果不是实时的——刚创建的文件要等 updatedb 之后才能找到。-i 忽略大小写,-c 只计数不列文件名,-b 只匹配路径的最后一段(basename),-e 只列出仍然存在的文件(过滤掉已删除但索引未更新的条目),-r 用正则匹配。想限制数量可以 locate pattern | head -20。它适合"我记得有个文件叫 xx 但忘了在哪"的场景;要精确、实时的查找还是用 find。
locate nginx.conf # 按索引快速找
locate -i readme # 忽略大小写
locate -c "*.log" # 只统计数量
locate -b sshd # 只匹配文件名部分
locate -e "*.tmp" # 只列仍存在的
sudo updatedb # 手动更新索引(新建文件后)4.3 stat 与 file — 文件元信息
stat 显示文件的完整元信息:inode 号、链接数、权限(八进制与符号两种)、属主属组、大小、以及三个时间戳。这三个时间容易混:atime(访问时间,读取内容时变)、mtime(修改时间,内容变化时变)、ctime(状态变更时间,改权限/属主/重命名时变,注意 c 不是 create)。判断是否被改过主要看 mtime。-f 看文件所在文件系统的信息(总容量、剩余、inode 数),-c 用格式化字符串只取需要的字段(脚本里解析很方便),-t 输出成单行紧凑格式。
file 不看扩展名,而是读文件头的"魔数"(magic number)来判断真实类型,所以改了扩展名也骗不过它。-i 输出 MIME 类型,-b 不显示文件名(只输出结果,脚本里好用),-L 跟随软链接看目标的类型,-s 用于读取特殊文件(如块设备)的内容来判断,-z 尝试探测压缩文件内部的原始类型。
stat file.txt # 完整元信息
stat -c '%s %U %a' file.txt # 只取大小、属主、八进制权限
stat -c '%y' file.txt # 格式化输出 mtime
stat -f / # 看文件系统(总容量、inode 数)
file app.bin # 判断真实类型
file -i image.png # MIME 类型
file -L symlink # 跟随链接看目标类型
file -s /dev/sda1 # 探测设备上的文件系统五、权限管理
5.1 chmod — 权限设置
权限分三组:属主(u)、属组(g)、其他人(o),a 代表全部三组。每组三个位:读(r=4)、写(w=2)、执行(x=1)。数字法就是把每组的值相加:755 = 属主 rwx(7)、组 r-x(5)、其他 r-x(5)。
符号法更灵活:chmod u+x file(属主加执行)、chmod go-w file(组和其他去掉写)、chmod a=r file(所有人设为只读)。+/- 是增减,= 是直接设定。-R 递归改目录。
有个特殊的大写 X:它只给目录或已有执行位的文件加执行权限,chmod -R u+X dir/ 可以把整个目录树改成可进入、但不会把普通文本文件也变成"可执行"——比 chmod -R 755 规范得多。
三个特殊位要留意:SUID(u+s,数字 4xxx)让程序以文件属主的权限运行,典型例子是 /usr/bin/passwd(普通用户能改 shadow 靠的就是它);SGID(g+s,2xxx)用在目录上时,新建文件自动继承目录的属组,团队协作目录几乎必配;sticky bit(+t,1xxx)让目录里的文件只有属主能删,/tmp 就是靠它防止用户互删。给随意一个程序设 SUID 是严重安全风险,等于开放了一段以 root 权限运行的任意代码。
chmod 755 script.sh # 数字法
chmod u+x script.sh # 符号法:属主加执行
chmod go-w file # 组和其他去掉写权限
chmod a=r file # 所有人只读
chmod -R u+X dir/ # 目录加执行,普通文件不加(规范写法)
chmod u+s /usr/bin/xxx # SUID
chmod g+s /shared/ # SGID:新文件继承目录属组
chmod +t /tmp # sticky bit5.2 chown — 改属主与属组
chown 同时改属主和属组,用冒号分隔:chown alice:dev file。只改属组可以省略属主写成 chown :dev file(等价 chgrp dev file)。老式写法用点号(alice.dev),但用户名若含点号会歧义,推荐一律用冒号。-R 递归改整个目录。--reference=src dst 把 src 的属主属组复制给 dst,批量对齐权限时省事。
一个细节:-R 默认会跟随符号链接改到目标文件上,这通常不是你想要的;加 -h 只改链接本身(不跟随)。另外 chown -R 递归时会连目录下的软链接目标一起改,容易误伤,建议配合 find -type f/d 精确控制范围。
chown alice file.txt # 只改属主
chown alice:dev file.txt # 同时改属主属组
chown :dev file.txt # 只改属组(等价 chgrp)
chown -R alice:dev dir/ # 递归
chown -h alice symlink # 只改链接本身,不跟随
chown --reference=src dst # 复制 src 的属主属组5.3 umask — 默认权限掩码
umask 决定新建文件和目录的默认权限。它是"要扣掉的权限",而不是"要赋予的"。算法的起点是:新建文件 666(注意文件默认没有执行位,这是安全设计)、新建目录 777;然后减去 umask 值。所以 umask 022 时:文件 = 666 - 022 = 644,目录 = 777 - 022 = 755。
常见的 umask 002(文件 664、目录 775)用于团队协作场景——同组用户能互相修改;umask 077(文件 600、目录 700)则是最严格的"只有自己能看",处理私钥、密钥文件时应设成这个。umask 不带参数显示当前值,-S 用符号形式显示更易读。它是当前 Shell 的属性,子进程继承;想永久生效要写进 ~/.bashrc 或 /etc/profile,systemd 服务则在 unit 里配 UMask=。
umask # 查看当前掩码
umask -S # 符号形式显示,如 u=rwx,g=rx,o=rx
umask 022 # 文件 644、目录 755(最常见)
umask 002 # 文件 664、目录 775(团队协作)
umask 077 # 文件 600、目录 700(最严格)六、批量处理:xargs
xargs 把标准输入转成命令行参数,是"管道"与"命令参数"之间的桥梁。很多命令(如 rm、cp)不接受管道输入,只接受参数,xargs 就是解决这个错配的。
它的关键参数:-n N 每次只传 N 个参数(-n1 逐个处理);-I{} 用占位符,可以在命令里多次引用同一个值(xargs -I{} cp {} {}.bak);-0 配合 find -print0 处理含空格的文件名(必写项);-P N 并发执行,处理大量文件时提速显著;-t 先打印要执行的命令再执行(调试用);-r(GNU 扩展)输入为空时不执行命令,避免 rm 无参数报错;-L N 按行而不是按空白分割。
find . -name "*.tmp" -print0 | xargs -0 rm # 安全处理空格文件名
ls *.log | xargs -I{} mv {} {}.bak # 占位符批量改名
echo "a b c" | xargs -n1 echo # 每次传一个参数
find . -type f -print0 | xargs -0 -P 8 gzip # 8 并发压缩
xargs -a hosts.txt -I{} ping -c1 {} # 从文件读参数七、脚本实战:把命令串起来
前面分节回答"单个命令怎么用",这一节回答"实际任务怎么拆解"。Shell 的价值不在记住多少参数,而在用管道把小工具拼成解决方案。
7.1 find + xargs 批量处理
# 统计所有 .log 的总大小(-print0/xargs -0 安全处理空格文件名)
find /var/log -name '*.log' -print0 | xargs -0 du -ch | tail -1
# 批量替换配置文件里的旧域名(改前先备份)
find ./conf -name '*.conf' -print0 | xargs -0 sed -i.bak 's/old.example.com/new.example.com/g'
# 清理 7 天前的临时文件
find /tmp -type f -name '*.tmp' -mtime +7 -delete7.2 安全的批量删除
# 第一步:只看,不删
find /data/cache -type f -name '*.tmp' -print
# 第二步:确认无误后再删
find /data/cache -type f -name '*.tmp' -delete
# 变量必须校验非空
[ -n "$TARGET" ] || { echo "TARGET 未设置,拒绝执行" >&2; exit 1; }
rm -rf "${TARGET:?}"7.3 遍历目录
# 正确:进程替换,循环体内变量能带出来
while IFS= read -r dir; do
echo "处理 $dir"
done < <(find . -maxdepth 1 -type d)
# 陷阱:管道右侧的 while 在子 Shell 里,变量出不来
find . -maxdepth 1 -type d | while read -r dir; do
COUNT=$((COUNT + 1)) # 这个 COUNT 出了循环就没了
done7.4 批量重命名与权限修正
# *.txt 批量改成 *.md
for f in *.txt; do
[ -e "$f" ] || continue
mv -- "$f" "${f%.txt}.md"
done
# 目录 755、文件 644 分开设置(不要 chmod -R 755)
find /var/www -type d -exec chmod 755 {} +
find /var/www -type f -exec chmod 644 {} +八、与主线衔接
find+xargs+grep是日志与源码检索的黄金组合,衔接 文本处理。ls -l/stat的权限与时间信息,是读懂 进程与系统命令 中用户态排查的基础。- 可执行位是"让脚本能直接运行"的前提——写好的脚本要先
chmod +x才能./script.sh执行。 - 需要更精细的按用户授权(ACL)、文件防篡改属性、实时监控文件变化,见 Linux 命令速查第①族。
一句话总结
文件目录操作 = 浏览(ls/tree)+ 增删(mkdir -p/touch/rm)+ 搬移(cp -a/mv/ln -sfn)+ 查找(find/locate)+ 权限(chmod/chown/umask)+ 批量(xargs);脚本里额外记住三条铁律——文件名一律走 -print0/xargs -0,删除前先 -print 演练,目录与文件权限分开设。想查全部参数见 Linux 命令速查第①族。