编程 终端能跑、进 cron 就废:Shell 脚本丢变量、丢 PATH、删错路径的修法

2026-09-16 20:03:29

终端能跑、进 cron 就废:Shell 脚本丢变量、丢 PATH、删错路径的修法

一、题库式 Shell 脚本为什么会出事

一个 CI/CD 脚本里写了 for i in $(ls /data/logs/*.log),当日志文件名包含空格或换行时,脚本会删错路径下的文件,导致数据丢失。题库只教语法,不教系统边界。

同一个需求,两种写法:

  • 循环处理:题库 for file in $(ls *);生产级 find . -maxdepth 1 -print0 | xargs -0
  • 变量引用:题库 $var;生产级 "${var}"(防空格注入)
  • 错误拦截:题库 if [ $? -eq 0 ];生产级 set -euo pipefail(异常自动终止)
  • 正则匹配:grep/sed;生产级 awk(处理结构化数据更高效)
  • 信号处理:忽略;生产级 trap 'cleanup' EXIT INT TERM(确保退出清理)

二、管道与子 shell:变量为什么丢了

每个管道符号 | 都会创建新的子进程。Shell 变量在管道右侧的子进程中是只读副本。下面这种累加计数是典型误区,count 在循环结束后即被销毁:

cat log | while read line; do count=$((count+1)); done

正确做法是用重定向,保持在主 Shell 进程内操作:

while read line; do count=$((count+1)); done < file

三、生产级脚本骨架

#!/usr/bin/env bash
# 健壮性开关:脚本报错即退出,管道任一环节失败即退出,变量未定义即报错
set -euo pipefail
IFS=$'\n\t'
cleanup() { rm -f "${TMP_FILE}"; echo "清理资源完成,进程退出。"; }
trap cleanup EXIT INT TERM
# 生产级读取示例:严防空格路径
find /var/log/app -name "*.log" -print0 | while IFS= read -r -d '' logfile; do
    awk '/ERROR/ {print $0}' "${logfile}" >> ./error_report.log
done

四、cron 环境为什么和终端不一样

脚本在终端跑得好,一放进 crontab 就找不到命令、找不到文件、输出为空。原因:

  1. cron 的 PATH 极简,一般只有 /usr/bin:/bin,脚本里用的 /usr/local/bin/python/opt/some/bin/tool 不在里面。
  2. HOME 可能没正确设置,脚本里的 ~/.ssh/config~/logs 访问不到预期位置。
  3. 不加载用户 shell 配置文件,alias、环境变量缺失。

排查建议:模拟 cron 的最小环境测试:

env -i /bin/bash /path/to/your/script.sh

修复方向:脚本开头显式设置 PATH、HOME:

#!/bin/bash
export PATH=/usr/local/bin:/usr/bin:/bin
export HOME=/root

五、Windows 上跑脚本常见的三处环境问题

  • PATH:终端能用不代表脚本能用;脚本里先检查 command -v npm || { echo "npm 不存在"; exit 1; }
  • 换行符:Windows 是 \r\n,Linux 是 \n,会出现 $'\r': command not found;用 dos2unix 转,或编辑器改成 LF。
  • 执行策略:PowerShell 默认禁止运行脚本,需管理员执行 Set-ExecutionPolicy RemoteSigned

六、for 循环读文件的隐藏雷区

for ip in $(cat ip_list.txt); do ping -c 1 $ip > /dev/null; echo "$ip 通"; done

ip_list.txt 的行尾带 \r,ping 的是错误地址;文件里若有通配符或空格,$(cat ...) 会被拆词,循环变量分裂。更稳的写法:

while IFS= read -r ip; do ping -c 1 "$ip" > /dev/null; echo "$ip 通"; done < ip_list.txt

-r 防止反斜杠转义,IFS= 确保行首尾空格不被吞掉。另外不要在循环体内修改该输入文件,因为 while ... done < file 是持续读入的。

七、rm 脚本不校验入参就是在赌人品

PROJECT="${1:-}"
if [ -z "$PROJECT" ]; then echo "用法: $0 project"; exit 1; fi
TARGET="/data/$PROJECT/tmp"
# 防止误删根目录或空路径
case "$TARGET" in /data/*) ;; *) echo "非法路径"; exit 1 ;; esac
rm -rf "$TARGET"/*

自动化脚本的价值不在于多酷,而在于可以放心反复执行。

八、括号与引号:7 种括号的用途

  • () 小括号:定义数组;新开子 Shell 执行命令,环境隔离(内部改变量、切目录退出后不生效)
  • (()) 双小括号:整数运算与数值比较
  • [] 单中括号:老式条件判断,注意内侧必须留空格(新手最常见的语法错误),新脚本建议弃用
  • [[]] 双中括号:Bash 专属条件判断,支持字符串、文件、多条件、正则,容错性高
  • {} 花括号(大括号):Brace expansion
  • ${} 变量取值、默认值兜底、字符串裁剪,杜绝变量拼接错乱
  • $() 命令替换,捕获标准输出,可多层嵌套;已全面淘汰老式反引号

规范:数字运算只用 (());字符串/文件/多条件/正则判断优先 [[]];获取命令结果统一 $();所有变量统一套 ${}。注意 [[]](()) 为 Bash 专属,sh 不识别,脚本头部必须声明 #!/bin/bash

九、几个提效做法

  • 善用 xargs 并行:find . -name "*.log" | xargs -P 4 -I {} gzip {}
  • 结构化文本优先 awk,一个 awk 常能替掉多个管道命令,减少进程创建开销
  • 脚本开头检查必要文件/路径是否存在;用 [[ ]] 代替 [ ];尽量用绝对路径,避免 PATH 不同导致失败
  • 涉及密码、数据库账号不要明文写入,改用环境变量传入
复制全文 生成海报 Shell Bash 运维 脚本 crontab

推荐文章

程序员茄子在线接单