从敲命令到写脚本:Shell编程入门实战指南

发布时间:2026/9/15 22:43:01
从敲命令到写脚本:Shell编程入门实战指南 先说个我观察到的现象很多人在终端里敲命令挺熟练cd、ls、grep、ps这些张口就来但一提到写Shell脚本就心里发怵总觉得那是“程序员”才该干的事。反过来也有不少人Python写得不错但一到要用命令行处理文件、批量操作服务器时就手忙脚乱非要写个.py文件才算“正经编程”。Shell的乐趣恰恰在于——它是这两种能力的交叉点。你把命令敲明白了其实已经具备了编程最基本的元素你需要的只是把“敲命令”这件事往前再推一步用脚本把一堆命令串起来让系统替你做那些重复、机械、容易出错的事情。这篇文章我不打算给你堆砌命令大全那是man命令和搜索引擎就能干的事。我想分享的是我从“只会敲命令”到“能独立写脚本”这段过程里真正想明白的几个关键点命令和编程之间的边界到底在哪里为什么同样的逻辑写在命令行里和写在脚本里结果会不一样还有那些文档里不会写、只有踩过坑才能总结出来的细节。内容尽量做到从零起步但我会默认你已经会打开终端、知道ls是列目录不然篇幅实在兜不住。适合正在学Linux命令、准备入坑Shell脚本的运维、开发还有想把日常操作自动化的普通用户。1. 先把“命令”和“编程”这层窗户纸捅破1.1 一行命令本身就是一段微型的程序很多人觉得“编程”门槛高得学变量、学循环、学函数。但你仔细想想命令行里每一条命令其实都自带“编程基因”。拿最普通的grep来说grep error app.log | wc -l这条管道已经包含了输入、处理、输出三个环节grep是过滤器wc -l是统计器管道|把前一个命令的输出传给下一个命令作为输入。这不就是一种数据流编程吗你再往深看一层命令行的通配符就是个典型的“变量展开”机制。ls *.txt里的*本质上就是在执行前由Shell帮你生成一系列文件名参数这跟编程里的“遍历集合”是同一个思路。for i in *.log; do echo $i; done你把它写成一行贴到终端里它就是个循环程序写成多行存成文件加个#!/bin/bash在开头它就成了脚本。区别只在“存储”与“复用”的形式底层逻辑一模一样。所以我的第一个建议是别把Shell编程当成一门新学科去学。它就是给你平时敲的那些命令加上了“组织能力”和“逻辑控制能力”。有了这两样你就能把反复敲了十遍八遍的操作固化成一个可复用的工具。1.2 从“敲命令”到“写脚本”核心是三步跨越我自己总结了一下这个跨越其实就三步第一步把命令“串起来”。用管道|、重定向、、套接字等方式让多个命令配合工作。比如你要统计nginx日志里访问量最高的10个IP一句话就够了awk {print $1} access.log | sort | uniq -c | sort -rn | head -10。这里面的sort、uniq、awk单个拿出来都很简单但它们组合在一起就产生了新的能力。第二步把参数“变量化”。你发现同一个命令序列要反复执行但每次处理的目标文件、要匹配的关键词可能不一样。这时候你就得把可变化的部分抽出来用$1、$2或者变量名来表示。一旦变量进来你的命令序列就从“一次性”变成了“可复用”。第三步把逻辑“分支化”。有了变量还不够你还得让它能判断文件存在不存在参数传没传进来命令执行成功还是失败这时候就需要if、case、、||这些控制结构。走到这一步你已经不是在“敲命令”了而是在“写程序”只是这个程序的写法特别直接一行一行都是对系统调用所见即所得。1.3 命令行的“即时反馈”是学习编程最好的温床为什么我特别推荐用Shell来入门编程逻辑因为反馈太快了。你写一个C程序要编译、要链接、要处理头文件写个Python程序虽然不用编译但至少也得存成.py文件再执行。而Shell里你敲一行回车结果立刻就在屏幕上错了马上就能改改完再执行这个“试错-修正”的循环可以做得非常短。我见过不少朋友在Shell脚本里用for循环写到一半忘了语法直接回终端敲一行for i in 1 2 3; do echo $i; done试试语法对不对一目了然。这就是Shell的好处——整个系统都是你的交互式开发环境写脚本不再是“写一段代码然后运行”而是“把已经验证过的命令组织起来”。2. 准备阶段终端、编辑器与命令执行的底层逻辑2.1 先搞清楚你在跟谁说话Shell的类型与登录环境你开机打开终端输入echo $SHELL大概率看到/bin/bash也有些人用的是zsh。我们常说的“Shell”其实是两类东西的统称一类是图形化终端模拟器比如GNOME Terminal、iTerm2它负责把键盘输入送给程序并显示输出另一类是真正的命令行解释器比如bash、zsh、sh它负责解析并执行你输入的字符串。这两层经常被混在一起说但搞明白它们对理解脚本执行环境特别重要。比如你双击运行一个.sh脚本它用的是哪个解释器答案是看脚本第一行#!/bin/bash或者#!/usr/bin/env bash这叫shebang。但如果你的脚本没有shebang呢那系统会用你当前的默认Shell来跑。这里就有个经典坑你在终端里用的是bash语法没问题但脚本文件第一行写的是#!/bin/sh而系统里的/bin/sh实际指向dash有些bash特有的语法比如[[ ]]、数组在dash下就是报错。所以写脚本第一步先想清楚你给谁跑。登录Shell和非登录Shell的区别也经常让人困惑。简单说登录Shell会读取/etc/profile、~/.bash_profile这类文件非登录Shell读取的是~/.bashrc。这就是为什么你改了~/.bashrc里的alias打开新终端有生效但偶尔某次通过SSH登录进去却不生效——因为SSH登录是“登录Shell”它读的是~/.bash_profile如果这个文件里没有显式source ~/.bashrc你改的别名就“丢了”。排查这类问题先问自己三句话当前是登录Shell还是非登录Shell我改的配置文件对应哪一类有没有被其他文件覆盖2.2 选择趁手的编辑器至少把vim的基础操作练熟写Shell脚本用什么编辑器这个话题能引发战争但我建议你至少在命令行环境里把vim的基本操作练熟。原因很现实你迟早要SSH到一台远程服务器上改配置那时候没有VS Code没有图形界面你手边只有vim、nano这一类终端编辑器。我见过太多人卡在vim上不知道按i进入插入模式不知道怎么保存退出一打开文件就慌了神。其实用vim写脚本只需要知道几个组合键就够上路了i进入编辑、Esc回到普通模式、dd删除一行、yy复制一行、p粘贴、/keyword搜索、:wq保存退出、:q!不保存退出。配合gg跳文件开头、G跳文件末尾基本能应付绝大多数场景。另外一个实用技巧在vim里可以直接调用Shell命令输入:!ls -la能看到当前目录内容而不退出vim想把当前文件内容用bash语法检查一下输入:%!bash -n也不会跑只是检查语法。如果你经常写脚本建议在~/.vimrc里加一行syntax on脚本文件的关键字、变量、字符串会有高亮找错误位置快得多。2.3 掌握type、which、command知道你敲的命令到底是什么Shell里有个隐藏的“同名覆盖”问题特别坑新手。系统自带一个命令你也写了个同名脚本放在~/bin里PATH环境变量一设置你敲的可能是你自己的脚本而不是系统命令。如何排查type -a ls会把所有叫ls的命令候选全部列出来顺序就是PATH查找顺序。更关键的是Shell里有“内建命令”和“外部命令”之分。cd、echo、export、read这些是Shell自带的不需要启动额外进程而grep、awk、ls则是外部程序系统要fork一个新进程来跑。这个区别在日常使用中感觉不明显但在脚本性能上很敏感——一个循环内跑一百次外部命令和一百次内建命令性能差距是数量级的。所以脚本优化的一条经验法则就是循环里能用内建命令解决的事别动不动就调外部程序。还有一个容易被忽略的command命令。当你定义了函数或别名覆盖了原命令想跳过它们直接执行原始命令就用command ls。这在写脚本时特别有用比如脚本里定义了一个叫cd的函数那你可能就得用command cd /path来真正切换目录防止递归调用。2.4 环境变量不只是PATH那么简单说环境变量大多数人都知道echo $PATH。但真正写脚本时你可能要关心的是这几个$HOME当前用户主目录、$PWD当前所在目录、$OLDPWD上一次所在目录cd -就是靠它、$IFS字段分隔符默认是空格、Tab、换行、$?上一条命令的退出码。尤其$?在脚本判断成败时是命根子0代表成功非0代表某种错误。IFS这个东西我建议专门花10分钟弄明白。它决定了Shell在解析字符串时把什么当作“分隔符”。默认情况下字符串a b c在for i in $str中会拆成三个词但如果某个文件名里带空格比如My File.txt用for i in *.txt就不会被拆开因为通配符展开是按“完整文件名”来的跟IFS没系。可变量值展开时IFS就介入了。这个区别让无数人写脚本时翻车明明文件叫“My File.txt”自己用变量去遍历文件列表时却被拆成了“My”和“File.txt”两个。避免方法简单粗暴遍历文件优先用通配符展开而非把ls的结果存进变量再遍历。3. 脚本核心语法会这几样就能解决90%的需求3.1 脚本的基本骨架与“引号”的哲学一个标准的bash脚本长这样#!/usr/bin/env bash # 作者、日期、用途注释 set -euo pipefail readonly SCRIPT_DIR$(cd $(dirname ${BASH_SOURCE[0]}) pwd) main() { echo 脚本所在目录: $SCRIPT_DIR } main $set -euo pipefail这一行我强烈建议你养成习惯。它做了三件事-e表示脚本中任何一条命令返回非0就立即退出免得错误一路滚下去-u表示使用未定义的变量直接报错而不是当成空字符串能揪出一堆拼写错误pipefail表示管道中任何一条命令失败整个管道的返回状态就是失败不会出现“前面都错了最后一条成功”的假象。这三件套能挡住大部分低级错误。引号的学问值得单独好好讲讲因为它是Shell里最容易出玄学问题的地方单引号var里面的内容原样输出不展开变量、不解析任何转义。适合存正则表达式、特殊字符。双引号var会展开变量和命令替换但不解析*、?这类通配符。这是你最常用的。反引号cmd或$(cmd)命令替换把命令的输出作为值赋给变量。注意$(cmd)里的变量、引号能正确嵌套反引号对嵌套支持差别再用反引号了。nameworld echo Hello $name # 输出: Hello $name echo Hello $name # 输出: Hello world echo Today is $(date %F) # 输出: Today is 2025-01-01还有一个小坑变量赋值时等号两边不能有空格。name world会被解析成执行三个参数的命令报错name: command not found。这个跟大多数编程语言习惯很不一样新手经常在这卡壳。3.2 分支判断[ ]vs[[ ]]还有testShell的条件判断语法可以说是劝退新手的第一大劫难。写法其实就两种[ 条件 ]和[[ 条件 ]]另外还有个独立的test命令跟[ ]等价。区别在哪[ ]是外部命令或者内建命令的一种形式它要求每个元素之间用空格隔开而且它对空变量特别敏感。比如name if [ $name world ]; then echo match fi这段代码会报错因为变量展开后变成了[ world ][命令看到一个孤零零的语法直接崩了。正确写法是给变量加双引号if [ $name world ]。而[[ ]]是bash扩展的关键字内部对空变量宽容得多不用加引号也能正确判断还支持、||、正则匹配~。所以我个人写脚本的规则很简单能用[[ ]]的地方绝不用[ ]。但要注意[[ ]]不是POSIX标准如果你明确知道自己要兼容/bin/sh、跑在dash或者其他最小化环境里那只能老老实实用[ ]。文件判断也是高频操作-f判断是否普通文件、-d判断是否目录、-x判断是否可执行、-z判断字符串是否为空、-n判断是否非空。组合用法就是if [[ -f /etc/nginx/nginx.conf ]]; then echo nginx config exists; fi。这套东西没难度但需要你在写脚本时形成“先判断再操作”的肌肉记忆避免直接操作不存在的文件时报出莫名其妙的问题。3.3for循环和while循环用好能少写一半代码循环是脚本里性价比最高的语法。最常用的是for遍历一个列表for file in *.log; do echo 处理 $file gzip $file done这里有个细节*.log如果匹配不到任何文件它不会为空而是保持字面量*.log传给for结果就是脚本尝试处理一个不存在的文件。所以严谨点的写法是加上判断for file in *.log; do [[ -e $file ]] || continue; ...; done。另一种更灵活的遍历是C风格循环适合需要计数或者操作数组索引的场景for ((i1; i10; i)); do echo 第 $i 次 donewhile循环通常配合read逐行处理文件或命令输出这是文本处理里的杀手锏while IFS read -r line; do echo 读取到: $line done /etc/passwd注意那个IFS read -r的写法IFS意思是这一行里不按空格拆分字段保持完整行-r表示不把反斜杠当转义符。这两个配合起来才能保证读到的每一行都会原样处理不会因为行内有特殊字符而“截断”。循环里还有一个我常用的小技巧对管道里的命令做循环。比如你想对某个命令的每一行输出做判断可以直接用ps aux | grep java | while read pid ...这样每个进程都能被循环处理而不用先在内存里建数组适合处理大数据量。但注意管道的右边是在子Shell里运行的你在while里修改的变量循环结束后外部是看不到的。如果一定要保留循环里算出来结果就得用进程替换 (cmd)或者把结果写入文件这个坑挺常见的。3.4 函数、返回值与shift把脚本从“线性执行”变成“模块化”当你的脚本超过50行就应该考虑用函数拆分逻辑了。函数定义很简单log_info() { echo [$(date %F %T)] $* } log_info 用户登录成功函数里$1、$2是函数自己的参数跟脚本的全局参数是分开的。返回值用return关键字注意Shell的返回值是数字0-255它不是“返回字符串”而是“返回状态码”。你要让函数产出字符串一般用echo捕获get_config() { echo /etc/nginx/nginx.conf } config_file$(get_config) echo 配置文件是: $config_fileshift这个命令我单独拿出来说因为它在处理命令行参数时太有用了。它的作用是把位置参数整体左移原来$1变$2、$2变$3原来的$1被丢弃。这就让你在脚本里可以这样处理参数——循环消费掉所有参数while [[ $# -gt 0 ]]; do case $1 in -f|--force) force1 shift ;; -n|--name) name$2 shift 2 ;; *) echo 未知参数: $1 exit 1 ;; esac done这段代码我以前懒得用基本都是硬编码$1、$2导致脚本参数一多就乱套。后来认真理解了shift才发现解析命令行参数就是这么简单——你不需要一个复杂的解析框架shift就是要你逐个消费参数的工具。配合case做分支脚本马上就能支持形如my_script --name foo -f这样的参数格式体验立刻上了一个档次。3.5 调试三板斧bash -n、bash -x、set -x写脚本不可能一次写对关键是快速找到错在哪。bash -n script.sh只做语法检查不会真正执行。语法有错比如if少了then、括号不匹配它会直接告诉你第几行有问题。写完后跑一下这个能拦截掉大部分低级错误。真正执行时发现问题最实用的是bash -x script.sh。它会打印出每一步实际执行了什么变量展开后的真实值是什么。举个例子你某个变量值是空的执行时就会发现[ world ]立刻明白条件为啥不成立。还有一种办法是在脚本内部临时加set -x开启调试、set x关闭。另一个调试神器是trap命令。你可以在脚本进函数或退出时打点比如trap echo line $LINENO: 变量 value$value DEBUG每一行执行前都会打印调试信息。虽然性能开销大但在排查复杂逻辑时非常管用比你手动加一堆echo干净得多。提示在脚本中看到“No such file or directory”却确认文件存在时第一反应应该是“文件来源是否为Windows环境”——CRLF换行问题会在第5章详细说。4. 实战三个源自日常需求的脚本案例4.1 批量重命名文件可别把“文件名带空格”不当回事假设你有一堆文件叫report (1).pdf、report (2).pdf现在要把括号里的数字改成三位编号变成report (001).pdf。新手可能会试图用sed处理但文件名里有空格直接for i in *.pdf会怎样不会被拆开因为通配符展开得到的是完整文件名空格是文件名的一部分。此时你只需要用循环和字符串处理#!/usr/bin/env bash set -euo pipefail for file in report \(*\).pdf; do # 提取括号中的数字 num$(echo $file | sed -n s/report (([0-9]*)).pdf/1/p) new_name$(printf report (%03d).pdf $num) mv -- $file $new_name echo 重命名: $file - $new_name done这里面有几个经验点第一--符号告诉mv后面的内容都是文件名避免文件名以-开头时被当成参数第二printf里的%03d自动补齐三位数字比用if判断优雅多了第三每次改动前先echo出来预览一遍确认无误再真正执行mv。批量操作最怕的就是脚本跑完才发现规则写错了几百个文件已经改名想恢复都难。4.2 系统健康检查脚本把零散命令组织成可读报告运维场景里检查一台服务器状态通常要敲一串命令uptime看负载、free -h看内存、df -h看磁盘、ps aux --sort-%cpu看最占CPU的进程。手动敲很爽但要把它们组合成一段有逻辑的检查那就是脚本的活。#!/usr/bin/env bash set -euo pipefail # 彩色输出函数 color_echo() { local color$1 local text$2 case $color in red) echo -e 033[31m$text033[0m ;; green) echo -e 033[32m$text033[0m ;; yellow) echo -e 033[33m$text033[0m ;; *) echo $text ;; esac } # 磁盘使用率检查超过80%告警 disk_check() { local usage usage$(df -h / | awk NR2{print $5} | sed s/%//) if [[ $usage -gt 80 ]]; then color_echo red 磁盘使用率: $usage% (超过80%告警) elif [[ $usage -gt 60 ]]; then color_echo yellow 磁盘使用率: $usage% (请注意) else color_echo green 磁盘使用率: $usage% (正常) fi } # 内存检查 mem_check() { local mem_used mem_total mem_used$(free -m | awk NR2{print $3}) mem_total$(free -m | awk NR2{print $2}) local percent$((mem_used * 100 / mem_total)) color_echo yellow 内存使用: ${mem_used}MB / ${mem_total}MB (${percent}%) } echo 服务器健康检查 echo - uptime信息: uptime echo echo - 磁盘检查: disk_check echo echo - 内存检查: mem_check这段脚本的核心思想就是把本来就该手动做的检查动作通过函数的拆分变成了模块化的输出。以后再要扩展检查项加一个函数、在main里调用就行。事实上很多开源的监控脚本就是这样慢慢长出来的——先是一两条命令然后加判断、加颜色、加告警最后变成几百行的工具。4.3 用ADB Shell操作Android设备从命令到自动化脚本前面讲的是Linux和服务器但在Android开发和测试领域Shell脚本同样无处不在。Android系统本身就基于Linux内核系统里也自带了完整的Shell环境。开发者通过adb shell命令可以进入设备内置的Shell执行各种操作。比如你在真机或模拟器上执行一条adb shell sh /storage/emulated/0/android/data/com.omarea.vtools/up.sh这行命令的意思是通过ADB连接到设备然后在设备上运行指定路径下的up.sh脚本。这在很多手机性能调节工具、Magisk模块脚本、自动化测试场景中非常常见。原理跟你在服务器上sh script.sh没有区别只是多了一层adb shell作为连接通道。这类脚本能干的事非常多批量安装/卸载应用、收集系统日志、模拟点击和滑动、开启或关闭系统功能、修改系统设置等等。封装成Shell脚本后一个命令就能代替手动操作手机上十几个步骤#!/usr/bin/env bash # 批量向已连接设备安装APK set -euo pipefail APK_DIR./apks if [[ ! -d $APK_DIR ]]; then echo APK目录不存在: $APK_DIR exit 1 fi for apk in $APK_DIR/*.apk; do echo 正在安装: $apk adb install -r $apk done echo 全部安装完成这个思路用一句话总结凡是你能手动通过命令完成的操作都可以写成脚本自动化。手动操作会遗漏、会疲劳、会按错脚本不会。4.4 把脚本与Git、Cron等工具联动构建自动化日常脚本写好了通常还要跟已有工具链配合。比如用Git管理脚本版本——把脚本放进Git仓库改动有记录、出问题能回滚。再比如用crontab定时执行——日志清理、备份、健康检查这些任务都可以定时跑脚本让系统自己干活。# 每天凌晨2点备份数据库并清理7天前的备份 0 2 * * * /home/user/scripts/backup_db.sh /var/log/backup.log 21这里特别要提醒“重定向”的作用 /var/log/backup.log把脚本输出追加到日志文件21把错误输出也一起并入日志。如果不加这一步cron执行的脚本输出会通过邮件发给你或者直接丢失加了以后你随时可以回看日志定位问题。我自己的习惯是任何脚本都要带两个“保险”第一个是日志记录脚本什么时候跑、执行了什么、结果如何第二个是退出码校验关键步骤失败时要能感知。这两者配合才敢放心让脚本在后台自动跑不然自动化反而变成定时制造问题的“定时炸弹”。5. 常见问题与排查技巧实录5.1 高频雷区速查表Shell脚本的坑特别多我先整理一张最常见的问题速查表都是我实际踩过的现象原因快速解决脚本报错“command not found”PATH设置错误或脚本所在目录没加进PATH、未使用./执行执行用./script.sh或把脚本目录加入PATH定义了变量但输出为空等号两边有空格比如name world去掉空格nameworld[ ]判断时报语法错误变量值为空条件变成[ world ]变量加双引号if [ $name world ]脚本在Windows上写好Linux执行报错文件是CRLF换行\r混进命令用sed -i s/r$// script.sh或者dos2unix script.sh脚本没执行权限chmod x没做chmod x script.sh循环里改了变量循环外没变化管道右侧命令在子Shell中执行改用进程替换 (cmd)或把结果写入临时文件通配符没匹配到任何文件*.log保持字面量传给循环加判断[[ -e $file ]] || continue远程执行脚本报错$\r同样CRLF问题在Windows上写脚本时把换行改为LF使用read读行只读到第一行没有用while read循环或者IFS没处理用while IFS read -r line; do ...; done file5.2 排查问题的思路顺序从“退出码”到“追踪输出”遇到脚本行为异常我个人的排查顺序是固定的第一看退出的状态码。上一条命令执行完立刻echo $?0就是成功非0就是失败。这一步能快速缩小问题范围——是命令本身失败了还是逻辑判断跟预期不符。第二跑一遍bash -x。看脚本实际执行的每一条命令特别是变量展开后的真实值。很多“我觉得应该是这样”的情况在-x输出里都会现原形——变量是空的、字符串多了个空格、路径不对等等。第三定位到具体行号。set -x配合$LINENO或者直接看报错信息里的行号然后去那行前后找原因。Shell的报错虽然有时候不够直观但行号通常是准的。第四把管道拆开逐个跑。管道是排查重灾区cmd1 | cmd2 | cmd3如果最终结果不对你不知道是哪一环出的问题。我的做法是一个一个跑先跑cmd1看输出对不对再跑cmd1 | cmd2逐步验证直到找出那个“破坏分子”。5.3 两个容易被忽略的深层坑子Shell与set -e陷阱子Shell问题我说过多次但值得再强调一遍。管道、命令替换$(...)、还有置于括号( ... )内的代码都是在子Shell里运行的。子Shell继承了父Shell的变量副本但修改不会传回去。这就导致count0 cat list.txt | while read line; do count$((count 1)) done echo $count # 输出还是0解决方式是避免在管道里做累加改用进程替换count0 while read line; do count$((count 1)) done (cat list.txt) echo $count # 输出正确另一个是set -e的“回马枪”。set -e在大部分场景很好用但它对“可能在判断中使用的非0退出码”也会直接终止脚本。比如set -e if grep -q error app.log; then echo 发现错误 fi如果grep没有匹配到内容它返回1但在if条件里这个1是预期的不会让脚本退出。但如果你写的是set -e grep -q error app.log echo 继续执行当grep没找到匹配时set -e会让脚本直接退出后面的echo根本不会执行。要临时跳过这个限制可以加上|| truegrep -q error app.log || true。这个细节掌握了能避免很多“莫名其妙就退出”的困惑。5.4 让脚本更健壮的几个小习惯写脚本经验多了以后你会发现很多“最佳实践”说白了就是让你少在半夜被叫起来处理故障。我自己的习惯清单如下每个脚本开头都加set -euo pipefail把未定义变量、管道假成功这些隐患提前扼杀。关键变量用readonly声明防止脚本执行中误修改。比如readonly BASE_DIR/opt/app后面不小心赋值时会报错能及时发现。临时文件和目录要有清理机制最稳妥的是用trap#!/usr/bin/env bash tmp_file$(mktemp) trap rm -f $tmp_file EXIT这样不管脚本正常结束还是中途报错退出临时文件都会自动清理掉。所有输出都带时间戳。写过日志的人都懂没有时间戳的日志排查问题时的价值直接减半。执行外部命令前先确认它存在。例如for cmd in curl jq git; do command -v $cmd /dev/null || { echo 缺少命令: $cmd; exit 1; } done这套检查放在脚本头部能避免跑到一半发现环境里缺少某个工具产生半途而废的烂摊子。最后的经验之谈从敲命令到写脚本最难的其实不是语法而是思维方式的转变。敲命令是“我直接下达指令系统立刻响应”写脚本是“我把指令组织成一套流程交给系统以后自动执行”。中间隔着一层“授权”——你得相信脚本能正确处理异常情况能给出足够的日志反馈能在出错时不至于造成灾难。我刚开始写脚本时最大的毛病就是把所有情况都想得太完美完全没有考虑“文件不存在”“目录没权限”“用户输入了不合法参数”这些现实情况。后来踩的坑多了才明白脚本写得健壮不健壮不在于它处理正常情况有多顺畅而在于它面对异常时能不能礼貌地告诉你“我处理不下去了、原因是什么”。如果你现在还在“敲命令”的阶段我的建议是别急着硬啃语法。你只需要做一件事下次手动敲了三条以上命令才完成一个操作时停一下把这串命令复制到一个文件里加上变量、加上循环、加上判断给它起个名字保存好。多来几次你自然就“从Shell命令到Shell编程”了。