尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Shell编程入门:从变量、循环到批量重命名实战

Shell编程入门:从变量、循环到批量重命名实战 简介面向Linux初学者的shell编程入门资料系统讲解从脚本创建、权限设置到变量使用、命令调用与流程控制的核心知识适合刚接触Bash或希望规范编写自动化脚本的读者。资源包仅含1个PDF文档大小805KB内容精炼、便于离线阅读覆盖echo、ls、grep、sed、awk等常用命令以及管道、重定向和反引号等实用技巧并配有简单示例和注释习惯说明。已有51人学习下载对于想快速建立shell编程整体框架的用户而言是一份轻量且实用的起手材料。通过学习可以掌握编写可执行脚本的基本流程理解环境变量与普通变量的区别并能够用条件判断、循环等流程控制语句组合出常见运维场景下的自动化工具。1. shell 编程入门从哪开始先把命令看成可组合的零件很多人把 shell 编程入门当成学一门新语言背语法、记关键字结果一个多月下来还是只会对着终端敲命令。其实 shell 编程的核心不是语法而是“如何把一堆现成的命令组织成一套可控的流程”。你在 Linux 上日常用的cd、ls、grep、awk单独执行都没问题可一旦需要重复跑 100 次、跨目录处理文件、按条件分支再手动敲就不现实了。把命令写进一个.sh文件配上变量、循环和判断这就是 shell 编程。它适合做运维、后端和测试的人在 macOS 或者 Windows 的 WSL 里基本同样适用。建议先不着急背语法先把“命令如何被拼接、展开、执行”这条线想清楚后面遇到的坑大多都能从这条线推出来。2. 变量、引号与通配符shell 脚本基础知识里最容易混的一组2.1 ${} 与 $()取值与取输出的差别shell 在真正执行一条命令之前会先对整行做展开这是理解 shell 脚本基础知识的前提。${}是变量展开取出某个变量的值$()是命令替换把命令的标准输出当作字符串插到当前位置。两者完全不是一回事但刚入门时几乎都会混。#!/bin/bash nameworld echo hello, ${name} today$(date %F) echo today is $today count$(( 3 4 )) echo count is $count这段代码里${name}取的是变量name的值输出hello, world。$(date %F)先执行date %F再把输出2025-01-01之类的结果赋给变量today。第三行的$(())是算术展开做整数计算结果赋给count。面试题里经常考三者的区别尤其是$(...)和反引号...的异同。反引号是老写法在脚本里碰到嵌套时非常难读建议新代码一律用$()这个习惯能省掉不少排错时间。# 不推荐反引号嵌套转义很绕 outecho \date %F\ # 推荐命令替换清晰可读 out$(echo $(date %F))这里顺便提示一下花括号不是每次都必须写。$name和${name}在大部分场景等价但当变量名后面紧跟其他字符时花括号就是必需的${name}_suffix写成$name_suffix会被解析成变量name_suffix结果就错了。2.2 单引号、双引号和反引号的三种行为引号在 shell 里不是装饰它直接决定哪些字符会被展开、哪些按原样输出。单引号内部所有字符都变成字面量双引号内部变量和命令替换仍然生效反引号本身是命令替换不用于引用字符串。home/root echo ${home} and $(date) echo ${home} and $(date %Y) echo date %Y第一行输出原样的${home} and $(date)因为单引号禁止一切展开。第二行输出/root and 2025双引号允许$和$()展开但不把空格拆成多个词。第三行用了反引号虽然也能执行命令但嵌套和可读性都不如$()。写法变量展开命令替换常见用途...不展开不展开传通配符、特殊字符给程序...展开展开拼接字符串、带空格的参数...展开执行并插入输出旧脚本兼容建议替换成$()需要注意双引号内的$仍然会被展开所以如果你想在awk这种程序里传一个$1给远端命令往往要混合使用单双引号。最典型的是这种awk {print $1}整个过程必须保持$1不被外层 shell 吃掉。遇到这类问题先静态判断“这一层引号内谁在解释”而不是急着加转义。2.3 通配符不是正则别混用shell 的命令行里可以直接写*.log这种模式由 shell 在展开阶段把匹配到的文件名替换进去。注意它和正则表达式完全不是一个东西*在这里表示任意长度字符串而在正则里表示重复前一个字符。很多人在这上面吃亏把find的正则写法直接套进ls或者反过来。# 通配符匹配只展开当前目录下以 .log 结尾的文件 for f in /var/log/*.log; do echo found: $f done # 没有匹配时的默认行为保留字面量 ls *.log一个容易踩的点是如果没有匹配到任何文件*.log会原样保留for循环仍然会执行一次拿到的值就是一个带星号的字符串。ls *.log此时会报出“No such file or directory”这类报错在 shell 面试题里也出现过。常用做法是在脚本开头写shopt -s nullglob让没有匹配时直接展开成空串循环体就自然不执行了。shopt -s nullglob for f in /var/log/*.log; do echo found: $f done如果你要对文件名做更复杂的匹配比如“只删掉 7 天前包含日期的日志”建议直接find配合-regex而不是硬凑通配符。通配符做文件匹配find -regex做路径正则两者选型看场景偶尔也会组合使用。3. 控制流与位置参数for 循环、shift 与常见坑3.1 shell 脚本 for 循环的三种写法shell 脚本 for 循环是一个绕不开的基础点写批处理、批量重命名、轮询接口都会用到。它大致有列表写法、C 风格写法和结合while read按行读取三种套路各自适用的场景不同。# 写法一列表写法值不多时最直观 for env in dev test prod; do echo deploy to $env done # 写法二C 风格次数已知时用 for ((i 1; i 5; i)); do echo round $i done # 写法三按行读取文件适合处理命令输出 while IFS read -r line; do echo read: $line done servers.txt第一种写法里for后面的列表来自手工输入、变量展开或者命令替换都可以。第二种是纯粹按次数循环适合生成序号。第三种严格说不是for但实际处理文件列表时比for更稳妥因为read按行读取不会因为文件名里有空格而裂开。三个写法要按数据来源取舍而不是看哪个顺眼用哪个。如果是把find的结果喂给while read强烈建议用-print0配合read -d 处理文件名中的空格和换行。下面这段代码在实际生产里很常见也是 bash 脚本里处理脏文件名的标准姿势while IFS read -r -d file; do echo 处理: $file done (find /data -type f -name *.log -print0)read -d 表示以空字符作为行结束符find -print0用空字符分隔文件名整个链路就把换行、空格、中文路径都安全地传过来了。如果没有做这个处理一个名为a b.log的文件会被拆成两个条目后续的rm可能误删文件。3.2 shift 让脚本像标准命令一样消费参数位置参数$1、$2、$3在脚本里代表传入的命令行参数。写一个只接受固定参数的小脚本可以硬编码$1和$2但稍微像样一点的工具脚本都要支持可选参数这时候shift就是最基础的解析手段。它把参数列表整体左移原来的$2变成$1配合case可以逐段处理。#!/bin/bash force0 dir while [ $# -gt 0 ]; do case $1 in --dir) dir$2 shift 2 ;; -f|--force) force1 shift ;; *) echo unknown option: $1 2 exit 1 ;; esac done echo dir$dir, force$force这段脚本用while [ $# -gt 0 ]判断参数是否处理完case根据当前$1的值决定怎么做。--dir需要额外吃一个值所以拿到$2之后shift 2-f是布尔开关只shift 1。*分支处理无法识别的参数报错后退出。shift的价值在于让脚本更像标准命令可读性和可维护性都比重用$1加一长串条件判断好很多。对于比较复杂的参数解析也可以用标准库getopt但需要额外注意版本差异我一般更愿意手写case加shift因为行为完全可控不依赖外部命令的具体实现。如果你想套用别人的框架至少要能看懂shift在这中间做了什么。3.3 shell 中常见坑空格、子 shell 与 if 的判断条件shell 的语法很敏感空格位置不对就完全变样这类问题在“shell 中常见坑”讨论里出现频率最高。攒几个常见场景当作对照清单排查时能快很多。现象原因快速处理if [ $x yes ]报错$x为空时展开成[ yes ]语法不对称加引号写成[ $x yes ]count 1报 command not found等号两边有空格被当成命令执行等号两侧不能有空格管道里的while变量外部取不到管道两侧命令在子 shell 中执行用while ... done (cmd)cd /nope rm -rf .没执行cd失败后rm也没执行脚本继续往下走加set -e或判断cd返回值没有匹配文件的*.log原样出现通配符没有展开shopt -s nullglob第一个问题最经典[其实是一个命令后面每个参数都要用空格隔开所以$x至少要加双引号。第二个是新手写赋值最常见的错误两边不能有多余空格否则 shell 会把count当成命令名。第三个涉及子 shell管道|会把左右两侧都放进子 shell 执行循环里修改的变量不会传回当前进程这种问题很难靠肉眼看出来。一个比较稳妥的做法是直接在脚本开头加set -euo pipefail。-e让任何一条命令非零退出就立刻失败-u使用未定义变量直接报错pipefail让管道中任一命令失败都导致整体失败。这三个选项配上之后很多静默错误会变成显式报错比出问题再猜要高效得多。4. 实战用 shell 重命名文件并处理脏文件名4.1 选择重命名工具mv、rename 还是自写循环用 Linux shell 重命名文件有几种常见做法mv适合单个或少量文件rename适合批量规则替换自写循环则适用于带业务逻辑的重命名。rename命令在不同发行版上有两套实现一套是 Perl 版支持正则替换另一套是 util-linux 版只做固定的字符串替换两者参数差异很大普通脚本里贸然依赖rename可能会在另一台机器上直接失效。我一般保留mv作为核心操作外面包一层循环控制逻辑。这样无论到哪台机器只要有 bash 和mv就能跑不依赖额外的命令版本差异。批量重命名的本质就是“对一批文件逐条构造目标路径然后执行移动”。流程不复杂但要重点考虑目标文件已存在、目录不存在、文件名含特殊字符三种情况。4.2 一个带 --dry-run 的最小批量重命名脚本下面给一个完整脚本批量给指定目录下的.log文件加上前缀同时支持--dry-run先预览再动手避免一次性改错。#!/bin/bash set -euo pipefail dir. prefixbackup dry0 usage() { cat 2 EOF 用法: $0 [--dir 目录] [--prefix 前缀] [--dry-run] 示例: $0 --dir /var/log --prefix backup --dry-run EOF exit 1 } while [ $# -gt 0 ]; do case $1 in --dir) dir$2 shift 2 ;; --prefix) prefix$2 shift 2 ;; --dry-run) dry1 shift ;; *) echo 未知参数: $1 2 usage ;; esac done if [ ! -d $dir ]; then echo 目录不存在: $dir 2 exit 1 fi while IFS read -r -d old; do base$(basename $old) new$dir/${prefix}-${base} if [ -e $new ]; then echo 跳过: 目标已存在 $new 2 continue fi if [ $dry -eq 1 ]; then echo mv $old $new else mv -- $old $new fi done (find $dir -maxdepth 1 -type f -name *.log -print0)脚本先用while和case解析参数得到目标目录、前缀和 dry-run 模式。参数处理完后检查目录是否存在然后进入核心循环。find加上-print0保证了文件名里的空格、换行不会破坏解析read -r -d 按空字符读取二者配合是 bash 里处理脏文件名最可靠的方式。拼接新文件名时用[ -e $new ]检查目标是否已经存在存在就跳过不给mv覆盖的机会。mv -- $old $new里的--告诉mv后续参数不要被当成选项解析万一文件名以-开头也不会出问题。--dry-run模式下循环只打印mv命令不实际执行方便你先看一遍再决定是否去掉这个参数运行。4.3 参数表与安全护栏这个脚本只有三个参数但足够说明问题。以后扩展成支持递归、支持正则替换也只需要在这个结构上加分支。参数默认值作用--dir.指定扫描目录--prefixbackup拼到原文件名前的前缀--dry-run关闭只输出将要执行的命令不真实移动安全护栏有两个核心点一是开头set -euo pipefail任何一个阶段失败都会直接退出避免半路出错后继续往下跑二是dry-run预览和“目标已存在就跳过”这两条能挡住绝大多数误操作。如果在生产环境用我还会再补一条先find统计文件数超过某个数量就二次确认。这种保护逻辑成本低但在批量改名这种不可逆操作上多一道确认从来不亏。5. 验证脚本质量的三个手段shellcheck、set -x 与临时目录5.1 先用 shellcheck 做静态检查写好的脚本不要急着拿真实目录验证先过一遍 shellcheck。它是一个静态分析工具能检查出未引用的变量、无意义的cat、cd后未判断结果等问题。安装方式和普通系统包一致macOS 上也可以直接装。 对刚入门的脚本shellcheck 的提示几乎每一条都值得看尤其是 SC2164cd前未检查这类与生产稳定强相关的告警。shellcheck rename.sh如果脚本是跟着文章里的例子写的运行时应该没有明显告警。有告警也没关系逐条看输出里的行号和说明改完再跑一次。静态检查能过滤掉大量低级问题让后面的调试集中在真正的逻辑错误上。5.2 打开 -x 和 -u 看清执行过程静态检查通过后如果有运行期问题用bash -x逐步查看展开结果。-x会在执行每条命令前打印展开后的命令配合PS4可以显示行号。PS4 [$LINENO] bash -x ./rename.sh --dir /tmp/sample --prefix backup --dry-run输出里能看到每个变量展开后的实际值比如dir是不是真的等于/tmp/samplebase提取的是否完整。把某个分支看不清楚时可以在脚本里临时export PS4 $BASH_SOURCE:$LINENO 方便区分命令来源。调试完毕后记得把临时加的set -x和PS4删掉或注释掉避免影响正常运行日志。5.3 在临时目录里搭最小测试环境最后把所有准备工作集中到临时目录验证用mktemp -d创建独立环境配合trap在脚本退出时自动清理。这个方法也适合其他所有 shell 脚本的回归测试是我在实际环境里反复使用的技巧。tmpdir$(mktemp -d) trap rm -rf $tmpdir EXIT cd $tmpdir || exit 1 touch 2025-01-01.log 2025-01 02.log bash /path/to/rename.sh --dir $tmpdir --prefix backup --dry-run先创建带空格的文件名再跑 dry-run确认输出里的路径都带着引号且完整随后去掉--dry-run真正执行一次看最终目录里的文件列表是否符合预期。trap ... EXIT保证无论脚本正常结束还是中途失败临时目录都会清掉不给自己留垃圾。这套组合做完脚本才算真正达到可以放到生产环境的标准。本文还有配套的精品资源点击获取
返回列表