CSP-S 提高级课程中心 / 编程环境进阶 / 第1讲
CSP-S 提高级 · 编程环境进阶 · s1-01

Linux 终端 与 文件目录操作

CSP-S 赛场只有一台 Linux 机器和一只命令行。本讲把 16 个高频命令、路径规则与重定向一次讲透,让你在开考 3 分钟内把"工作台"整理妥当。

16 个必会命令 4 类路径写法 3 个赛场案例 1 套目录模板 12 道洛谷原题
本讲 · 第 1/52 讲Linux终端与文件目录操作
模块 01

情景导航

用一个真实的考场故事,说明"会用命令行"为什么是提高组选手的第一道门槛。

故事开考前三分钟

CSP-S 第二轮,选手小 C 走进 NOI Linux 机房。桌上没有熟悉的 Dev-C++,只有一块黑底白字的终端。老师宣布:"比赛目录 3 分钟后开放,请自行在自己的主目录下建立题目文件夹。"

小 C 敲下 dir —— 屏幕回了一行 command not found;再敲 cd D:\比赛 —— 还是报错。

隔壁的小 D 不慌不忙敲了四条命令:mkdir -p ~/noi2024/day1、cd ~/noi2024/day1、pwd、ls。写代码时他习惯性加了 -Wall,编译警告立刻提示"变量 ans 未初始化",他顺手补了个 0。

两小时后,小 C 的代码因为数组开小而 RE,更致命的是他中途把 code.cpp 改坏了想回退,却发现没有任何备份,只能凭记忆重打一遍。而小 D 每次改动前都执行 cp code.cpp code.bak,一条 diff 就能看清改了什么。

差距不在算法,而在"会不会和操作系统打交道"。

💡 为什么提高组必须先学命令行

其一,比赛环境就是 Linux。NOI 系列全部在 NOI Linux 下作答与评测。你在 Windows 上越顺手,到考场的"翻译"成本越高:没有图形 IDE、没有右键菜单。

其二,命令行的可重复性极强。一天要建十几个目录、编译几十次、对拍上百组数据。把这串动作写成命令或脚本,省下的时间可以全部投入到思考题目上。

其三,命令行是透明的。图形界面隐藏了文件在哪、程序收到了什么输入;./a.out < in.txt 把输入输出摊在你眼前,调试效率完全不同。

本讲目标很具体:让你能独立完成"建目录、写文件、查文件、备份、批量编译、找线索"这六件事。

①生活类比
🎙️

终端 = 对讲机点单

不说"菜单在左下角",而是直接说"来一份牛肉面"。一句指令立刻执行,没有中间环节。

→ command
🏢

目录树 = 楼层房间号

每层有房间,房间里还有小房间。从顶层 / 一路走下来,就得到唯一门牌号。

→ /opt/noi/day1
🧭

绝对路径 vs 相对路径

绝对路径像"XX市XX路3号",在哪都能找到;相对路径像"往前走50米右转",取决于你现在站哪。

→ ~ 与 ./
🃏

通配符 = 点名可用外号

"姓王的同学都过来"就是 王*。一句命令处理一批文件,不用逐个敲名字。

→ *.cpp
②核心定义

终端(Terminal / Shell):用"文字指令"操纵操作系统的程序。你敲的每行叫一条命令,解释并执行它的是 shell(Linux 上通常是 bash)。

目录(Directory):存放文件的文件夹。Linux 中一切皆文件,目录本身也是一种文件。

根目录 / 与用户主目录 ~:/ 是整个文件系统的唯一起点;~ 是你自己的家(常在 /home/用户名)。

当前工作目录:shell 此刻站在哪个目录里。pwd 查看、cd 改变;相对路径全部以它为起点。

命令通用格式:命令 [选项] [参数]。选项以 -(短)或 --(长)开头。

重定向:改变程序默认的输入输出方向。> 写入文件(覆盖)、>> 追加、< 把文件当输入、| 把上一个命令的输出交给下一个。

📌 本讲核心知识地图

板块核心内容对应大纲
终端与命令结构提示符、命令 = 命令名 + 选项 + 参数【5】文件与目录操作
路径规则绝对/相对路径,. .. ~ /【5】文件与目录操作
目录操作pwd cd ls mkdir rmdir tree【5】文件与目录操作
文件操作touch cp mv rm cat more less head tail wc【5】文件与目录操作
通配符与查找* ? []、find、grep【5】文件与目录操作
重定向与管道> >> < |【5】文件与目录操作
③核心口诀
🔑 命令结构

命令 + 选项 + 参数,三段式记心里;
选项用减号,参数是对象,空格一个不能少!

🔑 路径三板斧

看家在哪用 pwd,想搬家就 cd,看屋里有什么用 ls;
/ 开头绝对路径,./ 开头相对路径,~ 是主目录!

🔑 危险命令

删除之前先 ls 一遍,覆盖之前先 cp 一份;
rm 没有回收站,手一抖代码全没了!

模块 02

知识探究

把 16 个高频命令按"结构 / 路径 / 目录 / 文件 / 查找 / 重定向"六块拆开讲透。

1终端与命令结构 基础

打开终端会看到一行提示符(prompt),它把"我是谁、我在哪"全告诉你了:

Shell提示符的读法
noi@linux:~/noi2024/day1$
 │    │     │            └─ $ 表示普通用户(# 表示 root 管理员)
 │    │     └─ 当前目录(~ 是主目录的简写)
 │    └─ 主机名
 └─ 登录用户名

命令由三段组成,中间用空格隔开:命令名(做什么)、选项(怎么做)、参数(对谁做)。

① 命令名ls
② 选项-la
③ 参数~/noi2024
④ 回车执行
写法含义例子
cmd -a -b两个短选项分开写ls -l -a
cmd -ab短选项合并写(等价)ls -la
cmd --long长选项,语义清晰ls --all
cmd a b多个参数cp a.cpp b.cpp
赛场提示:记不住选项就敲 命令名 --help,或 man 命令名(按 q 退出)。考场上没人禁止你查手册。
2路径与目录树 高频

Linux 的目录结构是一棵以 / 为根的倒挂树,任何文件都能用一条路径唯一确定。

📁 典型的 NOI Linux 目录树(局部)
/
├── home/
│   └── noi/                ← 你的主目录,可写成 ~
│       ├── noi2024/
│       │   └── day1/
│       │       ├── a.cpp
│       │       └── a.out
│       └── .vimrc          ← 以点开头 = 隐藏文件
├── tmp/                    ← 临时目录,重启会清空
└── usr/bin/                ← 存放 ls、cp 等系统命令
🧭 绝对路径
  • 总是以 / 开头
  • 从根出发,与"我在哪"无关
  • 例:/home/noi/noi2024/day1/a.cpp
  • 优点:唯一稳定;缺点:太长
  • 脚本里推荐用它
📍 相对路径
  • 不以 / 开头
  • 以当前工作目录为起点
  • 例:day1/a.cpp、./a.out
  • 优点:简短;缺点:换目录就失效
  • 手动敲命令时更常用
符号含义记忆方式
/根目录(路径开头)或路径分隔符树的根
~当前用户主目录"我家"
.当前目录一个点 = 这里
..上一级目录(父目录)两个点 = 退一步
-上一次所在目录(仅用于 cd -)时空穿梭
🧭 路径解析演示:站在 /home/noi 时,每种写法指向谁
1
起点:pwd 显示 /home/noi,你站在主目录;目标文件是 noi2024/day1/a.cpp。
2
相对路径 noi2024/day1/a.cpp:从当前位置出发 → /home/noi/noi2024/day1/a.cpp。
3
绝对路径 /home/noi/noi2024/day1/a.cpp:从根 / 出发,无论站在哪都指向同一个文件。
4
~ 是主目录简写:~/noi2024/day1 → /home/noi/noi2024/day1,与站在哪无关。
5
先 cd noi2024/day1 后:./a.out 指向 day1 里的可执行文件;../.. 退两层回到 /home/noi。
6
cd -:跳回上一次所在目录(即进入 day1 之前的位置 /home/noi)。
📌 判断口诀:以 / 或 ~ 开头 = 不看当前位置;否则一定先问"我现在在哪"。
易混点:cd .. 退到上一层,cd . 原地不动,cd ../.. 退两层。别写成 cd...(会报找不到目录)。
3目录操作命令 核心
1
🧭
pwd

显示当前工作目录的绝对路径

2
🚪
cd 目录

切换目录;单独用 cd 回主目录

3
👀
ls

列目录,-l 详情 -a 含隐藏

4
🏗️
mkdir

建目录,-p 一次建多层

5
🧹
rmdir

删除空目录,非空会报错

6
🌳
tree

树形展示目录(未装可用 find 代替)

Shell目录操作:从零搭出比赛工作区
pwd                            # 看我在哪 → /home/noi
mkdir -p noi2024/day1/tests    # -p:父目录不存在也一起建
cd noi2024/day1                # 进入(相对路径)
pwd                            # 确认 → /home/noi/noi2024/day1
ls -lah                        # -l 详情 -a 隐藏 -h 人性化大小
cd ..                          # 回上一层
cd ~                           # 回主目录
cd -                           # 跳回上一次所在的目录
🚶 目录操作 8 步走:每一步后你站在哪
1
pwd → /home/noi:动手前先确认站位。
2
mkdir -p noi2024/day1/tests:一条命令建出三层目录,人还在 /home/noi。
3
cd noi2024/day1:走进工作区(相对路径)。
4
pwd → /home/noi/noi2024/day1:确认真的进来了。
5
ls -lah → 目录是空的,只列出 . 和 .. 两个隐藏条目。
6
cd .. → 退一层,站在 /home/noi/noi2024。
7
cd ~ → 直接回主目录 /home/noi(无论刚才在哪都有效)。
8
cd - → 跳回上一次所在目录 /home/noi/noi2024。
📌 注意第 8 步:cd - 回的是"上一次"的位置,不是"上一层" —— 两个减号与两个点别混。
命令常用选项赛场用途
ls-l 详情 / -a 隐藏 / -h 人性化 / -t 按时间 / -R 递归确认文件是否真的存在
mkdir-p 建多层 / -v 显示过程一次搭好 day1..day3 与 tests
rmdir-p 连同空的父目录一起删删除空目录(比 rm -r 安全)
4文件操作命令 核心

创建、复制、移动、删除、查看 —— 五件事覆盖赛场上 90% 的文件操作。

Shell文件操作速览(含注意点)
# ── 创建 ──────────────────────────────
touch a.cpp b.cpp          # 建空文件;已存在则只更新时间戳
echo "1 2 3" > in.txt     # 写入内容(> 覆盖,>> 追加)

# ── 复制 ──────────────────────────────
cp a.cpp a.bak             # 备份!赛场上最重要的动作
cp -r tests tests_bak      # 复制目录必须加 -r
cp -i a.cpp tmp.cpp        # -i 覆盖前询问,防手滑

# ── 移动 / 改名 ───────────────────────
mv a.cpp day2/             # 移动到 day2 目录
mv a.cpp solve_a.cpp       # 同目录内 mv = 重命名

# ── 删除(没有回收站!)───────────────
rm tmp.cpp                 # 删文件
rm -r tests_bak            # 递归删目录
rm -i *.out                # 逐个确认,安全地批量删

# ── 查看内容 ──────────────────────────
cat a.cpp                  # 一次全部输出(小文件)
more big.txt               # 分屏,空格翻页,q 退出
less big.txt               # 更强:可上下滚动、/ 搜索
head -n 5 data.txt         # 看前 5 行
tail -n 5 data.txt         # 看后 5 行
tail -f log.txt            # -f 实时跟随文件新增内容
wc -l a.cpp                # 统计行数
✅ 安全习惯:先看再删

执行 rm -r tests 前先用 ls tests 确认里面是什么;批量删除改用 rm -i。

每次大改动前 cp code.cpp code.bak:成本 0.1 秒,收益是一份代码。

❌ 危险动作:一把梭

rm -rf *、> code.cpp(清空文件)都能在 0.1 秒内毁掉两小时的成果。

考场无网、无云端、无回收站,没有任何补救手段。

📄 cat vs less

cat 刷屏式输出,适合几行的小文件、看样例。

less 分屏浏览,支持 / 搜索、g 到开头、G 到结尾,适合大日志。

📄 cp vs mv

cp 复制:原文件还在,用于备份。

mv 移动:原位置消失;同目录内使用即改名。

🛟 备份—改坏—回退:一条完整时间线
1
touch a.cpp 新建文件,写出一个能过样例的版本。
2
cp a.cpp a.bak:花 0.1 秒备份。此刻 a.bak 与 a.cpp 内容完全一致。
3
echo "1 2 3" > in.txt:顺手准备好样例输入,随时可测。
4
对 a.cpp 做一次大改动,结果编译不过 / 样例也错了 —— 新版有问题。
5
diff a.bak a.cpp:< 开头是备份独有行、> 开头是新文件独有行,一眼看清改了哪几行。
6
cp a.bak a.cpp:整体回退到安全版本,两小时成果保住了。
7
rm -i *.out:最后清理产物时加 -i 逐个确认,绝不误删源码。
📌 关键在第 2 步:备份必须发生在改代码之前。第 5 步的 diff 能让你只回退出问题的几行,而不是闭眼覆盖。

通配符(wildcard)由 shell 先展开成一批真实文件名再交给命令,所以几乎对所有命令都生效。

a.cpp
b.cpp
a.out
in1.txt
in2.txt
note.md

把这 6 个文件当作你的工作目录内容,下表用通配符来"点名"

通配符含义匹配结果
*任意长度任意字符(含 0 个)*.cpp → a.cpp b.cpp
?恰好一个任意字符in?.txt → in1.txt in2.txt
[abc]括号内任选一个字符[ab].cpp → a.cpp b.cpp
[1-9]一个范围内的字符in[1-9].txt → in1.txt in2.txt
{a,b}大括号枚举展开{a,b}.cpp → a.cpp b.cpp
Shell通配符 + find + grep 的黄金组合
# ── 通配符 ────────────────────────────
rm -i *.out *.exe              # 清理产物
for f in *.cpp; do mv "$f" "day1_$f"; done   # 批量改名

# ── find:按属性找文件 ────────────────
find . -name "*.cpp"           # 递归找所有 cpp
find . -iname "data.in"       # -iname 忽略大小写
find ~ -newer a.cpp            # 找比 a.cpp 更新的文件

# ── grep:按内容找线索 ────────────────
grep -n "for" a.cpp             # -n 显示行号(调试关键)
grep -rn "dfs" .              # -r 递归整个目录
grep -c "printf" a.cpp         # 只统计出现次数
grep -v "^//" a.cpp            # -v 反向:去掉注释行
grep -in "memory" run.log      # -i 忽略大小写找报错信息
find . -name "*.cpp" | xargs grep -n "scanf"   # 经典组合
经典组合:find . -name "*.cpp" | xargs grep -n "scanf" —— 先找出所有 .cpp,再在每个文件里搜 scanf。排查"我到底在哪个文件里写错了读入"时特别管用。
6重定向与管道:赛场的测样例神器

程序默认从键盘读、往屏幕写。重定向把这条默认通道"改道"到文件。

① 输入./a.out < in.txt
② 运行stdin → 计算 → stdout
③ 输出> out.txt
④ 对拍diff out.txt ans.txt
Shell四种重定向 + 一个管道
# 1. 输出重定向(覆盖)
./a.out < in.txt > out.txt
# 2. 追加重定向(记日志专用,不覆盖)
./a.out >> log.txt
# 3. 输入重定向:把文件当键盘
./a.out < sample.in
# 4. 管道:前一个命令的输出交给后一个命令
cat a.cpp | grep -n "max"
ls -l | wc -l            # 数当前目录有多少条目
# 5. 丢弃输出:/dev/null 是"黑洞文件"
./a.out < in.txt > /dev/null
符号名称作用赛场用途
>输出重定向stdout 写入文件,覆盖保存程序输出
>>追加重定向stdout 追加到文件末尾累积调试日志
<输入重定向stdin 改从文件读取测样例,不用手打
|管道前一命令 stdout → 后一命令 stdin过滤、统计、查找
2>错误重定向只改 stderr分离警告与结果
警告:> 是覆盖!想追加到已有结果文件必须写 >>,否则前面辛苦攒的数据会被清空。
动交互演示:一次完整的考试目录操作流程

下面用步骤播放器把"从进机房到交卷"的全过程走一遍,每一步都对应真实命令。点"自动演示"看全过程,也可以连续点单步。

▶ 考场 5 步走
1
认位置:pwd 确认自己在 /home/noi,再用 ls -la 看主目录里有没有已有文件,避免覆盖。
2
建工作区:mkdir -p noi2024/day1/tests 一次建出三层,cd noi2024/day1 进入。
3
写代码先占位并备份:touch a.cpp 建文件,之后每改一版就 cp a.cpp a.bak。
4
测样例:g++ a.cpp -O2 -Wall -o a 编译;./a < in.txt > out.txt 跑样例;diff out.txt ans.txt 比对,无输出即一致。
5
交卷前检查:ls -l 确认目录里只有题目要求的源文件;grep -n "freopen" a.cpp 检查是否还留着调试读写。
📌 这套流程的价值在于:每一步都能在 10 秒内完成,且不容易出错。练成肌肉记忆,考场上就能把时间留给算法。
案例案例1:从零搭出规范的三天赛场工作区 实战

任务:在 NOI Linux 上为一场为期三天的比赛建立工作目录,结构如下并验证结果。

Text目标目录结构
~/csp2024/
├── day1/
│   ├── a.cpp
│   └── tests/
├── day2/
└── day3/

🔍 分析过程

第 1 步 · 确定起点。先 pwd 确认位置;不在主目录就 cd ~。 ~ 永远指向自己的主目录,是最稳的写法。

第 2 步 · 一次性建目录。用 mkdir -p 建多层,避免父目录不存在时报错;加了 -p 后父目录已存在也不报错,非常适合写在脚本里反复执行。

第 3 步 · 批量建文件。用大括号展开 {a,b,c}.cpp 一次生成三个文件,比敲三次 touch 更快更稳。

第 4 步 · 验证。用 find . -print 递归列出,或 ls -R,确认结构与预期一致。

Shell案例1 完整命令序列
cd ~                                  # 回主目录
pwd                                   # 确认:/home/noi
mkdir -p csp2024/day1/tests            # 一次建三层
mkdir -p csp2024/day2/tests
mkdir -p csp2024/day3/tests
cd csp2024/day1
touch {a,b,c,d}.cpp                    # 大括号展开 = 建 4 个文件
touch in1.txt in2.txt ans1.txt ans2.txt
find . -print | sort                 # 没装 tree 时的替代方案
ls -l
终端输出
$ find . -print | sort
.
./a.cpp
./ans1.txt
./ans2.txt
./b.cpp
./c.cpp
./d.cpp
./in1.txt
./in2.txt
./tests

$ ls -l
total 0
-rw-r--r-- 1 noi noi 0 Sep 14 08:31 a.cpp
-rw-r--r-- 1 noi noi 0 Sep 14 08:31 ans1.txt
-rw-r--r-- 1 noi noi 0 Sep 14 08:31 ans2.txt
-rw-r--r-- 1 noi noi 0 Sep 14 08:31 b.cpp
-rw-r--r-- 1 noi noi 0 Sep 14 08:31 c.cpp
-rw-r--r-- 1 noi noi 0 Sep 14 08:31 d.cpp
-rw-r--r-- 1 noi noi 0 Sep 14 08:31 in1.txt
-rw-r--r-- 1 noi noi 0 Sep 14 08:31 in2.txt
drwxr-xr-x 2 noi noi 4096 Sep 14 08:31 tests

💡 为什么推荐写成脚本?

一是可重复:赛前练习、赛场开考都能用同一条命令恢复环境。二是不会漏:手工敲容易忘建 tests 目录,脚本不会。三是可迁移:把路径换成变量,稍作修改就能用在别的比赛上。

案例案例2:误删 / 改坏代码后的三种抢救方案 救援

情境:你写了 80 行的 a.cpp,在一次大改动后发现效果不如从前,想回到一小时前的版本。

🔍 三条救援路线(按可靠性排序)

方案一 · 事先备份(最可靠)。每完成一个"能过样例的版本"就 cp a.cpp a_v2.bak,想回退就 cp a_v2.bak a.cpp。代价只有 0.1 秒。

方案二 · 用 diff 看清差异再决定。diff a.bak a.cpp 逐行列出差别:< 开头是旧文件独有行,> 开头是新文件独有行。这样你能只回退出问题的那几行,而不是整体回滚。

方案三 · 从副产品里找(碰运气)。若曾用 g++ -E 生成过预处理文件,或曾把代码输出到日志(>> log.txt),还能找回一部分。但不可依赖。

为什么 rm 之后找不回来?rm 只是把文件名从目录里删掉,不提供回收站。数据块虽然可能还在磁盘上,但在考场这种无工具、无权限的环境里基本无法恢复。

Shell案例2 完整抢救流程
# ── 事前:建立版本快照习惯 ────────────
cp a.cpp a_v1.bak              # 第一个能过样例的版本
cp a.cpp a_v2.bak              # 加上优化的版本
ls -lh *.bak                   # 确认备份存在

# ── 事中:先看清差异 ──────────────────
diff a_v2.bak a.cpp
# 12c12
# <     if (x >= 0) ans += x;   ← 旧版(能过样例)
# ---
# >     if (x > 0)  ans += x;   ← 新版(漏掉了 0)

# ── 事后:整体回退 ────────────────────
cp a_v2.bak a.cpp

# ── 万一真 rm 了,先冷静查找 ──────────
find ~ -name "a*.cpp"          # 全盘找同名/相似文件
find ~ -name "*.bak"           # 看有没有历史备份
grep -rl "#include" ~/noi2024  # 找含源码特征的文件
终端输出
$ diff a_v2.bak a.cpp
12c12
<     if (x >= 0) ans += x;
---
>     if (x > 0)  ans += x;

$ cp a_v2.bak a.cpp
$ diff a_v2.bak a.cpp
(无输出 —— 两个文件已经完全一致)
🔑 备份三原则

① 每通过一次样例就备份一次;
② 文件名带版本号(a_v1.bak、a_v2.bak),别只会叫 bak;
③ 回退前先 diff,别闭眼覆盖 —— 你可能会把真正有用的修改一起丢掉。

案例案例3:用 grep 在代码里找 bug 线索 提效

情境:下面这段代码逻辑看起来没问题,但样例总是差一点。先看代码,再用 grep 一步步缩小排查范围。

C++可疑代码 sum.cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int a[MAXN];
int main() {
    int n, q; long long ans = 0;
    cin >> n >> q;
    for (int i = 1; i <= n; i++) cin >> a[i];
    while (q--) {
        int l, r; cin >> l >> r;
        int s = 0;                     // ← 嫌疑最大
        for (int i = l; i <= r; i++) s += a[i];
        ans += s;
    }
    cout << ans << endl;
    return 0;
}

🔍 代码逐段解析 · 可疑的 sum.cpp

第 1~4 行 —— 头文件、命名空间与全局数组;MAXN = 1005 只给 a[1005],而 n 可达 105,数组开小是第一处硬伤(读入就越界)。

第 6 行 —— ans 已声明为 long long,说明作者有溢出意识,却漏掉了更关键的 s。

第 9~13 行 —— 每次询问暴力累加;第 11 行 int s:单个区间和最大约 1014,int 早已溢出,这是答案错误的直接原因。

第 12 行 —— 循环逐个加 a[i],单次询问 O(n),总复杂度 O(n·q) ≈ 1010,大数据下还会 TLE。

易错点 —— ① 累加用的"局部变量"比全局答案更容易漏改类型;② 改数据范围时 MAXN 与题目上界要一一对账,两处必须同时动。

🔍 用命令缩小范围

第 1 步 · 定位类型。grep -n "int" sum.cpp 列出所有涉及类型的行,配合行号一眼看出:区间和 s 用了 int,而数据范围是 |a[i]| ≤ 109、n ≤ 105,区间和最大可达 1014,必然溢出。

第 2 步 · 找调试残留。grep -n "freopen\|printf\|cerr" sum.cpp。无输出说明干净;有输出说明调试语句还在,交卷前必须处理。

第 3 步 · 查数组边界。grep -n "MAXN" sum.cpp:数组按 a[1005] 开,而 n 上限是 105,同样越界。

第 4 步 · 验证修复。把 int s 改成 long long s、MAXN 改成 100005,重新编译运行并与答案比对。

Shell案例3 排查命令
grep -n "int" sum.cpp
# 3:const int MAXN = 1005;         ← 数组开小(下面单独查)
# 4:int a[MAXN];
# 5:int main() {
# 6:    int n, q; long long ans = 0;
# 8:    for (int i = 1; i <= n; i++) cin >> a[i];
# 10:        int l, r; cin >> l >> r;
# 11:        int s = 0;              ← 嫌疑最大!
grep -n "freopen" sum.cpp        # 无输出 = 干净
grep -n "MAXN" sum.cpp
# 3:const int MAXN = 1005;         ← 需要改成 100005
g++ sum.cpp -O2 -Wall -o sum     # 改完重新编译测试
./sum < in.txt > out.txt
diff out.txt ans.txt
C++修复后的 sum.cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;          // 开够:n 最大 10^5
long long a[MAXN];
int main() {
    ios::sync_with_stdio(0);
    int n, q; long long ans = 0;
    cin >> n >> q;
    for (int i = 1; i <= n; i++) cin >> a[i];
    while (q--) {
        int l, r; cin >> l >> r;
        long long s = 0;             // 关键修复
        for (int i = l; i <= r; i++) s += a[i];
        ans += s;
    }
    cout << ans << endl;
    return 0;
}

🔍 代码逐段解析 · 修复后的 sum.cpp

第 3~4 行 —— MAXN 提到 100005,数组 a 同步改为 long long:元素可达 109,与 105 量级的累加每一步都应在 64 位下进行。

第 7 行 —— ios::sync_with_stdio(0) 关同步加速读入,n、q 大时省时明显。

第 13 行 —— long long s:关键修复。区间和上界 1014 < 9.2×1018,安全。

第 14 行 —— 仍是逐项累加 O(n) 每次;若询问次数也大,应升级为前缀和 O(1) 查询(见实践题 2 的 a.cpp)。

易错点 —— ① 只改 s 不改 a:数据再大时中间量先溢出,两处必须一起核对;② 编译加 -Wall,溢出隐患常能提前暴露。

终端输出
$ g++ sum.cpp -O2 -Wall -o sum
$ ./sum < in.txt
(修复前)-727379968     ← 溢出成负数
(修复后)1000000000000   ← 与标准答案一致
$ diff out.txt ans.txt
(无输出 = 正确)
方法论:grep 不只是"搜索工具",它是缩小问题范围的思维工具。不确定 bug 在哪时,就找一类特征(类型、数组、循环上界、调试语句)把所有候选列出来,再肉眼比对 —— 比从头读代码快得多。
模块 03

实践应用

三道贴近真实赛场的任务题:给场景、给输入、给期望输出,附完整命令与结果。

题1赛场环境搭建(命令任务)
基础命令实践

给定题号清单,按要求搭出目录结构并生成文件

题目背景:NOI 系列要求选手在自己的主目录下按题号组织文件。现在给你一份题号清单,请一次性完成环境搭建。

输入格式:主目录 ~ 下新建目录 contest;其中按题号建立 prob1/prob2/prob3 三个子目录,每个子目录里都要有一个空的 main.cpp 与一个空的 tests 目录。

输出格式:执行 find ~/contest 后能得到 10 行路径清单。输入样例即上述任务清单。

输出样例:

期望输出
/home/noi/contest
/home/noi/contest/prob1
/home/noi/contest/prob1/main.cpp
/home/noi/contest/prob1/tests
/home/noi/contest/prob2
/home/noi/contest/prob2/main.cpp
/home/noi/contest/prob2/tests
/home/noi/contest/prob3
/home/noi/contest/prob3/main.cpp
/home/noi/contest/prob3/tests
(find 输出恰好 10 行)

数据范围:题号数量 1 ≤ k ≤ 10;单条命令执行时间 < 1 秒,磁盘占用 < 1MB(常规约定 1s / 256MB)。

🔍 题目分析:两种命令风格对比

方法一 · 大括号展开mkdir -p contest/prob{1..3}/tests 一条命令建完所有目录,再 touch contest/prob{1..3}/main.cpp 建完文件。优点是极短;缺点是题号必须是连续整数。

方法二 · 循环脚本for i in 1 2 3; do ... done 逐个处理。优点是可处理任意题号(A/B/C 或 1001/1002),还能顺便写入代码模板;缺点是稍长。

选择建议:题号连续且规律 → 方法一;题号不规则或需要写模板 → 方法二。两者都值得掌握。

复杂度直觉:设题目数为 k,方法一是 O(1) 条命令,方法是 O(k) 次循环;k ≤ 10,二者耗时都在毫秒级 —— 真正省下的是你敲键盘的时间。

Shell方法一:大括号展开
cd ~
mkdir -p contest/prob{1..3}/tests    # 一次建出 contest、prob1~3 与各自 tests 共 7 个目录
touch contest/prob{1..3}/main.cpp     # 一次建出 3 个文件
find ~/contest
Shell方法二:循环脚本(题号不规则时)
#!/bin/bash
# 用法:bash setup.sh 1001 1002 1003
cd ~
mkdir -p contest
for t in "$@"; do
    mkdir -p "contest/prob$t/tests"
    touch    "contest/prob$t/main.cpp"
    echo "已建立 prob$t"
done
find ~/contest | sort
运行结果
$ bash setup.sh 1001 1002 1003
已建立 prob1001
已建立 prob1002
已建立 prob1003
/home/noi/contest
/home/noi/contest/prob1001
/home/noi/contest/prob1001/main.cpp
/home/noi/contest/prob1001/tests
(其余两项结构相同,共 10 行)

💡 想一想

如果把"题号清单"换成一个文件(每行一个题号),命令该怎么写?提示:while read t; do ... done < list.txt。这条思路在批量整理数据、批量评测时非常有用。

题2自动编译脚本与样例自动评测
提高脚本编程

编写脚本:编译 + 跑样例 + 自动比对,输出评测结论

题目背景:竞赛中每道题都要反复"改代码 → 编译 → 跑样例 → 对比答案",把这一串动作交给脚本,每次只需一行命令。

输入格式:脚本接受两个参数:源文件名 $1 与题目名 $2;目录中需存在 ${2}.in(输入样例)与 ${2}.ans(标准答案)。

输出格式:先输出编译结论(成功 / 失败),再输出每个样例的结论:AC(一致)或 WA(不一致,附差异前几行),最后给出通过数。

输入样例:

样例文件内容
$ cat a.in
5 2
1 2 3 4 5
1 3
2 5
$ cat a.ans
6
14
$ ls
a.cpp  a.in  a.ans

输出样例:

期望输出
编译成功:a.cpp -> a
样例1:AC
通过 1 / 1 组

数据范围:样例组数 1 ≤ t ≤ 20,每组输入规模 ≤ 106 个整数;编译 + 运行总耗时 < 5 秒。

🔍 题目分析:两种实现思路

方法一 · 单样例脚本固定处理一组输入输出:编译、运行、比对。理解成本最低,适合赛场上"临时拿来用"。

方法二 · 多组循环脚本用循环遍历 1..t 号样例,逐个比对并累计通过数。结构稍复杂,但一次写好一劳永逸。

关键技巧:① 用 || exit 1 让编译失败时立即停下,避免拿着过期产物瞎比对;② 用 diff -q 只看"是否相同"不刷屏;③ 用 diff -b 忽略行尾空白的差异(评测机通常也忽略)。

Shell方法二:多组样例评测 run_all.sh
#!/bin/bash
# 用法:bash run_all.sh a.cpp a 3   (3 = 样例组数)
SRC="$1"; NAME="$2"; T="${3:-1}"

g++ "$SRC" -O2 -Wall -o "$NAME" || { echo "编译失败!"; exit 1; }
echo "编译成功:$SRC -> $NAME"

PASS=0
for i in $(seq 1 "$T"); do
    ./"$NAME" < "$NAME$i.in" > "$NAME$i.out"
    if diff -b -q "$NAME$i.out" "$NAME$i.ans" > /dev/null; then
        echo "样例$i:AC"; PASS=$((PASS+1))
    else
        echo "样例$i:WA"
        diff -b "$NAME$i.out" "$NAME$i.ans" | head -n 10
    fi
done
echo "通过 $PASS / $T 组"

🔍 代码逐段解析 · run_all.sh

第 3 行 —— SRC="$1"、NAME="$2" 取位置参数,${3:-1} 表示第 3 个参数缺省为 1 组样例。

第 5 行 —— 编译;|| 在退出码非 0(编译失败)时触发花括号块,先提示再 exit 1,绝不拿上一次的旧可执行文件去比对。

第 8 行 —— seq 1 "$T" 生成 1..T 的样例编号,$( ) 命令替换喂给 for。

第 9~10 行 —— < "$NAME$i.in" 喂输入、> "$NAME$i.out" 收输出;文件名按 a1.in / a1.out / a1.ans 规则拼接。

第 11~16 行 —— diff -b -q 静默比对(-b 忽略行尾空白、-q 只说是否相同),相同则 PASS=$((PASS+1)) 计数,不同则打印前 10 行差异。

易错点 —— ① 去掉 || exit 1:编译错误后循环跑的是旧产物,结论全部失真;② 变量不加双引号,文件名含空格时会拆错参数。

配套的被测程序(用于验证脚本),实现"区间和查询":

C++被测程序 a.cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
long long s[MAXN];
int main() {
    ios::sync_with_stdio(0);
    int n, q; cin >> n >> q;
    for (int i = 1; i <= n; i++) {
        long long x; cin >> x;
        s[i] = s[i - 1] + x;               // 构建前缀和
    }
    while (q--) {
        int l, r; cin >> l >> r;
        cout << s[r] - s[l - 1] << "\n";  // O(1) 查询
    }
    return 0;
}
🔍 代码逐段解析 · 前缀和被测程序 a.cpp
1
第 1~4 行:const int MAXN = 100005 按数据上界开数组;long long s[MAXN] 存前缀和 —— 105 个 109 级别的数累加会超 int,必须 64 位。
2
第 7~10 行(预处理):s[i] = s[i-1] + x 一边读一边累加,一次遍历建好前缀和,复杂度 O(n)。
3
第 8 行(哨兵):全局数组 s[0] 自动为 0,它是 l=1 时的"安全垫",保证 s[l-1] 永远合法。
4
第 11~14 行(查询):区间 [l,r] 的和 = s[r] - s[l-1],两次数组访问完成一次询问,O(1)。
5
用样例验证:a = 1 2 3 4 5,s = 0 1 3 6 10 15;查询 [1,3] → s[3]-s[0] = 6,查询 [2,5] → s[5]-s[1] = 14,与 a.ans 的 6、14 一致。
6
易错点:① 写成 s[r] - s[l] 会把第 l 个元素漏掉(本讲"WA 演示"就是这么造出来的:[1,3] 变 5、[2,5] 变 12);② 数组必须开到 n+1 以上,MAXN 小于 n 上限会越界 RE。
📌 前缀和是"空间换时间"的最小例子:多花一个数组,换来每次询问 O(1)。
运行结果
$ bash run_all.sh a.cpp a 1
编译成功:a.cpp -> a
样例1:AC
通过 1 / 1 组

$ # 故意把 s[l-1] 写成 s[l],再跑一次
$ bash run_all.sh a.cpp a 1
编译成功:a.cpp -> a
样例1:WA
1c1
< 5
---
> 6
2c2
< 12
---
> 14
通过 0 / 1 组

💡 从"跑样例"升级到"对拍"

样例通过不代表程序正确。进一步的做法是对拍:写一个"一定正确但很慢"的暴力程序,再写一个随机数据生成器,循环执行"生成 → 各自运行 → 比较"。一旦出现差异,你手上立刻有一组"能复现 bug 的数据",比盲猜高效得多。

题3用 grep 与 find 追查"文件类"错因
提高综合任务

程序本地正常、交上去却报错:用命令行定位三类文件相关问题

题目背景:本地测样例一切正常,交到评测机却得到 RE 或 WA。经验表明其中一大类原因是文件路径与文件操作问题。请设计一套命令排查流程。

输入格式:一个源码文件 main.cpp 与若干数据文件;命令行可访问整个工作目录。

输出格式:输出"排查报告",逐项给出所在行号与结论(正常 / 可疑)。

输入样例:目录中有 main.cpp、data.in、data.out、可执行文件 main;源码第 8 行写有 freopen("data.in","r",stdin);。

输出样例:

期望输出
[1] 文件读写语句:main.cpp:8  freopen("data.in", "r", stdin);
[2] 引用的输入文件是否存在:yes(data.in 44 字节)
[3] 目录中共有源文件 1 个,产物 1 个
[4] 结论:本地文件齐全,但评测机上无 data.in → 必须删掉或注释 freopen

数据范围:文件数 ≤ 103;单条命令执行时间 < 1 秒(常规约定 1s / 256MB)。

🔍 题目分析:三类"文件型"错因与对应命令

错因一 · 调试用的 freopen 忘了删本地有文件所以能跑,评测机上没有该文件,直接 RE。排查:grep -n "freopen" main.cpp。这是提高组最常见的低级失误之一。

错因二 · 大小写或扩展名写错Linux 区分大小写,Data.in 与 data.in 是两个文件。排查:ls -l 看真实文件名,find . -iname "data.in" 忽略大小写找"名字相似"的文件。

错因三 · 输出格式不符行末多一个空格、末尾少一个换行都会 WA。排查:cat -A out.txt 把行尾 $、制表符 ^I 都显示出来。

复杂度直觉:grep 在单文件上是 O(文件长度),grep -r 是 O(所有文件总长度)。哪怕代码与数据几十 MB,也在 1 秒内完成。

Shellcheck.sh —— 交卷前的文件类自检
#!/bin/bash
FILE="${1:-main.cpp}"

echo "[1] 检查调试用的文件读写语句"
grep -n -E "freopen|fopen|ofstream|ifstream" "$FILE" \
    || echo "    未发现文件读写语句(正常)"

echo "[2] 检查源码引用到的输入文件是否存在"
grep -o -E '"[A-Za-z0-9_./-]+\.(in|txt)"' "$FILE" | tr -d '"' | while read f; do
    [ -f "$f" ] && echo "    $f 存在" || echo "    $f 不存在 ← 评测机上会 RE!"
done

echo "[3] 统计目录中的源文件与产物"
echo "    源文件:$(find . -name "*.cpp" | wc -l) 个"
echo "    可执行:$(find . -type f -executable | wc -l) 个"

echo "[4] 检查输出文件的行尾(隐藏字符)"
[ -f out.txt ] && cat -A out.txt | head -n 3 || echo "    暂无 out.txt"

🔍 代码逐段解析 · check.sh

第 2 行 —— FILE="${1:-main.cpp}":默认检查 main.cpp,也可传参指定别的源文件。

第 4~6 行(检查 1) —— grep -n -E 用扩展正则一次匹配 freopen/fopen/ofstream/ifstream 四类文件读写语句,-n 带行号;|| 在无匹配时输出"正常"。

第 9~11 行(检查 2) —— grep -o 只输出匹配片段(引号内的文件名),tr -d '"' 去引号,while read f 逐行用 [ -f "$f" ] 判断文件是否真的存在。

第 13~15 行(检查 3) —— $( ) 命令替换内嵌 find | wc -l,分别统计 .cpp 源文件与可执行文件个数。

第 17~18 行(检查 4) —— cat -A 把行尾显示为 $、制表符显示为 ^I,专抓肉眼看不见的隐藏字符。

易错点 —— ① 正则里的引号要放进 "..." 并注意转义层级,漏写会导致匹配不到;② [ -f ] 只认普通文件,目录会判否(目录要用 -d)。

运行结果
$ bash check.sh main.cpp
[1] 检查调试用的文件读写语句
8:    freopen("data.in", "r", stdin);
[2] 检查源码引用到的输入文件是否存在
    data.in 存在
[3] 统计目录中的源文件与产物
    源文件:1 个
    可执行:1 个
[4] 检查输出文件的行尾(隐藏字符)
5 2$
1 2 3 4 5$
1 3$

# 结论:本地一切正常,但评测机上没有 data.in
#       交卷前必须把第 8 行的 freopen 注释掉或删掉!
考场提醒:若比赛确实要求文件输入输出,请使用官方指定的文件名与相对路径,不要写死绝对路径(如 /home/noi/data.in),否则评测机上必然找不到文件。

💡 交卷前 30 秒检查清单

① grep -n "freopen" 源文件 —— 有没有调试残留;
② ls -l —— 源文件名与题目要求是否完全一致(含大小写与扩展名);
③ ./程序 < 样例 > out && diff out 答案 —— 样例是否仍然通过(防止最后时刻改崩)。

模块 04

总结提升

①核心要点与万能模板
知识板块必须记住的核心赛场用途
命令结构命令名 + 选项 + 参数,空格分隔看懂报错、查 --help
路径/ 绝对、./ 相对、~ 主目录、.. 上一层防止"文件找不到"
目录pwd / cd / ls -lah / mkdir -p / rmdir开考搭工作区
文件touch / cp -r / mv / rm -i / cat / less / head / tail备份与查看
查找* ? [] 通配符;find 按名字;grep -n 按内容批量处理、找 bug
重定向< 输入、> 覆盖、>> 追加、| 管道测样例、对拍
安全原则先 ls 再 rm;先 cp 再改;用 rm -i保护两小时成果

万能模板:开考 60 秒环境初始化脚本(保存为 init.sh,每场比赛第一件事就是跑它)

Shellinit.sh —— 考场环境一键初始化
#!/bin/bash
# 用法:bash init.sh csp2024 1 2 3 4   (比赛名 + 题号列表)
RACE="${1:-race}"; shift
cd ~ || exit 1
mkdir -p "$RACE"
for t in "$@"; do
    D="$RACE/$t"
    mkdir -p "$D/tests"
    cat > "$D/main.cpp" << 'EOF'
#include <bits/stdc++.h>
using namespace std;
int main() {
    ios::sync_with_stdio(0);
    return 0;
}
EOF
done
echo "工作区已就绪:"; pwd; find "$RACE" | sort

🔍 代码逐段解析 · init.sh(考场万能模板)

第 3 行 —— RACE="${1:-race}" 取第一个参数为比赛名(缺省 race);shift 把参数队列左移一位,之后 $@ 只剩题号列表。

第 4 行 —— cd ~ || exit 1:主目录进不去就直接放弃,避免把文件建到错误位置。

第 6~8 行 —— 对每个题号 t 建出 "$RACE/$t/tests"(-p 连同父目录一起建)。

第 9~15 行 —— cat > main.cpp << 'EOF' 是 heredoc 写模板:引号包住 'EOF' 表示不展开模板里的任何字符,原样写入文件。

第 17 行 —— 最后 find | sort 打印整个结构清单,肉眼核对有没有漏建。

易错点 —— ① 结束标记 EOF 必须顶格写,前面有空格就不会结束 heredoc;② 不加 'EOF' 的引号时,模板里的 $ 会被 shell 提前展开成空串,生成的代码是坏的。

运行结果
$ bash init.sh csp2024 1 2
工作区已就绪:
/home/noi
csp2024
csp2024/1
csp2024/1/main.cpp
csp2024/1/tests
csp2024/2
csp2024/2/main.cpp
csp2024/2/tests
②拓展案例

拓展 1:用 find + xargs 批量统计代码规模(两种实现)

思路:统计整个比赛目录下所有 .cpp 的行数总和(赛后复盘很实用)。

方案一 · xargs 一次性传参find . -name "*.cpp" | xargs wc -l,wc 会自动输出每个文件的行数并给出总计行。

方案二 · -exec 逐文件处理find . -name "*.cpp" -exec wc -l {} \;,更灵活但不会自动汇总。

关键区别:xargs 把文件名拼成参数列表一次传给 wc,效率高且有总计;-exec 对每个文件各调用一次,能配复杂逻辑但更慢。

Shell统计代码规模
find . -name "*.cpp" | xargs wc -l
#   62 ./csp2024/1/main.cpp
#   48 ./csp2024/2/main.cpp
#  110 total

# 只想要一个总数
find . -name "*.cpp" -exec cat {} + | wc -l

# 进阶:统计"有效代码行"(去掉空行与纯注释行)
find . -name "*.cpp" -exec cat {} + | grep -v '^[[:space:]]*$' | grep -v '^[[:space:]]*//' | wc -l

拓展 2:搭建"对拍"工作流(正解 + 暴力 + 生成器)

思路:准备三个程序:brute.cpp(暴力,慢但一定对)、solve.cpp(你的正解)、gen.cpp(随机数据生成器),循环执行"生成 → 各自运行 → 比较"。

为什么必须用重定向?要跑成百上千轮,手输数据不现实;< data.in 与 > out.txt 让全过程自动化,且不污染屏幕。

注意对拍只能证明"在这批随机数据上一致",不能证明算法正确 —— 但它能在几秒内抓出绝大多数低级错误。

Shellduipai.sh —— 对拍脚本
#!/bin/bash
# 用法:bash duipai.sh 1000
MAX="${1:-100}"
g++ brute.cpp -O2 -o brute
g++ solve.cpp -O2 -o solve
g++ gen.cpp   -O2 -o gen

for ((i = 1; i <= MAX; i++)); do
    ./gen > data.in                    # 生成随机数据
    ./brute < data.in > brute.out      # 暴力跑
    ./solve < data.in > solve.out      # 正解跑
    if ! diff -b -q brute.out solve.out > /dev/null; then
        echo "第 $i 轮发现差异!数据已保存在 data.in"
        diff -b brute.out solve.out | head -n 10
        exit 1                       # 保留现场,便于复现
    fi
done
echo "$MAX 轮全部一致,程序大概率没问题"

🔍 代码逐段解析 · duipai.sh(对拍模板)

第 3~6 行 —— MAX="${1:-100}" 缺省对拍 100 轮;随后一次性把暴力、正解、生成器全部编译好,避免循环里反复编译浪费时间。

第 8 行 —— for ((i=1; i<=MAX; i++)) 是 bash 的 C 风格循环,控制轮数。

第 9~11 行 —— ./gen > data.in 生成随机数据;同一份 data.in 分别重定向喂给暴力与正解,输出各存一份 —— 这就是重定向的价值:全自动、不污染屏幕。

第 12~17 行 —— diff -b -q 静默比对,! 取反:不一致时打印前 10 行差异并 exit 1,data.in 就是能复现 bug 的数据。

易错点 —— ① 发现差异后必须立即退出保留现场,若继续循环,./gen > data.in 会把复现数据覆盖掉;② 生成器的数据范围要贴合题目真实约束,太小的随机数测不出溢出类错误。

运行结果
$ bash duipai.sh 5
- 第 3 轮发现差异!数据已保存在 data.in
1c1
< 24
---
> 23
$ cat data.in
5 3
-2 7 -1 4 9
1 3
2 5
4 5

拓展 3:用 grep 从编译日志里分类提取警告

思路:用 -Wall -Wextra 编译大项目时屏幕会刷出大量警告。先把日志存下来,再用 grep 分类统计,最后按类型逐个击破。

技巧grep -c "warning" 数条数;grep -n "error" 定位错误行;grep -v "note" 过滤掉编译器附带的提示行,输出会干净很多。

Shell日志分析三连
# 1. 把编译信息存进日志(2> 抓 stderr,编译警告走 stderr)
g++ main.cpp -O2 -Wall -Wextra -o main 2> build.log

# 2. 分类统计
grep -c "error"   build.log        # 错误条数
grep -c "warning" build.log        # 警告条数

# 3. 只看错误所在行(去掉 note 提示)
grep -n "error" build.log | grep -v "note"

# 4. 只看"未初始化"这类高危警告
grep -n "uninitialized" build.log
③思考与启发

💡 思考 1:为什么赛场不提供图形界面,而坚持用终端?

有同学觉得命令行"反人类"。请从可重复性、可脚本化、远程操作、稳定性四个角度想一想:图形界面在什么场景下会拖后腿?

查看参考思路

① 可重复性:命令是一串明确字符,可以精确记录、复制、写进脚本,每次都得到同样结果;"点菜单"很难记录成可复现的步骤。

② 可脚本化:几百次编译、上百组对拍、批量生成数据只有命令能自动化,图形界面只能一次次手点。

③ 远程与低资源:评测机、服务器通常没有显示器,甚至跑在容器里,SSH 一条命令就能操作。

④ 稳定性:图形界面依赖桌面包与显卡驱动,终端几乎没有依赖,内存占用也小得多。比赛期间最怕环境崩溃。

结论:命令行把操作抽象成接口,会用它的人能把自己的工作流固化下来,越用越顺。

💡 思考 2:rm 没有回收站,这是设计缺陷吗?

从"命令行哲学"角度想想:为什么 Unix 把 rm 设计得这么"危险"?需要更安全的删除体验时,社区是怎么解决的?

查看参考思路

设计哲学:Unix 奉行"一个工具只做一件事并做到极致"。rm 的职责是"解除名字与数据的链接",加回收站会让它承担垃圾管理的额外职责,还会引入"回收站满了怎么办""跨设备怎么办"等不确定性。

安全保障由上层提供:① 用 rm -i 逐个确认;② 自己写一个 trash 函数(先 mv 到 ~/.trash);③ 用版本控制(如 git)管理代码,误删也能恢复。

竞赛启示:工具越锋利,越需要使用者有纪律。把纪律固化成习惯(先备份、先 ls、用 -i),比抱怨工具更有效。

💡 思考 3:能不能设计一条"从代码到评测结论"的完整流水线?

结合本讲的 mkdir、cp、grep、重定向、diff 与循环,描述一个"一键评测所有题目"的脚本应包含哪些环节。

查看参考思路

环节一 · 环境准备:用 mkdir -p 保证工作目录与输出目录存在。

环节二 · 遍历题目:for 循环遍历题号列表,逐个处理。

环节三 · 编译:g++ -O2 -Wall -o,把 stderr 重定向到日志;编译失败就记录并跳过。

环节四 · 运行与比对:./程序 < in > out 加 diff -b -q;同时用 time 记录耗时,超时即标记"可能 TLE"。

环节五 · 汇总报告:把每题结论(AC / WA / TLE / 编译错误)追加到一个结果文件,最后 cat 出一览。

进阶:真实比赛中这个思路由 OJ 系统实现。理解它之后,你就明白为什么"标准输入输出 + 重定向"是竞赛的核心约定。

模块 05

学习检测

先找错,再背口诀 —— 把易错点消灭在考试之前。

易错1

把 Windows 命令当 Linux 命令用

❌ 错误
在 NOI Linux 里敲 dir、copy、cls、cd D:\比赛,得到 command not found。
✅ 正确
Linux 对应命令是 ls、cp、clear、cd /home/noi/比赛。命令名区分大小写,ls 不能写成 LS。
口诀:dir 改 ls,copy 改 cp,cls 改 clear —— Windows 那套在 Linux 里全部换掉!
易错2

相对路径用错,文件"明明在却找不到"

❌ 错误
已站在 ~/csp/day1 里,却敲 cp day1/a.cpp .,路径变成 day1/day1/a.cpp,报 No such file。
✅ 正确
先 pwd 看清自己站在哪,再用正确的相对路径(cp a.cpp .);不确定时优先写绝对路径。
口诀:动手之前先 pwd,相对路径靠站位;拿不准就写绝对路径!
易错3

复制目录忘记 -r

❌ 错误
cp tests tests_bak → omitting directory 'tests',目录根本没被复制。
✅ 正确
cp -r tests tests_bak。-r(递归)让 cp 把目录连同内容一起复制。
口诀:目录复制加 -r,不加就只报个错,什么也没复制!
易错4

重定向写错:想追加却用了覆盖

❌ 错误
每轮调试信息都写进 log.txt,却用了 ./prog > log.txt,每次都把上次内容清空,只剩最后一轮。
✅ 正确
追加用 >>:./prog >> log.txt。测样例输出才用覆盖 >(每次都想要最新结果)。
口诀:一个箭头是覆盖,两个箭头才追加;测样例用覆盖,记日志用追加!
易错5

危险删除:rm -rf 没确认就回车

❌ 错误
想删测试目录却敲成 rm -rf *,把整个工作区(含源代码)全部删除,无法恢复。
✅ 正确
① 先 ls 确认目标;② 用 rm -ri 目录名 逐个确认;③ 写全路径,如 rm -ri ~/csp/day1/tests;④ 绝对不用 rm -rf /。
口诀:rm 之前先 ls,用 f 还是 i 要分清;路径写全不写星,代码在手心不惊!
📋速查表:16 个必会命令
命令作用关键选项赛场场景
pwd显示当前目录绝对路径—动手前确认位置
cd切换目录可用 ~ .. -进入题目目录
ls列出目录内容-l -a -h -t -R确认文件是否存在
mkdir创建目录-p(建多层)开考搭工作区
rmdir删除空目录-p清理空目录
touch创建空文件 / 更新时间—先建出源文件
cp复制文件或目录-r(目录) -i(询问)备份代码
mv移动 / 重命名-i -f改名、归档
rm删除(无回收站)-r -i -f清理产物(慎用)
cat一次输出全部内容-n 行号看小样例
more分屏查看空格翻页,q 退出看中等文件
less分屏查看(更强)/ 搜索,g/G 跳首尾看大日志
head查看开头若干行-n 行数看数据格式
tail查看末尾若干行-n 行数 / -f 跟随看运行日志末尾
find按属性递归查找文件-name -type -newer -iname找回文件、批量处理
grep按内容查找文本-n -r -i -v -c找 bug 线索
📌知识点笔记卡
📖 必记概念
  • 命令 = 命令名 + 选项 + 参数
  • / 根目录,~ 主目录
  • . 当前目录,.. 上级目录
  • 绝对路径以 / 开头,与位置无关
  • Linux 区分大小写
  • > 覆盖、>> 追加、< 输入、| 管道
🧰 必背模板
  • 建目录:mkdir -p a/b/c
  • 备份:cp code.cpp code.bak
  • 编译:g++ a.cpp -O2 -Wall -o a
  • 跑样例:./a < in.txt > out.txt
  • 比对:diff -b out.txt ans.txt
  • 找残留:grep -n "freopen" a.cpp
🚨 考场提醒
  • 开考先 pwd 再 mkdir -p
  • 大改动前必须备份
  • 不用 rm -rf *
  • 交卷前删掉 freopen
  • 源文件名与题目要求完全一致
  • 删掉多余的可执行文件与测试数据
课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增。C组后附 12 道洛谷原题。

🟢 A组 · 基础(4题)

A1命令填空

写出命令:① 显示当前目录 ② 建三层目录 a/b/c ③ 复制目录 tests 为 tests2 ④ 查看文件最后 5 行

查看参考答案

思路:先想清楚"操作对象是文件还是目录、是查看还是修改"。

① pwd;② mkdir -p a/b/c;③ cp -r tests tests2;④ tail -n 5 文件名。

考点:-p(建多层)、-r(递归)、-n(行数)三个选项。

易错:② 漏掉 -p 会报 No such file or directory;③ 漏掉 -r 会报 omitting directory 且目录不会被复制。

A2路径判断

当前在 /home/noi/a/b,下列命令会到哪个目录:① cd .. ② cd ~ ③ cd ../c ④ cd /home

查看参考答案

思路:相对路径从当前目录出发,绝对路径从 / 出发。

① /home/noi/a(退一层);② /home/noi(主目录);③ /home/noi/a/c(先退到 a 再进 c);④ /home(绝对路径,与当前位置无关)。

易错:把 cd .. 误当成"两步";或写下 cd ...(三个点),shell 会去找名为 ... 的目录并报错。

A3通配符

目录下有 a.cpp b.cpp main.out test1.in test2.in note.md,写出匹配结果:① *.cpp ② test?.in ③ [ab].* ④ *.{cpp,md}

查看参考答案

思路:* 匹配任意长度(含 0 个字符),? 匹配恰好一个字符,[] 匹配括号内任一字符。

① a.cpp b.cpp;② test1.in test2.in;③ a.cpp b.cpp;④ a.cpp b.cpp note.md。

易错:把 * 理解成"至少一个字符";或以为通配符能匹配隐藏文件 —— 它其实不匹配以 . 开头的文件。

A4重定向

写出命令:① 用 in.txt 作为输入、把输出存到 out.txt ② 把结果追加到 log.txt ③ 只统计 a.cpp 里 for 出现的次数

查看参考答案

① ./a.out < in.txt > out.txt;② ./a.out >> log.txt;③ grep -c "for" a.cpp。

考点:输入重定向 <、输出重定向 > 与追加重定向 >> 的区别,以及 grep -c 只输出计数。

易错:把 ② 写成 >,会把之前累积的日志全部清空;③ 忘了 -c 会把每一行都打印出来。

🟡 B组 · 提高(4题)

B1脚本编写

写一个脚本,接收比赛名与若干题号,为主目录下的每道题建立"目录 + 源文件 + tests"结构。

查看参考答案

思路:$1 是比赛名,shift 之后 $@ 就是题号列表;循环处理每个题号。

#!/bin/bash
RACE="${1:-race}"; shift
cd ~ || exit 1
for t in "$@"; do
    mkdir -p "$RACE/$t/tests"
    touch "$RACE/$t/main.cpp"
    echo "已建立 $RACE/$t"
done
find "$RACE" | sort

考点:位置参数 $1/$@、shift、for 循环、变量加引号。

易错:不写 shift 会把比赛名也当成题号;变量不加双引号时,路径含空格会出错。

B2查找技巧

如何在整个 ~/noi 目录里找出所有含有"freopen"的 .cpp 文件,并显示行号?

查看参考答案

思路:先按名字筛出 .cpp,再按内容筛出含关键字的行。

grep -rn --include="*.cpp" "freopen" ~/noi,或写成 find ~/noi -name "*.cpp" | xargs grep -n "freopen"。

考点:grep -r 递归、--include 限定文件类型、-n 显示行号、管道配合 xargs。

易错:只写 grep -n 而不加 -r,只会在当前目录的文件里找;文件名含空格时 xargs 会拆错,需配合 -print0 | xargs -0。

B3安全操作

你想删除 ~/csp/day1/tests 里所有 .out 文件,又不希望误删源码。请写出安全做法。

查看参考答案

思路:先"看"再"删",并且把范围限定到具体目录、具体后缀。

① 先看:ls -l ~/csp/day1/tests/*.out;② 再删:rm -i ~/csp/day1/tests/*.out。

考点:通配符与相对/绝对路径的结合、-i 交互确认。

易错:在当前目录直接敲 rm -rf *(可能包含源码);或用 rm -rf *.out 而不加 -i,一旦通配符匹配范围超出预期就无从挽回。

B4错误诊断

同学执行 cp ~/a.cpp day1/ 报错 No such file or directory。列出至少三种可能原因。

查看参考答案

思路:报错信息里"不存在"的可能是源、也可能是目标目录,要分别排查。

① 源文件不存在:ls ~/a.cpp 确认;② 目标目录 day1 不在当前目录下 —— 用 pwd 与 ls 确认,可能需要写 ~/day1/;③ 目标路径里含不存在的中间目录(如 day1/tests/ 但 day1 还没建)。

考点:相对路径的起点是当前工作目录;cp 不会自动创建目标目录。

易错:想当然以为 cp 会自动建目录(它不会,必须先用 mkdir -p)。

🔴 C组 · 挑战(4题)

C1综合辨析

解释为什么 ls *.txt 与 grep -n "*.txt" file 中 * 的含义完全不同。

查看参考答案

思路:关键在于"谁在解释这个 *"。

前者:* 由 shell 在命令执行前展开成一批真实文件名,ls 收到的是若干具体文件名。

后者:* 被写在双引号里,shell 不展开,原样交给 grep;grep 用的是正则表达式语法 —— 在正则里 * 表示"前一个字符重复 0 次或多次",独立的 "*.txt" 实际上匹配的是"任意多个非换行字符 + 一个点 + txt"。

结论:通配符(glob)由 shell 处理、作用于文件名;正则(regex)由程序处理、作用于文件内容。两者语法相似但语义不同,混用最容易出错。

C2脚本设计

写一个脚本 watch.sh:监控当前目录下所有 .cpp 文件的修改时间,一旦有文件更新就自动重新编译并跑样例。

查看参考答案

思路:用 md5sum(或 find -newer)记录"上次快照",定时比较,发现变化就触发编译与评测。

#!/bin/bash
while true; do
    NOW=$(md5sum *.cpp | md5sum)
    if [ "$NOW" != "$OLD" ]; then
        echo "检测到改动,重新编译…"
        g++ main.cpp -O2 -Wall -o main && ./main < main.in > main.out
        diff -b -q main.out main.ans && echo "样例通过"
        OLD="$NOW"
    fi
    sleep 2
done

考点:命令替换 $( )、while true 循环、字符串比较、&& 短路执行。

易错:忘记在比较后更新 OLD,导致每轮都重复编译;或在比赛环境下使用 sleep 死循环占用评测机资源(真实比赛中不建议常驻脚本)。

C3原理分析

为什么 grep -r "xxx" . 有时会"卡住"?如何避免搜索到 /proc、/dev 等特殊目录?

查看参考答案

原因:/proc、/sys、/dev 是"虚拟文件系统",里面的"文件"其实是内核状态,读它们可能阻塞或无限长(例如 /dev/zero)。若从 / 递归搜索就会卡住。

避免方法:① 只在选手自己的目录搜索:grep -rn "xxx" ~/csp;② 用 --exclude-dir 排除:grep -rn --exclude-dir={proc,sys,dev,bin} "xxx" .;③ 用 find 先限定类型再交给 grep:find ~/csp -type f -name "*.cpp" | xargs grep -n "xxx"。

结论:递归搜索一定要限定范围。"全盘搜索"既慢又危险,在赛场上更是时间杀手。

C4综合设计

设计一套"赛前 5 分钟自检清单"(用命令实现),覆盖:环境、文件、编译、样例、残留检查五项。

查看参考答案

思路:把每项检查变成一条可执行、输出明确的命令,串成一个脚本。

#!/bin/bash
# 1. 环境
pwd; echo "可用 g++:$(g++ --version | head -n1)"
# 2. 文件
ls -l
# 3. 编译(失败立即停止)
g++ main.cpp -O2 -Wall -std=c++17 -o main || exit 1
# 4. 样例
./main < main.in > main.out && diff -b -q main.out main.ans \
    && echo "样例:AC" || echo "样例:WA"
# 5. 残留检查
grep -n "freopen\|printf\|cerr\|TODO" main.cpp \
    || echo "无调试残留(正常)"

考点:把零散命令组织成有逻辑顺序的流程;|| 与 && 控制失败时是否继续。

易错:把编译放在样例之后(编译失败还去跑旧的产物);忘记 -Wall 导致警告被忽略。

🏆 洛谷原题精选(12道)

以下 12 道题建议在NOI Linux 终端下完成:用 mkdir 建目录、用 vim 写代码、用 g++ 编译、用重定向测样例,把本讲的命令全部用一遍。按难度从入门到普及+排列。

1
P1000
超级玛丽游戏入门
2
P1001
A+B Problem入门
3
P1046
陶陶摘苹果入门
4
P1047
校门外的树入门
5
P1421
小玉买文具入门
6
P1425
小鱼的游泳时间入门
7
P1085
不高兴的津津入门
8
P1307
数字反转入门
9
P1553
数字反转(升级版)普及-
10
P1059
明明的随机数普及-
11
P1308
统计单词数普及-
12
P1177
【模板】快速排序普及-