2

文件操作

·30 分钟

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 -rcp -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 对比

特性scprsync
增量传输❌ 每次全量✅ 只传变化部分
断点续传--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 是软复制吗?复制了一个快捷方式?

简单说:就是 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  ← 还能访问!

因为删除文件只是:

  1. 删除目录条目(文件名)
  2. link count 减 1
  3. 只有当 link count = 0 时,才真正删除 inode 和数据

软链接 vs 硬链接 对比

软链接硬链接
创建命令ln -s target linkln 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, --includeGNU 长选项
单横杠+单词-name, -exec, -typefind(老派风格)

find 是个老古董,它的多字母参数用单横杠,这是历史原因。记住这个特例就好。

大多数现代命令同时支持短选项和长选项:

grep -r ...         # 短选项
grep --recursive ...  # 长选项,效果一样

ls -a ...
ls --all ...        # 效果一样

本节小结

命令速查表

命令最常用写法用途
cpcp -a /src /dst完整备份目录
tartar -czvf xxx.tar.gz /path压缩
tartar -xzvf xxx.tar.gz解压
rsyncrsync -avz --progress /src/ user@host:/dst/远程同步
lnln -s /target /link创建软链接
findfind /path -name "*.py"按名字查找
findfind /path -size +1G按大小查找
grepgrep -rn "keyword" --include="*.py" /path/递归搜索内容

踩过的坑

  1. find 的参数是单横杠,不是双横杠(-name 不是 --name
  2. rsync 的尾部斜杠会影响复制结果(有斜杠复制内容,无斜杠复制目录本身)
  3. cp -a 不是"软复制",而是"完整复制"(保留权限、软链接等)
  4. 逗号不能连接命令,要用 ;&&
  5. find * 会被 shell 展开,应该用 find .find /path

学到的概念

  1. inode 是文件的元数据,文件名只是指向 inode 的指针
  2. 硬链接 = 多个文件名指向同一个 inode(不是副本!)
  3. 软链接 = 存储目标文件路径的特殊文件(类似快捷方式)
  4. $? 是上条命令的退出码,0 表示成功
  5. "${arr[@]}" 是遍历数组的安全写法

下一篇:1.2 文本处理(sed/awk)或 进程管理