Lab0 工具链实战

Unix 哲学可以概括为几条非常“工程化”的原则:

  • 一个程序只做好一件事,并把这件事做到足够好。
  • 程序之间通过文本流协作,而不是彼此耦合。
  • 优先组合小工具,而不是一开始就写一个“大而全”的系统。
  • 让每一步都可观察、可替换、可调试。

1. 基础命令:文件与目录是所有工作的起点

场景命令
当前目录pwd
切换目录cd <dir> / cd .. / cd -
列出详细信息ls -alh
创建目录mkdir -p <dir>
创建空文件/更新时间戳touch <file>
复制cp <src> <dst> / cp -r <dir> <dst>
移动/重命名mv <src> <dst>
删除rm <file> / rm -r <dir>
查看文件cat <file> / less <file>

[!tip] 删除类操作建议先用 lsfind ... -printecho 预演一遍目标。


2. 重定向与管道:把命令拼成“数据流”

命令行真正强大的地方,不是单个命令,而是命令之间的数据连接。

2.1 重定向速查

写法含义
cmd > out.log覆盖写标准输出
cmd >> out.log追加写标准输出
cmd 2> err.log覆盖写标准错误
cmd 2>> err.log追加写标准错误
cmd > all.log 2>&1合并 stdout + stderr
cmd < in.txt文件作为 stdin

2.2 管道速查

写法含义
cmd1 | cmd2上一条输出喂给下一条
cmd | tee out.log一边显示一边落盘

2.3 实战

# 构建日志统一收集
make > build.log 2>&1

# 一边看一边保存
./run_tests.sh | tee test.log

# 统计错误行数
grep "ERROR" app.log | wc -l

3. find:文件系统检索入口

3.1 语法

find <path...> <条件...> <动作>

3.2 常用条件

类别示例说明
名称-name "*.c" / -iname "readme"名称匹配
类型-type f / -type d文件/目录
时间-mtime -7 / -atime +30最近修改/长时间未访问
大小-size +100M按文件大小
深度-maxdepth 2限制层级

3.3 常用动作

动作示例说明
打印-print默认动作
详细输出-lsls -dils
执行命令-exec cmd {} +批量执行
删除-delete高危

3.4 实战

# 找出所有 C 源文件
find src -type f -name "*.c"

# 排除 build 目录
find . -path ./build -prune -o -name "*.log" -print

4. grep:文本筛选主力

4.1 高频参数

参数含义
-n显示行号
-r递归搜索
-i忽略大小写
-E扩展正则
-v反向匹配

4.2 实战

# 递归查 TODO
grep -rn "TODO" src

# 找多个关键词
grep -E "error|fatal|panic" app.log

# 反向筛选非空行
grep -v "^$" README.md

5. sed:流式替换与清洗

sed 适合做“按行扫描 + 规则替换/删除/打印”的文本处理,尤其适合批量改配置和清洗日志。

5.1 语法结构

sed [选项] '<地址><命令>' file
  • 地址:决定“哪些行”执行命令(如 1,5/regex/)。
  • 命令:对命中行执行动作(如 s 替换、d 删除、p 打印)。

5.2 常用选项

选项作用
-n关闭默认输出,配合 p 精准打印
-i原地修改文件
-E使用扩展正则(ERE)

5.3 地址与命令速查

类型示例说明
行号地址sed -n '10,20p' file打印 10-20 行
正则地址sed '/ERROR/p' file命中 ERROR 的行
替换首个sed 's/old/new/' file每行只替换首个
全局替换sed 's/old/new/g' file每行全部替换
删除命中行sed '/^#/d' file删除注释行
删除空行sed '/^$/d' file清理空行
引用分组sed -E 's/(foo)_(bar)/\\2_\\1/' file分组重排

5.4 实战模板

# 1) 预览替换(不改文件)
sed 's/^DEBUG=.*/DEBUG=false/' .env

# 2) 原地替换(建议先备份)
sed -i.bak 's/^DEBUG=.*/DEBUG=false/' .env

# 3) 提取某段日志(起止模式)
sed -n '/BEGIN/,/END/p' app.log

# 4) 只看去掉注释和空行后的配置
sed '/^#/d;/^$/d' conf.ini

5.5 易错点

  • -i 会直接改文件,建议优先用 -i.bak
  • 正则里含 / 时,改用其他分隔符更清晰:sed 's#/usr/local#/opt#g' file
  • sed 默认会输出每一行;只想输出命中内容时要配 -n ...p

6. awk:按列处理与聚合

awk 适合“按字段处理文本”:筛选、格式化、统计、聚合一条命令完成。

6.1 语法结构

awk [选项] 'pattern { action }' file
  • pattern:匹配条件(可省略,省略表示每行都执行)。
  • action:对命中行执行的动作(打印、累加、格式化)。
  • 特殊块:BEGIN {}(读文件前执行)、END {}(读完后执行)。

6.2 常用变量与分隔符

含义
$0整行
$1..$N第 1..N 列
NR当前行号(全局)
FNR当前文件内行号
NF当前行字段数
FS输入分隔符
OFS输出分隔符

6.3 高频模式

目标示例说明
打印列awk '{print $1, $5}' access.log默认空白分隔
指定分隔符awk -F: '{print $1, $7}' /etc/passwd: 分列
条件过滤awk '$3 > 100 {print $1, $3}' data.txt只输出满足条件行
行号输出awk '{print NR \":\" $0}' file给每行加编号
求和awk '{sum += $3} END {print sum}' data.txtEND 输出聚合结果
计数awk '/ERROR/ {cnt++} END {print cnt+0}' app.log模式命中计数

6.4 实战模板

# 1) 输出文件前 10 行的第 1、2 列
awk 'NR<=10 {print $1, $2}' access.log

# 2) CSV 处理(逗号分隔)
awk -F, 'NR==1 {print "name,score"; next} {print $1 "," $3}' score.csv

# 3) 统计状态码分布(假设第 9 列是状态码)
awk '{code[$9]++} END {for (c in code) print c, code[c]}' access.log

# 4) 多文件处理时区分文件名
awk '{print FILENAME, FNR, $0}' a.txt b.txt

6.5 易错点

  • 分隔符不对会导致字段错位,先 awk '{print NF, $0}' 抽样检查。
  • 数值比较要确保字段是数字;必要时可写 $3+0 强制数值语义。
  • 关联数组遍历顺序默认无序;如果要排序,交给 sort 处理。

7. xargs:把输入转成参数批处理

7.1 高频参数

参数含义
-0配合 find -print0 安全处理空格
-n N每批 N 个参数
-I {}占位符替换

7.2 实战

# 安全批量 grep
find src -name "*.c" -print0 | xargs -0 grep -n "TODO"

# 每次处理 10 个参数
find . -name "*.jpg" | xargs -n 10 echo

8. gcc:从源码到可执行文件

8.1 常用命令

# 一步编译链接
gcc main.c -o app

# 分步编译
gcc -c main.c -o main.o
gcc -c util.c -o util.o
gcc main.o util.o -o app

8.2 建议参数

参数作用
-std=c11C 标准
-Wall -Wextra开启警告
-g调试信息
-O2优化
-Iinclude头文件目录
-L<dir> -l<name>库路径与库

推荐开发命令:

gcc -std=c11 -Wall -Wextra -g src/main.c -o app

9. Makefile:把“命令集合”变成“构建系统”

当项目文件变多后,手敲 gcc 会失控。Makefile 的价值是声明依赖,让增量构建自动发生。

9.1 一个可直接用的最小 Makefile

CC := gcc
CFLAGS := -std=c11 -Wall -Wextra -g -O2
TARGET := app
SRCDIR := src
OBJDIR := build
SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SOURCES))

.PHONY: all clean run

all: $(TARGET)

$(TARGET): $(OBJECTS)
	$(CC) $(CFLAGS) $^ -o $@

$(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR)
	$(CC) $(CFLAGS) -c $< -o $@

$(OBJDIR):
	mkdir -p $(OBJDIR)

run: $(TARGET)
	./$(TARGET)

clean:
	rm -rf $(OBJDIR) $(TARGET)

9.2 Makefile 符号与语义速查

符号/写法含义示例
target: deps规则定义:目标依赖于前置文件app: main.o util.o
\t<cmd>配方命令(必须是 Tab 开头)\tgcc ...
=递归展开变量(延迟求值)A = $(B)
:=立即展开变量(定义时求值)A := $(B)
?=仅在未定义时赋值CC ?= gcc
+=追加变量CFLAGS += -O2
$(VAR)取变量值$(CC)
%模式匹配(模式规则)%.o: %.c
$@当前目标名规则里输出文件名
$<第一个依赖单源编译常用
$^所有依赖(去重)链接阶段常用
$?比目标新的依赖增量更新场景
.PHONY声明伪目标.PHONY: clean run
#注释# build config
\换行续写长命令拆行
``order-only 依赖

9.3 常用内置函数

函数作用示例
$(wildcard pattern)匹配文件列表$(wildcard src/*.c)
$(patsubst a,b,text)模式替换.c -> .o
$(subst from,to,text)字符串替换替换路径前缀
$(addprefix p,names)批量加前缀生成路径列表
$(addsuffix s,names)批量加后缀生成扩展名
$(shell cmd)执行 shell 并取输出$(shell uname)

9.4 使用方式

make        # 构建
make run    # 运行
make clean  # 清理

10. Shell 脚本:把流程标准化成一条命令

Makefile 擅长构建依赖,Shell 脚本擅长串联任意步骤(检查、测试、打包、发布)。

10.1 一个可直接用的 build.sh

#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR"

LOG_DIR="logs"
mkdir -p "$LOG_DIR"

echo "[1/4] lint-like scan"
find src -name "*.c" -print0 | xargs -0 grep -nE "TODO|FIXME" || true

echo "[2/4] build"
make >"$LOG_DIR/build.log" 2>&1

echo "[3/4] run"
./app >"$LOG_DIR/run.log" 2>&1

echo "[4/4] done"
echo "build log: $LOG_DIR/build.log"
echo "run log:   $LOG_DIR/run.log"

10.2 Shell 常用语法:函数、条件、循环

能力语法模板说明
函数fn() { ... }复用逻辑
条件 ifif [[ cond ]]; then ... elif ... else ... fi分支判断
分支 casecase \"$x\" in ... esac多分支更清晰
for 循环for x in a b; do ...; done遍历列表
while 循环while cond; do ...; done条件循环
数组遍历for x in \"${arr[@]}\"; do ...; done批量参数
小括号 ()(cd /tmp && ls)子 Shell;不影响当前 Shell 目录/变量
双小括号 (( ))((i++)); ((a > b))算术运算/算术判断(整数)
中括号 [ ]if [ -f file.txt ]; then ...; fi传统测试命令,[] 两侧要留空格
双中括号 [[ ]]if [[ "$s" == *.log ]]; then ...; fiBash 扩展测试;支持模式匹配,变量通常无需额外转义

10.3 怎么判断输入参数是不是空

这里通常要分两种情况:

  • 没传参数:比如直接执行 ./build.sh,此时 $1 不存在。
  • 传了空字符串:比如执行 ./build.sh "",此时 $1 存在,但内容为空。
# 判断“第一个参数是否为空或未传”
if [[ -z "${1:-}" ]]; then
  echo "first argument is empty"
fi

# 判断“是否根本没传任何参数”
if [[ $# -eq 0 ]]; then
  echo "no arguments provided"
fi

# 区分“没传”和“传了空字符串”
if [[ $# -eq 0 ]]; then
  echo "missing argument"
elif [[ -z "$1" ]]; then
  echo "argument is an empty string"
else
  echo "argument = $1"
fi
  • -z:判断字符串长度是否为 0。
  • ${1:-}:当 $1 未设置时,给一个空字符串默认值;配合 set -u 更安全。
  • $#:当前传入参数个数。

如果脚本开启了 set -u,不要直接写 [[ -z "$1" ]] 判断“可能不存在”的参数,更稳妥的写法是 [[ -z "${1:-}" ]]

10.4 Shell 实战版(含函数 + 条件 + 循环)

#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR"

LOG_DIR="logs"
mkdir -p "$LOG_DIR"

log() {
  printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*"
}

require_cmd() {
  local cmd="$1"
  if ! command -v "$cmd" >/dev/null 2>&1; then
    echo "missing command: $cmd" >&2
    exit 1
  fi
}

scan_todo() {
  log "scan TODO/FIXME in src"
  find src -name "*.c" -print0 | xargs -0 grep -nE "TODO|FIXME" || true
}

build_target() {
  local mode="${1:-debug}"
  case "$mode" in
    debug) CFLAGS_EXTRA="-O0 -g" ;;
    release) CFLAGS_EXTRA="-O2 -DNDEBUG" ;;
    *)
      echo "usage: $0 [debug|release]" >&2
      exit 2
      ;;
  esac
  log "build mode=$mode"
  make CFLAGS_EXTRA="$CFLAGS_EXTRA" >"$LOG_DIR/build-$mode.log" 2>&1
}

run_with_checks() {
  if [[ ! -x ./app ]]; then
    echo "binary ./app not found or not executable" >&2
    return 1
  fi
  log "run app"
  ./app >"$LOG_DIR/run.log" 2>&1
}

main() {
  local mode="${1:-debug}"
  local required=(find xargs grep make)
  local cmd
  for cmd in "${required[@]}"; do
    require_cmd "$cmd"
  done

  scan_todo
  build_target "$mode"

  local i=0
  while [[ $i -lt 1 ]]; do
    run_with_checks || true
    i=$((i + 1))
  done

  log "done"
  log "build log: $LOG_DIR/build-$mode.log"
  log "run log:   $LOG_DIR/run.log"
}

main "$@"

10.5 执行方式

chmod +x build.sh
./build.sh           # debug
./build.sh release   # release

11. 一条龙流程(查问题 -> 改代码 -> 构建)

# 1) 定位源码文件
find src -type f \( -name "*.c" -o -name "*.h" \)

# 2) 搜索待处理标记
find src -name "*.c" -print0 | xargs -0 grep -nE "TODO|FIXME"

# 3) 预览替换
find src -name "*.c" -print0 | xargs -0 sed 's/old_api/new_api/g'

# 4) 构建并保存日志
make > build.log 2>&1

[!warning] 高危操作:rm -rffind ... -deletesed -i。建议固定流程:先预览、再执行、最后核验。