第 2 章
文件操作
1.1 文件操作
写在前面
让我先回忆一下我用过的文件操作命令:mv, cp, mkdir, cat, nano, vim, scp, rsync, tar, unzip, split, head, tail...
简单的还好,复杂的命令我就记不住了,用的时候完全靠查 AI。但面试的时候总不能说"等我问问 Claude"吧?所以我决定系统地学一遍,用理解命令的设计逻辑来代替死记硬背。
命令分类
首先把这些命令分个类,心里有个框架:
| 类别 | 命令 | 核心动作 |
|---|---|---|
| 本地操作 | mv, cp, mkdir, cat, head, tail | 在本机搬东西、看东西 |
| 编辑器 | nano, vim | 改东西 |
| 远程传输 | scp, rsync | 跨机器搬东西 |
| 打包压缩 | tar, unzip, split | 打包/拆包 |
cp 复制文件
cp -r 与 cp -a 的区别(面试考点)
先问自己一个问题:cp -r 和 cp -a 的区别是什么?
我只知道 cp -r 是复制文件夹及其内容,cp -a 没用过。
答案:
cp -r # recursive,递归复制目录
cp -a # archive,归档模式 = -r + 保留所有属性
区别在哪? -a 会保留:
- 权限(permission)
- 所有者(owner)
- 时间戳(timestamp)
- 软链接(不会变成实际文件)
实际场景:备份服务器配置时,用 -a 才能保证复制过去的文件权限不乱。
# 备份nginx配置应该用这个
cp -a /etc/nginx /backup/nginx_20260113
💡 记忆技巧:
a = archive = 完整备份
⭐ 最常用写法
# 复制文件
cp file.txt /dst/
# 复制目录(完整备份)
cp -a /src/dir /dst/
其他参数(了解)
cp -i # interactive,覆盖前询问
cp -v # verbose,显示复制过程
cp -u # update,只复制更新的文件(源文件比目标新才复制)
cp -n # no-clobber,不覆盖已存在的文件
小测验
问:把
/etc/nginx目录完整备份到/backup/,保留所有权限和软链接,用什么命令?
我的回答:
cp -a /etc/nginx /backup/
✅ 正确!用 -a 而不是 -r,确保权限和软链接都保留。
tar 压缩解压
我之前只记得 tar -czvf 是压缩成 tar.gz,tar -xzvf 是解压,但 file.tar.gz 在前还是路径在前我都忘记了,用 exclude 也是查 AI。
参数记忆法
其实有个口诀:
tar -czvf archive.tar.gz /path/to/dir # 压缩:Create
tar -xzvf archive.tar.gz # 解压:eXtract
tar -tzvf archive.tar.gz # 查看:lisT(不解压看里面有啥)
参数含义(按顺序):
c/x/t= 动作(创建/解压/查看)z= gzip压缩v= verbose 显示过程f= file 指定文件名(f后面必须紧跟文件名!)
顺序怎么记? 动作在前,文件在后:
tar -czvf [输出文件] [要压缩的目录] # 创建:先说输出到哪
tar -xzvf [输入文件] # 解压:说从哪个文件解压
tar -xzvf [输入文件] -C [目标目录] # 解压到指定位置
⭐ 最常用写法
# 压缩目录
tar -czvf archive.tar.gz /path/to/dir
# 解压
tar -xzvf archive.tar.gz
# 解压到指定目录
tar -xzvf archive.tar.gz -C /target/dir
# 压缩时排除某些文件和文件夹
tar -czvf backup.tar.gz /path --exclude='*.log' --exclude='.git'
不同压缩格式(了解)
# .tar.gz 或 .tgz(gzip压缩,最常用)
tar -czvf archive.tar.gz /path # z = gzip
# .tar.bz2(bzip2压缩,压缩率更高但更慢)
tar -cjvf archive.tar.bz2 /path # j = bzip2
# .tar.xz(xz压缩,压缩率最高)
tar -cJvf archive.tar.xz /path # J = xz
💡 记忆:
z= gzip,j= bzip2,J= xz(压缩率递增,速度递减)
小测验
问:压缩
/home/user/project目录,但排除.git文件夹和所有.pyc文件,写出命令?
我的回答:
cd /home/user
tar -czvf project.tar.gz --exclude="*.pyc" --exclude=".git" project/
✅ 正确!
rsync 远程同步
我只记得 scp 的写法:
scp 本地路径 user@IP:/远端路径 # 推送
scp user@IP:/远端路径 本地路径 # 拉取
我知道 rsync 能断点续传,scp 不能。但 rsync 具体怎么用,参数记不住。
scp vs rsync 对比
| 特性 | scp | rsync |
|---|---|---|
| 增量传输 | ❌ 每次全量 | ✅ 只传变化部分 |
| 断点续传 | ❌ | ✅ --partial |
| 显示进度 | ❌ | ✅ --progress |
| 限速 | ❌ | ✅ --bwlimit |
| 保留属性 | 部分 | ✅ -a 全部保留 |
| 删除同步 | ❌ | ✅ --delete |
| 适用场景 | 临时传个小文件 | 大文件、批量同步、备份 |
结论:除了临时传个小文件,其他情况都用 rsync。
⭐ 最常用写法
# 同步到远程(最常用)
rsync -avz --progress /local/path/ user@remote:/remote/path/
# 从远程拉取
rsync -avz --progress user@remote:/remote/path/ /local/path/
# 大文件传输(支持断点续传 + 限速)
rsync -avz --progress --partial --bwlimit=10M /local/path/ user@remote:/path/
参数解释:
-a= archive(保留一切属性)-v= verbose-z= 压缩传输--progress= 显示进度--partial= 断点续传--bwlimit=10M= 限速 10MB/s
⚠️ 注意尾部斜杠
这个坑我踩过:
/local/path/表示复制 path 里面的内容/local/path表示复制 path 这个目录本身
图解:
源目录:
source/
├── a.txt
└── b.txt
# 有斜杠:rsync source/ dest/
dest/
├── a.txt ← 内容直接进去
└── b.txt
# 无斜杠:rsync source dest/
dest/
└── source/ ← 多套了一层!
├── a.txt
└── b.txt
其他参数(了解)
--exclude='*.log' # 排除某些文件
--delete # 删除目标目录中源目录没有的文件(镜像同步)
--dry-run 或 -n # 模拟运行,不实际传输(测试用)
--delete 的作用(危险操作!):
# 场景:源目录删了一些文件,想让目标目录也同步删除
rsync -av --delete /src/ /dst/
# ⚠️ 建议先 dry-run 看看会删什么
rsync -av --delete --dry-run /src/ /dst/
小测验
问:把课题组服务器的
/data/models/目录完整备份到另一台机器,用什么命令?
我的回答:
rsync -avz --progress /data/models/ user@remote:/data/models/
✅ 正确!
rsync 本地复制:创建代码新版本
rsync 不只是用来远程同步,本地复制也很好用!
场景:我经常需要复制一份代码创建新版本,但不想要 output 文件夹里的结果文件(太大了)。
⭐ 最常用写法
# 复制项目并重命名,排除 output 文件夹内容
rsync -av --exclude='output/*' /path/to/project/ /path/to/project_v2/
实际例子
# 我的项目结构
OSTrack/
├── lib/
├── output/ # 训练输出,几十GB
│ ├── checkpoints/
│ └── logs/
├── data/ # 软链接到数据集
└── train.py
# 创建新版本,排除 output 内容
rsync -av --exclude='output/*' ~/code/OSTrack/ ~/code/OSTrack_v2/
# 结果:output 文件夹还在,但里面是空的
OSTrack_v2/
├── lib/
├── output/ # 空的!
├── data/ # 软链接保留
└── train.py
常见变体
# 排除多个文件夹
rsync -av \
--exclude='output/*' \
--exclude='__pycache__' \
--exclude='*.pyc' \
--exclude='.git' \
~/code/project/ ~/code/project_v2/
# 完全不要 output 文件夹(连空文件夹都不创建)
rsync -av --exclude='output' ~/code/project/ ~/code/project_v2/
# 只保留代码文件(排除所有数据和输出)
rsync -av \
--exclude='output' \
--exclude='data' \
--exclude='*.pth' \
--exclude='*.ckpt' \
~/code/project/ ~/code/project_v2/
--exclude='output/*' vs --exclude='output' 的区别
--exclude='output/*' # 排除 output 里的内容,但保留空文件夹
--exclude='output' # 整个 output 文件夹都不要
图解:
原始: --exclude='output/*' --exclude='output'
project/ project_v2/ project_v2/
├── lib/ ├── lib/ ├── lib/
├── output/ ├── output/ ← 空的 │ (没有output)
│ └── model.pth └── train.py └── train.py
└── train.py
为什么用 rsync 而不是 cp?
# cp 没有 exclude 功能!
cp -a project project_v2 # 会复制所有内容,包括 output
# 要用 cp 实现同样效果,需要:
cp -a project project_v2
rm -rf project_v2/output/* # 复制完再删,麻烦!
# rsync 一条命令搞定
rsync -av --exclude='output/*' project/ project_v2/
我的数据集传输脚本
之前我用 AI 帮我写过一个批量传输数据集的脚本,现在来分析一下:
# 配置变量
TARGET_IP="10.177.19.58"
TARGET_USER="ubuntu"
TARGET_PASSWORD="5070TI@!"
TARGET_TRAINING_PATH="/data/training_dataset"
# 训练数据集列表
datasets=("coco" "GOT-10k" "LaSOT_train" "Tracking_Net")
# 批量传输
for dataset in "${datasets[@]}"; do
echo "========================================="
echo "开始传输训练数据集: $dataset ..."
echo "时间: $(date)"
sshpass -p "$TARGET_PASSWORD" rsync -a --partial --bwlimit=6M \
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
"$dataset/" \
"$TARGET_USER@$TARGET_IP:$TARGET_TRAINING_PATH/$dataset/"
if [ $? -eq 0 ]; then
echo "✅ $dataset 传输完成!"
else
echo "❌ $dataset 传输失败!"
break
fi
echo "完成时间: $(date)"
echo "========================================="
done
脚本中做得好的地方
| 技巧 | 代码 | 说明 |
|---|---|---|
| 断点续传 | --partial | 大文件传一半断了能继续 |
| 限速 | --bwlimit=6M | 不影响其他人用网络 |
| 错误处理 | $? -eq 0 + break | 失败就停,不傻跑 |
| 日志输出 | echo + date | 知道啥时候传的 |
脚本知识点解析
1. "${datasets[@]}" 为什么这样写?
datasets=("coco" "GOT-10k" "LaSOT train") # 注意第三个有空格
# ❌ 不加引号
for d in ${datasets[@]}; do echo $d; done
# 输出:coco, GOT-10k, LaSOT, train ← 空格被拆开了!
# ✅ 加引号
for d in "${datasets[@]}"; do echo "$d"; done
# 输出:coco, GOT-10k, LaSOT train ← 正确!
@ 表示数组所有元素,引号防止空格拆分。
补充:@ vs * 的区别
datasets=("a" "b c" "d")
# ${datasets[@]} 加引号 → 保持元素独立
for d in "${datasets[@]}"; do echo "[$d]"; done
# 输出:[a] [b c] [d] ← 3个元素
# ${datasets[*]} 加引号 → 合并成一个字符串
for d in "${datasets[*]}"; do echo "[$d]"; done
# 输出:[a b c d] ← 1个元素
💡 记忆:遍历数组用
"${arr[@]}",这是最安全的写法。
2. $? 是什么?
上一条命令的退出码:
0= 成功- 非0 = 失败
ls /tmp # 成功
echo $? # 输出 0
ls /不存在的目录
echo $? # 输出 2(失败)
所以 if [ $? -eq 0 ] 就是检查上条命令是否成功。
补充:其他常用的特殊变量
$? # 上条命令的退出码
$! # 上一个后台进程的 PID
$$ # 当前脚本的 PID
$0 # 脚本名称
$1 # 第一个参数
$# # 参数个数
$@ # 所有参数(作为独立字符串)
3. -e "ssh -o StrictHostKeyChecking=no" 是干嘛的?
-e 是指定用什么方式连接远程(不是环境变量!)。
StrictHostKeyChecking=no 的作用:
# 正常 SSH 第一次连接会问你:
The authenticity of host '10.177.19.58' can't be established.
Are you sure you want to continue connecting (yes/no)?
# 脚本自动化时不能手动输入 yes,所以关掉这个检查
⚠️ 生产环境别这么干,有安全风险。我的场景是内网传数据集,问题不大。
可以改进的地方
密码明文存脚本 = 安全隐患
# ❌ 现在的做法
TARGET_PASSWORD="5070TI@!" # 密码暴露了
# ✅ 更好的做法:用 SSH 密钥
ssh-keygen -t ed25519 # 生成密钥
ssh-copy-id user@remote # 复制公钥到服务器
# 之后就不需要密码了
软链接与硬链接
学到这里我有个疑问:软链接是什么?cp -a 是软复制吗?复制了一个快捷方式?
软链接(Symbolic Link)
简单说:就是 Linux 版的"快捷方式"。
# 创建软链接
ln -s /真实/文件/路径 /快捷方式/路径
# 实际例子
ln -s /usr/local/cuda-11.8 /usr/local/cuda
我在课题组服务器上见过这个:
$ ls -l /usr/local/cuda
lrwxrwxrwx 1 root root 21 Jan 13 10:00 /usr/local/cuda -> /usr/local/cuda-11.8
这样做的好处:程序都访问 /usr/local/cuda,切换 CUDA 版本只需要改软链接指向。
cp -a 和软链接的关系
cp -a 不是"软复制",它是"完整复制"。区别在于遇到软链接时的行为:
# 假设有这个结构
/data/
├── model.pt # 真实文件,1GB
└── latest -> model.pt # 软链接,指向model.pt
# cp -r 复制(不保留软链接)
cp -r /data /backup
# 结果:latest 变成了 model.pt 的副本,又占1GB!
# cp -a 复制(保留软链接)
cp -a /data /backup
# 结果:latest 还是软链接,只占几字节
一张图说清楚:
原始状态:
┌─────────────┐ ┌─────────────┐
│ latest (链接)│ ───→ │ model.pt │
│ (几字节) │ │ (1GB) │
└─────────────┘ └─────────────┘
cp -r 后(链接变实体):
┌─────────────┐ ┌─────────────┐
│ latest │ │ model.pt │
│ (1GB 副本!) │ │ (1GB) │
└─────────────┘ └─────────────┘
cp -a 后(保留链接):
┌─────────────┐ ┌─────────────┐
│ latest (链接)│ ───→ │ model.pt │
│ (几字节) │ │ (1GB) │
└─────────────┘ └─────────────┘
硬链接与 inode 详解
我又有疑问了:硬链接真的不是文件副本吗?inode 是什么?
什么是 inode?
要理解硬链接,先要理解 Linux 文件系统的结构。
类比:把文件系统想象成一个图书馆。
图书馆类比:
┌─────────────────────────────────────────────────────────┐
│ 图书馆 │
│ │
│ 书名卡片(目录条目) 实际的书(inode + 数据) │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │《Linux入门》 │ ────────→│ inode #12345 │ │
│ │ 索书号: 12345 │ │ - 作者: xxx │ │
│ └──────────────┘ │ - 页数: 500 │ │
│ │ - 位置: 3排5架2层 │ │
│ │ - 书的内容... │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────────┘
在 Linux 文件系统中:
- 文件名 = 书名卡片(目录条目),只是一个"指针"
- inode = 文件的元数据(权限、所有者、时间戳、数据块位置等)
- 数据块 = 文件的实际内容
Linux 文件系统结构:
┌─────────────────────────────────────────────────────────────┐
│ 磁盘 │
│ │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ 目录区 │ │ inode 区 │ │
│ │ │ │ │ │
│ │ 文件名 → inode号 │ │ inode #12345: │ │
│ │ ┌─────────────┐ │ │ ├─ 权限: -rw-r--r-- │ │
│ │ │ model.pt │──────→│ ├─ 所有者: user │ │
│ │ │ inode: 12345│ │ │ ├─ 大小: 1GB │ │
│ │ └─────────────┘ │ │ ├─ 时间戳: 2026-01-13 │ │
│ │ │ │ ├─ link count: 1 │ │
│ └──────────────────┘ │ └─ 数据块位置: [100,101,...] │ │
│ └───────────────┬──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ 数据区 │ │
│ │ │ │
│ │ 块100: [实际文件内容...] │ │
│ │ 块101: [实际文件内容...] │ │
│ │ ... │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
查看 inode 信息
# 查看文件的 inode 号
ls -i model.pt
# 输出:12345 model.pt
# 查看详细 inode 信息
stat model.pt
# 输出:
# File: model.pt
# Size: 1073741824 Blocks: 2097152 IO Block: 4096 regular file
# Device: 801h/2049d Inode: 12345 Links: 1
# Access: (0644/-rw-r--r--) Uid: ( 1000/user) Gid: ( 1000/user)
# Access: 2026-01-13 10:00:00
# Modify: 2026-01-13 09:00:00
# Change: 2026-01-13 09:00:00
硬链接的本质
硬链接 = 给同一个 inode 取另一个名字
# 创建硬链接
ln model.pt backup.pt
创建硬链接后:
┌─────────────────────────────────────────────────────────────┐
│ 磁盘 │
│ │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ 目录区 │ │ inode 区 │ │
│ │ │ │ │ │
│ │ ┌─────────────┐ │ │ inode #12345: │ │
│ │ │ model.pt │──────→│ ├─ 权限: -rw-r--r-- │ │
│ │ │ inode: 12345│ │ ┌─→│ ├─ 所有者: user │ │
│ │ └─────────────┘ │ │ │ ├─ 大小: 1GB │ │
│ │ ┌─────────────┐ │ │ │ ├─ link count: 2 ← 变成2了!│ │
│ │ │ backup.pt │────┘ │ └─ 数据块位置: [100,101,...] │ │
│ │ │ inode: 12345│ │ └──────────────────────────────┘ │
│ │ └─────────────┘ │ │
│ │ │ 两个文件名 → 同一个 inode │
│ │ (硬链接) │ → 同一份数据 → 只占 1GB! │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
验证:inode 号相同
# 创建测试文件
echo "hello" > original.txt
# 创建硬链接
ln original.txt hardlink.txt
# 查看 inode 号
ls -li
# 输出:
# 12345 -rw-r--r-- 2 user user 6 original.txt
# 12345 -rw-r--r-- 2 user user 6 hardlink.txt
# ↑ ↑
# inode号相同 link count = 2
删除原文件后,硬链接还能用吗?
rm original.txt
cat hardlink.txt # 输出:hello ← 还能访问!
因为删除文件只是:
- 删除目录条目(文件名)
- link count 减 1
- 只有当 link count = 0 时,才真正删除 inode 和数据
软链接 vs 硬链接 对比
| 软链接 | 硬链接 | |
|---|---|---|
| 创建命令 | ln -s target link | ln target link |
| 存储内容 | 目标文件的路径字符串 | 目标文件的 inode 号 |
| 本质 | 快捷方式 | 同一文件的另一个名字 |
| inode | 有自己的 inode | 共享 inode |
| 跨文件系统 | ✅ 可以 | ❌ 不行(inode 是文件系统内唯一) |
| 链接目录 | ✅ 可以 | ❌ 不行(防止循环) |
| 原文件删除后 | ❌ 失效(悬空链接) | ✅ 仍可访问 |
| 文件大小 | 几字节(存路径) | 和原文件一样(因为是同一个) |
软链接的 inode 结构
软链接的存储结构:
┌─────────────────────────────────────────────────────────────┐
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ latest │───→│ inode #99999 │ ← 软链接有自己的 │
│ │ (软链接) │ │ 类型: 符号链接 │ inode │
│ └─────────────┘ │ 内容: "model.pt" │ ← 存的是路径字符串│
│ └────────┬────────┘ │
│ │ 解析路径 │
│ ▼ │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ model.pt │───→│ inode #12345 │ ← 真正的文件 │
│ │ (真实文件) │ │ 内容: [1GB数据] │ │
│ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
💡 总结:
- 硬链接 = 多个文件名指向同一个 inode(同一个房间的多把钥匙)
- 软链接 = 一个文件存储另一个文件的路径(门上贴的便条:去隔壁房间)
⭐ 最常用写法
# 创建软链接(99%的场景用这个)
ln -s /真实/路径 /链接/路径
# 例:管理 CUDA 版本
ln -s /usr/local/cuda-11.8 /usr/local/cuda
# 切换版本时
rm /usr/local/cuda
ln -s /usr/local/cuda-12.0 /usr/local/cuda
小测验
问:为什么 CUDA 版本管理用软链接而不是硬链接?
答:因为 /usr/local/cuda-11.8 是一个目录,而硬链接不能链接目录(防止文件系统出现循环)。
find 查找文件
⭐ 最常用写法
# 按名字找
find /path -name "*.py"
# 找大文件
find /path -size +1G
# 找到后删除
find /path -name "*.log" -mtime +30 -delete
# 找到后执行命令
find /path -name "*.bak" -exec rm {} \;
基本用法详解
# 按名字找
find /data -name "*.pt" # 找所有 .pt 模型文件
find /data -name "*.log" -type f # 只找文件,不要目录(-type f = file)
find /data -type d -name "cache" # 只找目录(-type d = directory)
# 按时间找
find /data -mtime -7 # 最近7天修改过的(- 表示之内)
find /data -mtime +30 # 30天前的旧文件(+ 表示之前)
# 按大小找
find /data -size +1G # 大于1GB的文件
find /data -size +100M -name "*.log" # 大于100MB的日志
找到后执行操作
# 删除30天前的日志
find /var/log -name "*.log" -mtime +30 -delete
# 找到后执行命令(-exec)
find /data -name "*.bak" -exec rm {} \;
# ↑ ↑↑
# {}=找到的文件 \;=结束标记
小测验
题目1:找出
/data目录下所有超过 5GB 的文件
我的回答:
find /data -size +5G -type f
✅ 正确!
题目2:找出
/data/training_dataset下 30 天没动过的文件夹
我的回答:
find /data/training_dataset -type d --mtime +30
# ↑↑
# 错了!双横杠
❌ 错误!正确写法:
find /data/training_dataset -type d -mtime +30
# ↑
# 单横杠
规律:find 的参数都是单横杠(这是老派 Unix 风格)。
题目3:在项目代码里搜索所有用了
torch.cuda的 Python 文件
我的回答:
cd /home/user/project, find * --exec grep "torch.cuda" {} \;
❌ 有几个问题!让我一个一个分析。
问题分析
问题1:逗号不能连接命令
# ❌ 逗号不是命令分隔符
cd /home/user/project, find * ...
# ✅ 正确的命令分隔符
cd /home/user/project; find . ... # 分号:依次执行
cd /home/user/project && find . ... # &&:前一个成功才执行下一个
问题2:find * 的问题
# find * 会发生什么?
# * 会被 shell 展开成当前目录的所有文件
# 比如当前目录有 a.py b.py c/,则变成:
find a.py b.py c/ ...
# 这不是我们想要的!
# ✅ 正确写法
find . ... # 从当前目录开始找
问题3:--exec 是双横杠
# ❌ 双横杠
find . --exec grep "torch.cuda" {} \;
# ✅ 单横杠(find 是老派风格)
find . -exec grep "torch.cuda" {} \;
问题4:应该加 -name "*.py" 限定文件类型
不然会搜索所有文件,包括 .pyc、.so 等二进制文件。
问题5:grep -l 只显示文件名,更清爽
多种正确写法
# 写法1:先 cd 再 find(我原本想做的)
cd /home/user/project && find . -name "*.py" -exec grep -l "torch.cuda" {} \;
# 写法2:直接指定路径(推荐,不用 cd)
find /home/user/project -name "*.py" -exec grep -l "torch.cuda" {} \;
# 写法3:用 grep -r(最简单,不用 find)⭐ 推荐
grep -rl "torch.cuda" --include="*.py" /home/user/project/
# 写法4:用 xargs(更快,批量处理)
find /home/user/project -name "*.py" | xargs grep -l "torch.cuda"
# 写法5:subshell(如果确实需要在某目录执行)
(cd /home/user/project && grep -rl "torch.cuda" --include="*.py" .)
命令分隔符对比
| 分隔符 | 含义 | 例子 |
|---|---|---|
; | 依次执行,不管成功失败 | cd /tmp; ls |
&& | 前一个成功才执行下一个 | cd /tmp && ls |
|| | 前一个失败才执行下一个 | cd /xxx || echo "失败" |
& | 后台执行 | sleep 10 & |
| | 管道,前一个的输出作为后一个的输入 | ls | grep ".py" |
推荐用 &&:
cd /home/user/project && find . -name "*.py" ...
# 如果 cd 失败(目录不存在),find 就不会执行,更安全
grep 搜索文件内容
⭐ 最常用写法
# 搜索文件内容
grep "error" app.log
# 递归搜索目录(最常用!)
grep -rn "torch.cuda" --include="*.py" /project/
# 只显示匹配的文件名
grep -rl "TODO" --include="*.py" /project/
基本用法
grep "error" app.log # 找包含 error 的行
grep -i "error" app.log # 忽略大小写(ignore case)
grep -n "error" app.log # 显示行号(number)
grep -c "error" app.log # 只显示匹配数量(count)
grep -l "error" *.log # 只显示匹配的文件名(list)
grep -v "error" app.log # 反向匹配(不包含 error 的行)
递归搜索目录
grep -r "CUDA" /project/ # 递归搜索
grep -rn "import torch" --include="*.py" /project/ # 只搜.py文件,显示行号
上下文显示
grep -A 3 "error" app.log # After: 显示匹配行及后3行
grep -B 3 "error" app.log # Before: 显示匹配行及前3行
grep -C 3 "error" app.log # Context: 显示匹配行及前后各3行
xargs 批量处理
-exec 对每个文件都启动一次命令,慢。xargs 批量处理,快:
# -exec 方式(每个文件启动一次 grep)
find /project -name "*.py" -exec grep -l "torch" {} \;
# 相当于执行:
# grep -l "torch" file1.py
# grep -l "torch" file2.py
# grep -l "torch" file3.py
# ...(每个文件一次命令)
# xargs 方式(一次性把所有文件传给 grep)
find /project -name "*.py" | xargs grep -l "torch"
# 相当于执行:
# grep -l "torch" file1.py file2.py file3.py ...(一次命令)
# 处理带空格的文件名
find /project -name "*.py" -print0 | xargs -0 grep -l "torch"
# -print0 和 -0 用 null 字符分隔,而不是空格
小测验
问:在
/home/user/project目录下,找出所有包含TODO的 Python 文件,并显示行号
我的回答:
grep -rn "TODO" --include="*.py" /home/user/project/
✅ 正确!这是最简单直接的写法。
单横杠 vs 双横杠
学到这里我有点困惑,到底什么时候用单横杠,什么时候用双横杠?
| 风格 | 例子 | 常见于 |
|---|---|---|
| 单横杠+单字母 | -r, -n, -l | 几乎所有命令 |
| 双横杠+单词 | --recursive, --include | GNU 长选项 |
| 单横杠+单词 | -name, -exec, -type | find(老派风格) |
find 是个老古董,它的多字母参数用单横杠,这是历史原因。记住这个特例就好。
大多数现代命令同时支持短选项和长选项:
grep -r ... # 短选项
grep --recursive ... # 长选项,效果一样
ls -a ...
ls --all ... # 效果一样
本节小结
命令速查表
| 命令 | 最常用写法 | 用途 |
|---|---|---|
| cp | cp -a /src /dst | 完整备份目录 |
| tar | tar -czvf xxx.tar.gz /path | 压缩 |
| tar | tar -xzvf xxx.tar.gz | 解压 |
| rsync | rsync -avz --progress /src/ user@host:/dst/ | 远程同步 |
| ln | ln -s /target /link | 创建软链接 |
| find | find /path -name "*.py" | 按名字查找 |
| find | find /path -size +1G | 按大小查找 |
| grep | grep -rn "keyword" --include="*.py" /path/ | 递归搜索内容 |
踩过的坑
find的参数是单横杠,不是双横杠(-name不是--name)rsync的尾部斜杠会影响复制结果(有斜杠复制内容,无斜杠复制目录本身)cp -a不是"软复制",而是"完整复制"(保留权限、软链接等)- 逗号不能连接命令,要用
;或&& find *会被 shell 展开,应该用find .或find /path
学到的概念
- inode 是文件的元数据,文件名只是指向 inode 的指针
- 硬链接 = 多个文件名指向同一个 inode(不是副本!)
- 软链接 = 存储目标文件路径的特殊文件(类似快捷方式)
$?是上条命令的退出码,0 表示成功"${arr[@]}"是遍历数组的安全写法
下一篇:1.2 文本处理(sed/awk)或 进程管理