news 2026/9/17 16:42:52

Shell脚本编程入门:从变量展开到set -euo pipefail的实用指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Shell脚本编程入门:从变量展开到set -euo pipefail的实用指南

简介:面向Linux/Unix初学者的shell编程入门资料,以单个PDF文件封装,大小约805KB。这份PDF从建立第一个脚本开始,循序渐进地讲解#!/bin/sh声明、chmod +x赋予可执行权限、变量赋值与${}取值的写法、注释规范,并系统梳理echo、ls、grep、cut、sort、expr、find、sed、awk等常用Unix命令的典型场景;同时结合管道、重定向、反引号及if流程控制等示例,演示如何组合命令完成文本处理与数据传递,帮助零基础读者理解shell脚本的执行逻辑。资源共1个PDF文件,内容紧凑、结构清晰,适合离线阅读和实操对照。目前已有51人学习下载,既可作为首次接触命令行者的入门教材,也可当作日常编写小工具时的轻量速查手册。

1. 为什么《shell编程入门.pdf》这类资料,真正的价值在“折腾”

很多人把《shell编程入门.pdf》下载到本地就当自己在学 shell 了,结果三个月后写自动化任务还是只会 ctrl+r 搜历史命令。这很正常:shell 编程的知识密度藏在“你真正用得上的那 20%”里,而这 20% 又恰恰是交互式敲命令时最不容易察觉的部分,比如变量展开、位置参数、退出码和子 shell 边界。

本文不按 PDF 章节顺序讲命令手册,而是按“先建立底层直觉,再写好能跑的脚本,最后学会看脚本为什么挂”的路径走。新手可以跟着敲,干过两三年的运维或后端也能在 ${} 和 $() 的区别、shift 用法、set -euo pipefail 这些点上带走东西。目标只有一个:合上窗口,你愿意也在自己机器上从头写一个 shell 脚本。

2. 从“敲命令”到“写脚本”:shell 脚本基础知识的一层窗户纸

2.1 你在命令行里做的每一件事,都是在“当场执行一条脚本”

shell 脚本和交互式命令之间没有本质区别。你在终端里输入ls -la | grep 'error' | wc -l,其实就是在写一个一行三段的管道脚本;shell 负责把这一行拆成三个命令,把前一个命令的标准输出接给后一个的标准输入。所谓 shell 编程入门,入门的就是搞清楚 shell 在“拆这一行”时用了哪些规则,以及哪些规则会带来让你意外的行为。

我一般让学生做的第一个练习,不是写 hello,而是打开终端逐行敲下面这组命令,然后问自己:第 4 行和第 5 行,安全边界差在哪:

echo $HOME echo "$HOME" echo '$HOME' rm -rf $HOME/backup rm -rf "$HOME/backup"

第 3 行不会删除 backup,但它会原样输出$HOME这几个字符;第 4 行如果$HOME为空,rm -rf /backup的后果谁执行谁知道。这段代码里,双引号决定了变量是否展开,决定了空格是否被当作分隔符,这是后面所有脚本安全性的地基。把它当成一行命令来看,你就在交互模式下做了一次最朴素的脚本基础知识训练。

2.2 最小可运行的 shell 脚本:文件头、执行权限与运行方式

新建一个first.sh,第一行必须是 shebang,我建议直接写#!/bin/bash而不是#!/bin/sh。Ubuntu 和 Debian 上/bin/sh实际指向 dash,它不是 bash,语法子集不同,${var,,}这种大小写转换写法在 dash 里会直接报错。脚本里凡是需要 bash 特性的地方,开头就写 bash,避免到生产环境才暴雷。

#!/bin/bash # 第一个 shell 脚本:输出当前用户和工作目录 echo "hello, $USER" echo "pwd: $(pwd)"

保存后先chmod +x first.sh,然后有两种跑法:./first.shbash first.sh。前者依赖文件头里的#!/bin/bash,由内核调用对应解释器;后者是“我手动指定用 bash 解释这个文本”,文件头可以不存在甚至写错不报错。日常调试我会用bash first.sh,衔接接下来要讲的bash -x调试模式;交付给别人的脚本则一定要把权限和 shebang 配好,否则别人执行时只会得到一个 permission denied。

2.3 位置参数与特殊变量:$0、$1、$#、$@ 的关系

脚本像函数一样可以接收参数。写一个arg.sh

#!/bin/bash echo "脚本名: $0" echo "第一个参数: $1" echo "参数个数: $#" for i in "$@"; do echo "遍历: $i" done

执行bash arg.sh a "b c" d,你会看到$#是 3 而不是 4,"$@"会保留b c这个带空格的整体。这里最关键的一个细节是"$@"$@的区别:加引号时每个参数保持独立,不加引号时参数会再次经过分词和通配符展开,遇到文件名带空格的场景就裂开。下面把特殊参数列成一张表,shell 面试题里经常考到:

符号含义典型使用场景
$0脚本名(或当前 shell 名)日志里打印来源
$1~$9第 1~9 个位置参数从命令行取值
$#参数个数校验必传参数
$@全部参数,"$@"为安全形式for 遍历
$?上一条命令的退出码,0 表示成功判断命令是否成功
$$当前 shell 的 PID生成临时文件名

注意:"$@"$*的区别在于引号内是否保留参数边界。遍历参数时用"$@",要把所有参数拼成字符串时才用"$*"

退出码这里补一个使用习惯:任何命令都有退出码,我用mkdir -p /tmp/xxx || exit 1这种写法保证失败时不会继续往下执行,而不会写完 mkdir 之后默认它一定成功。shell 编程入门阶段最划算的投资,就是把“命令失败会导致脚本继续跑”这条安全直觉练成本能。

3. 变量、引号与扩展:linux shell 里 ${} 和 $() 的区别,以及另外两个容易绕晕的语法点

3.1 变量赋值:等号两边不能有空格的真正原因

shell 里写变量的第一课通常是“定义变量时name = "x"是错的,必须写成name="x"”。这句话如果只背结论,下次遇到name= "x"还是懵。本质原因是:shell 的词法分析里,name=x是一个完整的词,解析器才能识别成赋值语句;一旦你写name = x,空格让解析器把它拆成三个词,那么name就被当成一个需要执行的命令,=x变成它的参数。反过来说,这也是“命令、变量赋值、重定向都依赖词边界”的同一个原理。

除了赋值,还有两个修饰词的细节。readonly name="x"之后这个变量不能被重新赋值,适合放配置项;unset name把变量彻底删掉,删掉后再引用就是个空字符串而不是报错。变量引用时我统一建议写成${name}而不是$name,原因是当后面紧跟字母或数字时会吞噬边界,比如:

v=hello echo "$v_world" # 空,因为 shell 找的是 v_world 这个变量 echo "${v}_world" # 输出 hello_world

这个现象叫“变量名边界吞噬”,是$var不带花括号时最常见的错误来源。写成${v}之后,shell 能明确知道变量名到哪里结束,后面接字母、数字、下划线都不会出错。这个套路在第 3.3 节会进一步延伸成子串处理。

3.2 单引号、双引号、反引号:引号决定“词”的边界

三种引号行为分别对应三种诉求。单引号内所有字符保持字面值,双引号内做变量展开和命令替换,但不再分词;反引号在旧代码里承担命令替换,现在推荐写成$(),因为反引号里嵌套$(...)或反斜杠时转义规则非常反直觉。一个容易被忽略的事实是:双引号里的$依然会被展开,所以密码、密钥这类含$的字符串要保存在单引号里,或者用反斜杠转义。

更实际的坑是“不写引号”。变量不加双引号时,它的值会被 shell 按 IFS(默认是空格、制表符、换行)做分词,还会对值里的*?[做文件名通配扩展。看下面这个片段:

dir="my docs" mkdir $dir # 实际创建了 my 和 docs 两个目录 mkdir "$dir" # 正确地只创建一个“my docs”

我见过很多人在循环里犯的错是for i in $(ls *.txt),文件名稍微带个空格,一个文件就被拆成两行处理。正确写法是for i in *.txt,让通配符展开发生在正确的位置。引号这道关口,决定了脚本是“大部分时候能跑”还是“什么时候都能跑”。三种引号的对比表如下:

写法变量展开命令替换分词典型用途
"$var"传参时保留整体
'$var'字面字符串
$var不推荐,除非明确要分词
`cmd`结果会分词旧代码,建议换$(cmd)

3.3 ${} 与 $() 的区别:一个展开变量,一个执行命令

这是 shell 面试题和日常代码评审中最容易混的一组。${var}是参数展开,作用是取出变量 var 的值,属于“取值”;$(cmd)是命令替换,作用是先执行 cmd,再把它的标准输出作为结果拼回命令行,属于“执行+取值”。两者可以组合,但注意写成"${$(hostname)}"是错的,因为参数展开的括号里必须是一个变量名,不能再放一个命令替换。常见正确用法是先取到命令结果存进变量,再展开:

host=$(hostname) # 命令替换,把 hostname 输出存入 host echo "${host}" # 参数展开,取出 host 变量的值 file="/var/log/nginx/access.log" echo "${file##*/}" # 去掉最长前缀 => access.log echo "${file%.log}" # 去掉最短后缀 => /var/log/nginx/access echo "${file//access/error}" # 全部替换 access 为 error

后三行是参数展开里的子串处理,虽然标题带“入门”,但这几个在写脚本时高频用到,值得提前掌握。${file##*/}用来从完整路径里取文件名,${file%/*}用来取目录名,这两个在打包、备份、日志处理脚本里几乎每份都会出现。注意#是删前缀,%是删后缀,一个符号表示最短匹配,双符号表示最长匹配。

提示:不用背全部参数展开用法,记下##*/取文件名、%/*取目录名、//old/new做替换,已经能覆盖大多数场景。

4. shell 脚本 for 循环的三种写法、if 判断与 shift 命令:把脚本从“命令集合”变成“程序”

把 shell 脚本从“命令集合”变成“程序”,靠的是循环、判断和函数这三样流程控制。市面上那些“shell 脚本编程 100 例”的 PDF,去掉重复后剩下的骨架也就是这三个结构;把这三者组合好,绝大多数文件批处理、备份、部署前检查任务都够用了。

4.1 shell 脚本 for 循环的三种写法:列表、命令替换与 C 风格

for 循环是 shell 脚本里出现频率最高的流程控制,三种写法分别对应不同场景。第一种是列表式,适合处理固定的枚举值:

for env in dev staging prod; do echo "deploy to $env" done

第二种是通配符展开,处理文件列表:

for log in /var/log/nginx/*.log; do [ -f "$log" ] && echo "rotate: $log" done

这里*.log如果没有匹配文件,shell 会把原样字符串*.log作为唯一一次循环传给变量,所以循环体里先用[ -f "$log" ]判断文件确实存在,避免把通配符字符串当成真实路径去操作。第三种是 C 风格,适合需要步长和索引的场景:

for ((i=0; i<10; i+=2)); do echo "index: $i" done

三种写法在面试手写题里都出现过,for i in "$@"遍历参数、for i in $(seq 1 100)造数字序列也属于列表式变体。写 for 时最常提醒的只有一个点:循环体内变量一定要加双引号,for i in "$@"而不是for i in $@,和第 3 章的分词陷阱完全一致。

4.2 if 判断与 test 表达式:文件、字符串、数字的常用条件

if 在 shell 里不是独立的语法结构,它更接近对“一条命令的退出码”做分支。if cmd; then等价于判断“cmd 退出码是否为 0”。所以if [ -f "$file" ]里的[实际上是一个名为[的命令,最后一个]只是它的参数,因此[]周围的空格都是语法的一部分:写成[-f "$file"]时,shell 会把它当成一个名为[-f的命令去查找,直接报 command not found。

单中括号[ ]和双中括号[[ ]]的区别很值得讲:[[ ]]是 bash 的关键字,里面支持&&||、正则=~,且内部不做分词和文件名扩展;[ ]是外部命令,写法更受限制。我写新脚本一律用[[ ]],同时接受[ ]在 POSIX 约束环境下的存在。条件表达式的常用项整理成下表:

表达式含义典型场景
-f "$file"文件存在且为普通文件判断配置文件是否存在
-d "$dir"目录存在建目录前检查
-x "$bin"文件存在且可执行检查依赖命令
-n "$var"字符串长度非空必填参数校验
-z "$var"字符串长度为 0判空
"$a" == "$b"字符串相等([[ ]]内)模式匹配
"$n" -gt 5数字大于,注意不是>数值比较

一个完整的参数校验片段通常是这样的,顺便看到$()>&2exit的配合:

if [[ -z "$1" ]]; then echo "用法: $0 <备份目录>" >&2 exit 1 fi if [[ -d "$1" ]]; then tar -czf "backup-$(date +%F).tar.gz" "$1" else echo "目录不存在: $1" >&2 exit 1 fi

>&2这个重定向说透:它把提示信息写到标准错误而不是标准输出,这样脚本被别的命令捕获输出时,提示信息不会混进数据流。报错往>&2走,是 shell 脚本与别的程序协作的好习惯。这段代码还把前面$()$?的退出码思路串起来了:每个可能失败的步骤,都要想好“失败之后怎么办”。

4.3 函数与 shift 命令:位置参数的消费方式让参数解析变清晰

函数在 shell 里和脚本本身共享“位置参数”这套机制。定义时先给函数体加local声明局部变量,避免污染全局:local name="$1"。函数里的$1是函数自己的第一个参数,而不是脚本的第 1 个参数,这个作用域规则刚上手时最容易写串。

shift命令每次把位置参数整体左移一位:原来的$2变成$1$#减一。它最常见的用途是手写参数解析,实现“支持一个开关、一个参数”的最小命令行工具:

#!/bin/bash verbose=0 output="" while [[ $# -gt 0 ]]; do case "$1" in -v|--verbose) verbose=1; shift ;; -o|--output) output="$2"; shift 2 ;; -h|--help) echo "用法: $0 [-v] [-o file]"; exit 0 ;; *) echo "未知参数: $1" >&2; exit 1 ;; esac done echo "verbose=$verbose, output=$output"

这里每次shift都配合 case 消费掉已处理的参数,-o因为要带走一个值所以shift 2。对于只做入门用途,手写 while+case+shift 的解析方式比 getopts 更直观,也更容易在面试时现场讲清楚。之前提过的"$@"安全遍历在这里再次生效:循环条件[[ $# -gt 0 ]]和 case 里的"$1"都不会破坏带空格的参数值。到这里,一个脚本已经拥有了变量、循环、判断、函数四个完整零件,可以称为“程序化脚本”而不是“命令集合”了。

5. shell 脚本排错三板斧:bash -x、set -euo pipefail 和三个高频坑

5.1 bash -x 展开调试,一行一行看它到底跑了什么

脚本挂掉的第一反应不要是加 echo,先跑一遍bash -x ./your.sh --参数。这个模式会输出每条命令执行前的最终形态,也就是变量展开之后的样子,行首的+表示这一行正要执行。你在输出里能直接看到变量被替换成了什么值,文件名错、分词错、参数为空基本都藏不住。只调试某一段时,用set -x开启、set +x关闭,把探针圈定在可疑区域。读取顺序是“对照输出里的展开结果,反推是哪个变量变空了或参数变多了”,而不是盯着脚本原文猜。

5.2 三个高频坑:cd 失效、/bin/sh: wq、通配符落空

第一个坑是cd在脚本里失效。脚本里执行cd /opt/xxx只会改变这个脚本进程的工作目录,脚本退出后你的终端还在原目录;若想让 cd 影响当前 shell,必须source script.sh而不是bash script.sh

第二个坑很有画面感:在 Vim 里写完脚本想保存退出,结果报了[no write since last change] /bin/sh: wq: command not found shell returned 1。这通常不是 shell 脚本的问题,而是 Ex 命令行模式下把:wq输成了:!wq:多按的一个!让 Vim 把wq当成外部命令交给 /bin/sh 去执行,自然找不到。保存退出要按:进入命令行模式再输入wq,别多按!,写 shell 脚本时顺手记住这个区分能省去不少困惑。

第三个坑是通配符没有匹配到任何文件时,*.log会以字面量参与命令。for 循环拿到*.log字符串后如果直接拿去删,后果不可预期。处理办法是循环里加[ -e "$file" ] || continue,或者先shopt -s nullglob让无匹配时展开为空,我偏好后者。

5.3 set -euo pipefail:脚本头部的一行安全配置

脚本头部建议写:

#!/bin/bash set -euo pipefail

逐个拆开:-e让任意一条命令非 0 退出时整个脚本立即退出,阻止错误继续发酵;-u让使用未定义变量直接报错,把$name打成$nmae这类拼写错误暴露出来;-o pipefail让管道中任何一个命令失败都算失败,cmd1 | cmd2不再只看最后一条命令的退出码。这三项合起来,几乎把“脚本默默出错”最关键的来源堵住了。

有一个常见疑问是set -e下如果grep pattern file没匹配到(退出码 1),脚本会直接退出吗?答案是看它出现在哪里:出现在 if 条件、while 条件、&&||的左侧时不会触发退出,因为这些位置的失败本来就是逻辑分支的一部分。理解了这条,就能解释很多“明明设置了 -e,脚本为什么还继续跑”的疑问。新写的脚本从第一行就带上这三项,比自己记住“每个命令都要判断退出码”靠得住得多。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 16:42:49

一阶倒立摆双闭环PID控制:从建模到参数整定的完整实践

简介&#xff1a;基于双闭环PID控制的一阶倒立摆系统课程设计文档&#xff0c;面向自动控制原理学习者与从事PID控制算法研究的工程技术人员。内容系统梳理了一阶倒立摆的数学建模过程&#xff0c;涵盖对象模型与线性化模型的处理方法&#xff0c;并结合SIMULINK仿真平台给出完…

作者头像 李华
网站建设 2026/9/17 16:42:32

HDC陡坡缓降触发退出与ESP标定:车速闭环、CAN诊断及热保护

简介&#xff1a;这份PDF资料围绕HDC陡坡缓降控制系统展开&#xff0c;面向汽车电子、底盘控制方向的技术人员与汽车爱好者&#xff0c;系统梳理了该项安全技术的工作原理与工程实现。内容从HDC与ABS防抱死系统的协同配合讲起&#xff0c;涵盖1997年路虎Freelander首次搭载、随…

作者头像 李华
网站建设 2026/9/17 16:39:34

数据库实时同步怎么选?从CDC原理到Oracle实战的完整选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 16:39:19

基于SSM的高校社团管理系统:权限、CRUD与审核流实战

简介&#xff1a;这份毕业设计论文文档围绕SSM高校学生社团管理系统展开&#xff0c;面向计算机相关专业的本科与高职毕业生&#xff0c;以及需要完成课程设计、论文答辩的学生开发者&#xff0c;可用于毕业设计选题参考、论文写作借鉴与技术方案学习。资源包共1个docx文件&…

作者头像 李华
网站建设 2026/9/17 16:38:48

WLTC工况能量流测试:破解电动车续驶里程优化关键路径

简介&#xff1a;本资源是一份面向新能源汽车研发工程师、高校车辆工程专业师生及动力电池系统研究人员的技术分析报告&#xff0c;聚焦WLTC工况下纯电动汽车能量流的实测建模与多层级能效评价。报告以某纯电动SUV为对象&#xff0c;在AVL转毂台架上开展WLTC循环测试&#xff0…

作者头像 李华