单引号 vs 双引号
单引号里一切原样保留,连 $ 和反斜杠都不解释;双引号里变量、命令替换、算术展开仍然生效。写正则、awk 程序、含 $ 的密码时优先用单引号。
echo '$HOME 不会被展开'
echo "$HOME 会被展开"grep -n '^root' /etc/passwdawk '{print $1, $3}' /etc/passwd字符串里既要单引号又要变量时,用 '"'"' 拼接,或者改用双引号加转义。
同一件事在 Shell 里通常有五六种写法,差别往往在「引号怎么用」「错误去哪了」「失败要不要停」。这一节按使用场景整理引号与变量展开、管道与重定向、文本处理、作业控制,以及可以直接抄走的一行命令片段。默认以 bash 为准,zsh 有差异的地方会写明。
Shell 里绝大多数「命令不生效」其实都是引号问题。记住一条主线:单引号是完全字面量,双引号只让 $、反引号和反斜杠特殊,不加引号才会做分词和通配符展开。
单引号 vs 双引号单引号里一切原样保留,连 $ 和反斜杠都不解释;双引号里变量、命令替换、算术展开仍然生效。写正则、awk 程序、含 $ 的密码时优先用单引号。
echo '$HOME 不会被展开'
echo "$HOME 会被展开"grep -n '^root' /etc/passwdawk '{print $1, $3}' /etc/passwd字符串里既要单引号又要变量时,用 '"'"' 拼接,或者改用双引号加转义。
${VAR} 参数展开花括号把变量名和后面的字符隔开,并支持一大批默认值语法:${VAR:-默认值} 为空时取默认值,${VAR:=默认值} 为空时赋值,${VAR:?提示} 为空时报错退出,${#VAR} 取长度。
PORT="${PORT:-8080}"
echo "监听端口: $PORT"echo "日志长度: ${#LOG_LINE}": "${DB_HOST:?请先设置 DB_HOST}"脚本里读环境变量一律用 ${VAR:-默认值},能避免大量「变量为空导致命令变成另一个命令」的事故。
命令替换 $() 与进程替换 <()$(命令) 把命令输出当字符串用,嵌套时比反引号清晰得多。<(命令) 把命令输出伪装成一个文件路径,常用来给只接受文件名的命令喂数据。
echo "今天是 $(date +%F),内核 $(uname -r)"diff <(sort a.txt) <(sort b.txt)while read -r line; do echo "$line"; done < <(ls -1)<() 依赖 /dev/fd,在部分精简容器里不可用;此时可退回临时文件。
花括号与通配符展开{a,b,c} 是纯文本展开,不依赖文件是否存在;* ? [] 是通配符,必须匹配到真实文件,匹配不到时默认按字面量传给命令。
cp /etc/nginx/nginx.conf{,.bak}mkdir -p data/{raw,clean,archive}ls -1 /var/log/*.logshopt -s nullglob # 匹配不到时展开为空,脚本更安全* 默认不匹配以点开头的隐藏文件,需要 .* 或 dotglob。
IFS 与按行读取read 默认会把行首尾空白吃掉,并解释反斜杠转义。读文件时统一用 IFS= read -r,否则带空格或反斜杠的行会被改坏。
while IFS= read -r line; do
printf '%s\n' "$line"
done < input.txtIFS=: read -r user _ uid _ <<< "$(grep '^root:' /etc/passwd)"
echo "$user $uid"read 的 -r 表示不处理反斜杠。忘了它,Windows 风格的路径几乎必然出错。
重定向决定数据去哪,退出码决定脚本停不停。这两件事搞混,是「命令明明成功了脚本却退出」和「错误信息看不到」的主要来源。
重定向 > >> 2> &>> 覆盖写,>> 追加写,2> 只重定向标准错误,&> 或 >file 2>&1 把输出和错误合到一起。n>file 可以按编号重定向任意文件描述符。
command > out.log 2> err.logcommand >> /var/log/app.log 2>&1command &> /dev/null # 全部丢弃command > /dev/null 2>&1 # POSIX 写法2>&1 的位置很关键:> file 2>&1 正确,2>&1 > file 只把错误指向终端。
退出码 $? 与 && ||$? 是上一条命令的退出码,0 表示成功。&& 前一条成功才执行后一条,|| 前一条失败才执行,; 无条件顺序执行。
systemctl is-active --quiet nginx && echo 运行中 || echo 未运行cd /data && rm -f cache/*.tmpcommand; echo "退出码: $?"管道默认只看最后一条命令的退出码。要抓中间失败,用 set -o pipefail 或 ${PIPESTATUS[@]}。
管道与 tee| 把左边标准输出接到右边标准输入。tee 一边显示一边写文件,配合 sudo 可以往只有 root 能写的文件里追加内容。
journalctl -u nginx --since today | grep -i error | tee errors.logecho '127.0.0.1 api.local' | sudo tee -a /etc/hostsps aux | awk '{print $3, $11}' | sort -rn | headsudo tee -a 是 >> 的提权替代品:sudo echo x >> file 里的重定向由当前用户执行,会失败。
set -euo pipefail脚本开头的三条保险:-e 任一命令失败就退出,-u 用未定义变量报错,-o pipefail 让管道中任一环失败即算失败。生产脚本建议一律加上。
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'set -x # 调试:打印每条实际执行的命令set +e # 临时允许失败-e 对 if 条件、&& 左侧、! 取反的命令不生效,这正是它可用却不烦人的原因。
xargs 批量处理把标准输入拆成参数列表反复调用命令。-0 配合 find -print0 处理含空格和换行的文件名,-n1 一次一个,-P 并行,-I{} 把参数插到任意位置。
find /data -name '*.log' -print0 | xargs -0 -r gzipcat urls.txt | xargs -n1 -P8 curl -sOls *.png | xargs -I{} convert {} -resize 50% sm-{}-r(GNU)或 --no-run-if-empty 可避免输入为空时仍然执行一次命令。
不写脚本也能做完大部分日志统计,靠的就是这四个工具加管道。下面的组合可以直接替换字段名和文件名后使用。
cut 取列-d 指定分隔符(默认制表符),-f 选字段,-c 按字符位置切。处理空格分隔的文本用 awk 更稳。
cut -d: -f1,3 /etc/passwdcut -c1-19 /var/log/app.log | uniq -cawk '{print $1, $NF}' access.logsort 排序-n 按数值,-h 按人类可读大小(1K 2M),-r 倒序,-u 去重,-k 指定列,-t 指定分隔符。
du -sh /var/lib/* | sort -rh | head -10sort -t: -k3 -n /etc/passwdsort -u hosts.txt > hosts.uniq排序结果受 locale 影响。要按字节序稳定排序,加 LC_ALL=C。
uniq 去重与计数uniq 只能去掉相邻重复行,所以几乎总是先 sort。-c 统计次数,-d 只看重复行,-u 只看唯一行。
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -20sort ip.txt | uniq -d统计 top N 的标准姿势:sort | uniq -c | sort -rn | head。
wc 计数-l 行数,-w 词数,-c 字节数,-m 字符数。统计文件数时应配合 find 而不是 wc。
wc -l /var/log/nginx/access.logfind /data -type f -name '*.csv' -exec wc -l {} + | tail -1grep -c 'ERROR' app.loggrep -c 比 grep | wc -l 快,也不会有管道退出码的坑。
tr / paste / column 排版tr 做字符级替换和删除,paste 按列合并多个文件,column -t 把对齐混乱的输出排成表格。
tr '[:lower:]' '[:upper:]' < names.txttr -d '\r' < dos.txt > unix.txtawk -F: '{print $1, $3}' /etc/passwd | column -ttr 不认识多字符替换,把 abc 换成 xyz 要用 sed。
SSH 一断命令就没了,是新手最常踩的坑。短任务用 nohup / &,正式服务交给 systemd,交互式长任务用 tmux。
& jobs fg bg命令末尾加 & 直接丢到后台;jobs 列出当前 shell 的作业,fg 调回前台,bg 让暂停的作业继续在后台跑。
./backup.sh &
jobs -lfg %1Ctrl+Z # 暂停当前任务,然后输入 bg 转后台& 起的进程仍是当前 shell 的子进程,关掉终端会收到 SIGHUP。
nohup 与 disownnohup 忽略 SIGHUP,让命令在终端关闭后继续运行;disown 把已经跑起来的后台任务从作业表里摘掉。
nohup ./sync.sh > sync.log 2>&1 &./sync.sh & disownsetsid ./daemon.sh > /dev/null 2>&1 < /dev/null &nohup 不会把进程变成守护进程,退出后仍然是普通进程,机器重启就没了。
tmux 会话把长任务放在 tmux 会话里,断线重连后继续看。适合编译、数据迁移、需要交互的排查。
tmux new -s buildtmux ls
tmux attach -t buildCtrl+B 然后 D # 脱离会话多数发行版默认没装:apt install tmux / dnf install tmux。
timeout 限制运行时间给命令设一个上限,超时后先发 TERM,再加 -k 会在宽限期后发 KILL。适合脚本里调用可能卡死的命令。
timeout 30s curl -sSf https://example.com/healthztimeout -k 5s 60s ./migrate.sh || echo '迁移超时'默认信号是 TERM。对忽略 TERM 的程序要显式加 -s KILL。
一个能直接改的模板,以及几个反复出现的坑。
脚本模板带严格模式、参数校验、临时目录清理和日志函数的骨架。
#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_NAME="$(basename "$0")"
log() { printf '[%s] %s\n' "$(date +'%F %T')" "$*"; }
die() { log "错误: $*" >&2; exit 1; }
[[ $# -ge 1 ]] || die "用法: $SCRIPT_NAME <目标目录>"
TARGET=$1
[[ -d $TARGET ]] || die "目录不存在: $TARGET"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
log "开始处理 $TARGET"trap ... EXIT 保证脚本无论正常结束还是报错退出都会清理临时文件,是很容易漏掉的一步。
位置参数与 shift$1..$9 是位置参数,$# 是个数,$@ 是全部参数(加引号时每个参数保持独立)。
for arg in "$@"; do echo "参数: $arg"; donewhile [[ $# -gt 0 ]]; do
case $1 in
-v|--verbose) VERBOSE=1; shift ;;
-o|--output) OUT=$2; shift 2 ;;
*) die "未知参数: $1" ;;
esac
done"$@" 和 $@ 差别很大:前者保留带空格的参数,后者会重新分词。
[[ ]] 与 (( ))bash 里判断字符串用 [[ ]],判断数字用 (( ))。[[ ]] 不会做分词和通配符展开,比 [ ] 安全。
[[ -f $file && $file == *.conf ]] && echo 是配置文件(( count > 10 )) && echo 超过阈值[[ ${name,,} == *.tar.gz ]] && echo 压缩包 # ,, 转小写test / [ 是外部命令语义,字符串比较要用 = 而非 ==,且变量必须加引号。
for 循环遍历的三种写法遍历列表、遍历命令输出、遍历文件。遍历 find 结果时用 while read 而不是 for。
for i in $(seq 1 5); do echo "第 $i 次"; donewhile IFS= read -r f; do gzip "$f"; done < <(find /data -name '*.log')for f in /etc/nginx/conf.d/*.conf; do nginx -t -c "$f"; donefor f in $(find ...) 会在文件名含空格时出错,一律改成 while read。
键盘上省下的时间比换工具更多。下面按 bash 默认绑定(emacs 模式)列出。
行编辑快捷键光标移动、删除、清屏、补全等最常用的一组键。
Ctrl+A / Ctrl+E # 行首 / 行尾
Ctrl+U / Ctrl+K # 删到行首 / 行尾
Ctrl+W # 删除前一个单词
Ctrl+L # 清屏(等价 clear)
Ctrl+C / Ctrl+D # 中断当前命令 / 结束输入Ctrl+D 在空行上等于 logout,SSH 里误按会直接掉线。
历史与反向搜索Ctrl+R 增量搜索历史命令,是复用复杂命令最快的方式。
Ctrl+R # 输入关键词反向搜索
Ctrl+R 再按 Ctrl+R # 继续往前找
右方向键或 Ctrl+E # 接受当前匹配history | grep ssh!! # 重复上一条命令
sudo !! # 用 sudo 重跑上一条history -c 只清当前会话,别指望它抹掉磁盘上的 ~/.bash_history。
补全与纠错Tab 补全命令、路径、变量;连续两次 Tab 列出候选。shellcheck 能在运行前查出脚本里的常见错误。
Tab Tab # 第一次补全,第二次列候选shellcheck deploy.shbash -n script.sh # 只做语法检查,不执行按「我想干什么」组织的现成写法,改掉路径和字段就能用。
统计访问量前 20 的 IPnginx access.log 默认格式里第一列就是客户端 IP。
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20找出占用空间最大的目录逐层下钻,直到定位到具体的大文件。
du -h --max-depth=1 / 2>/dev/null | sort -rh | head -15du -ah /var | sort -rh | head -20find / -xdev -type f -size +1G -printf '%s\t%p\n' 2>/dev/null | sort -rn | head-xdev 不跨文件系统,避免扫到挂载进来的网络盘或 /proc。
按时间批量归档日志/etc/logrotate.d/ 是正解,但临时处理用一行也够。
find /var/log -name '*.log' -mtime +7 -print0 | xargs -0 -r gzip -9find /var/log -name '*.gz' -mtime +90 -delete先跑不带 -delete 的版本确认清单,再加删除参数。
把命令结果写成 CSV用 OFS 指定输出分隔符,适合直接喂给表格软件。
ps -eo pid,user,pcpu,pmem,comm --sort=-pcpu | awk 'BEGIN{OFS=","} NR>1 {print $1,$2,$3,$4,$5}' > procs.csvdf -hP | awk 'NR>1 {print $1","$5","$6}' > disk.csv字段里本身含逗号时要加引号,这种场景用 python3 -c 或 csvkit 更可靠。
等待服务就绪启动依赖服务后轮询端口,避免脚本抢跑。
for i in $(seq 1 30); do
curl -sf http://127.0.0.1:8080/healthz >/dev/null && break
sleep 1
done || { echo '服务未就绪'; exit 1; }until ss -ltn | grep -q ':5432'; do sleep 1; done一定要有次数上限,否则依赖挂掉时脚本会永远等下去。
批量重命名没有 rename 命令时的纯 Shell 写法,先把 mv 打印出来确认。
for f in *.jpeg; do echo mv -- "$f" "${f%.jpeg}.jpg"; done # 先干跑for f in *.jpeg; do mv -- "$f" "${f%.jpeg}.jpg"; donedebian 系的 rename 是 perl 版,用法与 util-linux 的完全不同,跨机器别直接抄。