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

资讯详情

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

Shell多行注释详解:冒号+here-doc的三种方案与避坑指南

Shell多行注释详解:冒号+here-doc的三种方案与避坑指南 1. 为什么Shell没有原生块注释先看清这门语言的脾气1.1 从C、Python转过来的人几乎都会在注释上卡一次很多从C、Java或者Python转过来写Shell脚本的人第一周都会问同一个问题Shell里到底怎么给一段代码加多行注释我在这行混得久了类似的疑问在公司技术群里见过不下十次每次的回答都指向同一个事实——Shell这门语言在设计上就没有提供块注释。C有/* ... */Python有三引号Java有/* ... */到了Bash/KSH这里只剩一个#而且只认到行尾。你试着写#!/bin/bash /* 这是一段注释 */ echo helloBash会直接甩给你一句command not found: /*。原因不复杂Shell的语法规则里每个词都可能是一条命令/*也不例外。Shell在词法阶段就把#开头的内容丢掉了但并没有为块注释预留任何语法入口。这不是Bash的疏忽而是历史惯性。最早的Bourne Shell面对的场景是短小精悍的命令组合一行一个意图#足够用了。等到脚本动辄几百上千行、需要临时屏蔽大段逻辑的时候大家才发现手边没有趁手的工具。于是这几十年里脚本社区沉淀出了一套用语法零件拼凑注释的民间方案核心就两个零件冒号命令:和here-doc。你说它优雅吧它确实是个hack你说它脆弱吧它又在所有POSIX兼容Shell里稳定运行了几十年。这篇文章就把这套方案的原理、写法、坑和Bash/KSH兼容性一次讲透。1.2 冒号命令与here-doc注释方案的两块拼图先认识第一个零件冒号命令:。它是Shell内置命令唯一的作用就是什么都不做然后返回状态码0。你可以给它传参数、给它重定向输入输出它一概不管。正因为永远返回0在set -e的脚本里拿它当注释载体也完全安全不会因为返回非零让脚本中途退出。再看第二个零件here-doc。它的标准写法是cat EOF 这里的内容会作为标准输入交给cat EOFEOF的意思是从下一行开始一直读到一行内容恰好是EOF为止把这段文本整体作为命令的标准输入。EOF本身只是个定界符你可以换成任意字符串只要开头和结尾一致。把两个零件拼在一起就得到了多行注释的标准形态: COMMENT 从这一行起到COMMENT为止 这些内容都会被原样交给冒号命令。 冒号命令不读标准输入也不做任何事 所以这里写什么都不会影响脚本逻辑。 COMMENT:收到了一个巨大的标准输入然后直接无视它。外层脚本的解析到此为止注释块里的任何东西都进不了执行流程。这就是用语法结构模拟注释功能的全部原理。理解了这一点后面所有坑的来龙去脉都顺理成章了。注意这里的核心不是:而是带引号的定界符COMMENT。引号决定了注释块里的内容是按字面处理还是会被Shell二次展开这里面可藏着大文章。2. 三套主流注释方案原理、写法与应用场景2.1 方案A冒号加单引号定界符90%场景的主力先看一个完整的实际例子。我写部署脚本时文件头部经常这么用#!/bin/bash # 本脚本用于发布前端静态资源到测试环境 : SCRIPT_DESCRIPTION 功能清单 1. 拉取最新代码产出环境号与提交时间 2. 执行 npm run build 并校验产物 3. 推送至 Nginx 静态目录保留最近5个版本 注意发布前必须确认当前分支是 release 分支 且本地没有未提交的改动。 SCRIPT_DESCRIPTION set -euo pipefail echo 开始发布...重点在于定界符带了单引号SCRIPT_DESCRIPTION。带引号后注释块里的内容是完全字面的$HOME不会被展开成路径$(date)不会被当成命令执行反引号包起来的命令同样原地不动特殊字符比如$、\、、全都按原样保留。这正是注释该有的样子我写进去的是给人看的说明文字不是给Shell跑的代码。如果不加这个引号后果我们在第3节细说这里先记住结论——只要是拿here-doc当注释定界符必须用单引号包起来。2.2 方案Bif false包裹临时屏蔽代码块时优先有时候你注释的不是文档而是一大段正在调试的代码。比如你怀疑某段逻辑有问题想先不执行它又不舍得删这时候if false方案最顺手if false; then echo 这段不会执行 deploy --force notify --channel danger fi echo 正常流程继续false这个命令永远返回非零所以then后面的代码块永远不会进入执行分支。它的好处是你只需要把false改成true整段代码就立刻复活来回切换非常方便。调试完如果确认废弃再整体删除也不心疼。这里顺带回答一个很多人问过的点Bash的if语句必须有else子句吗答案是不需要。if 条件; then 命令; fi是完整的合法语法else是可选的这正好让if false; then ... fi成为合法的永不执行块。但用这个方案有个致命前提块里的代码虽然不执行却必须要通过语法解析。Shell的解析发生在执行之前if false挡得住执行挡不住解析。你往里面塞一句if [[这种语法残缺的东西Bash照样报syntax error。所以这个方案适合屏蔽语法完整的代码块如果要屏蔽的是乱七八糟的调试草稿请回到方案A。2.3 方案C逐行#注释最笨但最稳回到最朴素的方案每行前面加#。它没有任何魔法任何Shell版本下都不可能出问题# echo hello # if [ -f /tmp/x ]; then # cat /tmp/x # fi手写当然累但编辑器早就帮你搞定了。VSCode里选中多行按Ctrl/Vim里用可视模式加shift #都能快速批量注释和取消。命令行层面也有现成的批处理手段。假设要屏蔽script.sh的第10到30行# 注释 sed -i 10,30s/^/#/ script.sh # 取消注释 sed -i 10,30s/^#// script.sh一行命令的事而且sed是POSIX标准工具老KSH环境里也一样能用。逐行#最大的价值在协作场景。它没有定界符、没有隐藏状态任何人用grep一搜就能看到所有注释的位置合并代码时冲突也最直观。代价是视觉上比较吵大段注释全是#号铺底扫起来费眼睛。2.4 三套方案对比与我的选型建议把三套方案放在一起看方案语法形式内容是否参与解析内容是否会被执行适合场景方案A: EOF否否文档说明、任意文本、代码示例方案Bif false; then ... fi是必须语法合法否临时屏蔽完整代码块方案C逐行#否否日常小段注释、正式仓库代码我的选择习惯是文件头部的设计说明、技术备注、使用文档一律方案A调试阶段的大段代码临时下岗用方案B真正提交进仓库的正式代码回归方案C。方案A虽然好用但定界符本身有状态团队里有人不熟悉反而容易制造幽灵注释方案B则容易让人忘记清理解析约束。另外还有个小众写法值得一提用:加单引号做单行注释比如: 这是一条注释。它本质上就是把注释文本当作一个单引号参数丢给空命令。单行场景下能用但文本里一旦出现单引号就要拆分转义实用性不如老老实实打#。3. 定界符引号和注释内容多行注释的坑几乎全在这里3.1 定界符不加引号你的注释块会复活并执行这是多行注释踩坑频率排行榜的第一名而且后果可能是灾难性的。把方案A里的单引号去掉写成: COMMENT 当前时间$(date) 当前目录$PWD COMMENT表面上看起来没什么区别实际上$(date)会被真实执行$PWD会被展开成真实路径。如果注释块里恰好躺着一条$(rm -rf $HOME)这种命令脚本一旦跑起来你以为的注释会瞬间变成执行现场。这个坑的原理是定界符是否被引用决定了here-doc内容的处理模式。定界符不带引号时Shell会对正文做参数展开、命令替换和算术展开定界符带引号时正文完全按字面处理。是的引号只加在定界符上就足以改变整段文本的性质这是很容易忽略的Shell语法细节。还有一个隐蔽版本脚本里启用了set -u未定义变量直接报错时不带引号的here-doc如果引用了不存在的变量Bash在读取注释的阶段就会直接崩溃#!/bin/bash set -u : COMMENT $UNDEFINED_VAR COMMENT # 报错bash: UNDEFINED_VAR: unbound variable你根本没打算执行这段内容它却因为一段注释里的变量名不存在而中断整个脚本。这就是定界符引号的重要性——把注释当注释写永远带上单引号。重要不要用EOF不带引号的形式写任何注释。命令替换会执行变量会展开set -u会误报这三个雷里随便踩一个都够你查半天。3.2 注释内容里混入与定界符一样的行第二个高频坑是定界符冲突。Shell的here-doc读取规则很简单看到起始定界符后逐行扫描一旦遇到一行内容和定界符完全相同就认为文档结束了。假设你写了: NOTE 这里解释用法 EOF是常见约定很多工具用它作为结束标记 NOTE echo 脚本继续如果注释正文里恰好出现了一个独立成行的NOTE比如你原本想举例说明定界符不要选NOTE这种词: NOTE 教你一个技巧定界符使用NOTE没问题 NOTE # 这一行一旦和定界符完全一致… 这里实际上是脚本代码了会报错 NOTE结果就是here-doc在第一个NOTE行就提前终止后面所有内容都被当成真正的Shell代码解析报错信息五花八门command not found、syntax error near unexpected token根本看不出是注释的问题。破解方法有两个。第一定界符选得足够独特。用_COMMENT_2024_、DOC_END_01这种带上下划线和大写字母的组合正常正文里撞车的概率几乎为零EOF、END、COMMENT这种通用词反而是重灾区。第二写完注释后用grep -n ^定界符$ script.sh扫一遍确认正文里没有独立成行的同名行。3.3 Git Bash和Windows编辑器的CRLF换行符一个隐藏炸弹在Windows上用Git Bash写脚本的人迟早会遇到一个诡异现象同一个多行注释在Linux服务器上一切正常在本地Git Bash里却报here-document at line 1 delimited by end-of-file (wanted ...)。这个问题十有八九是CRLF换行符干的。Windows的记事本、部分IDE默认用\r\n作为行尾而Linux/Git Bash按\n解析。于是你的结尾定界符行实际内容是COMMENT\rShell认的是COMMENT\r是多余字符永远匹配不上here-doc一路扫到文件末尾最后报unexpected end of file。排查方法很简单用cat -A看文件的真实内容cat -A script.sh | tail -5 # 如果看到 COMMENT^M$ 这样的输出^M就是CRLF的残留修复同样简单把行尾统一换成LFsed -i s/\r$// script.sh # 或者用 dos2unix script.sh然后在编辑器里把行尾设置固定为LF一劳永逸。Git Bash本身没有任何问题它就是个标准的Bash 4/5运行环境真正作妖的是Windows编辑器的行尾策略。3.4 单引号、双引号、无引号定界符三种行为对照最后把这三种定界符写法做个系统对照你自己写的时候对照着选定界符写法参数展开$VAR命令替换$(cmd)算术展开$((11))典型后果COMMENT执行执行执行危险禁止用于注释COMMENT不执行不执行不执行安全标准注释写法COMMENT不执行不执行不执行效果等同单引号\COMMENT不执行不执行不执行反斜杠转义也算引用单引号、双引号、反斜杠转义这三种写法在展开行为上是等价的只要定界符被引用正文就原样保留。我统一推荐单引号因为它最直观一眼就能看出这里是被引用状态。如果配合-使用还有个缩进细节-允许正文行和结尾定界符前面带TabShell会把这些Tab剥掉再匹配但空格不行。想缩进美观地写注释块必须用Tab键。: -INDENTED 这两行的Tab会被剥掉排版好看些 INDENTED这个特性在KSH和Bash里行为一致但团队里其他不熟悉的人容易踩空格和Tab的混用坑所以我个人还是建议定界符顶格写少用-。4. Bash与KSH兼容性同一段注释在不同Shell下的实地测试4.1 为什么这套方案是公认的可移植写法先说结论:加here-doc的多行注释不是Bash的私有技巧而是POSIX标准口径下所有兼容Shell都能用的公共方案。POSIX对Shell语法的定义里:是必须存在的冒号实用程序是必须存在的here-doc重定向定界符引号禁用展开的规则也是标准明确规定的。这意味着从Linux默认的dash到老古董Bourne Shell的现代继承者再到各路KSH变体只要实现遵守POSIX这套注释写法就成立。标题里为什么专门说Bash/KSH因为它们是日常交互和脚本开发里最常见的两个阵营。Bash是Linux发行版的事实标准KSH在AIX、Solaris这类企业Unix环境里有着庞大存量很多老运维的肌肉记忆都在KSH上。两个阵营语法大体兼容但细节差异不少多行注释这个点恰好是两边都能安稳落地的少数玩法之一。4.2 在bash、ksh93、mksh、dash上的实测记录我写了个最小测试脚本分别用三种注释方案跑一遍然后在不同Shell下验证#!/bin/sh # 方案A : COMMENT 这一段是方案A的注释内容 $HOME 不会被展开 $(echo 这行不会执行) COMMENT # 方案B if false; then echo 这行不会执行 fi # 方案C # echo 这条注释不会执行 echo OK: 三种注释方案全部通过分别在bash 5.1、ksh93、mkshMirBSD Korn Shell、dash上的执行结果Shell方案A方案B方案Cbash 5.1通过通过通过ksh93u通过通过通过mksh R59通过通过通过dash 0.5.x通过通过通过连最精简的dash都认这套写法说明它在绝大多数生产环境里不会有兼容性危机。至于ksh88它的here-doc行为和ksh93一脉相承同样可用pdksh系也遵循同一标准。真要说差异不在注释本身而在你注释内容里藏了什么语法——比如[[是KSH/Bash特有dash不认$(( ))算术展开POSIX认但老版本ksh88对嵌套写法支持有限。但这些都是注释块内部的话题只要定界符带单引号注释内容根本不进入解析器壳的差异影响不到它。4.3 比语法差异更常见的环境差异实际项目里更常见的在这台机器正常在那台机器报错往往不是注释语法本身的问题而是环境配置差异。比如说KSH的交互环境受ENV环境变量控制很多用户的~/.kshrc里会开set -u或定义一堆别名而Bash则是~/.bashrc。这些启动文件只影响交互式Shell不影响非交互式脚本。但脚本自己如果写了set -u再配上没加引号的here-doc注释就会触发3.1里说的unbound variable错误。同一个脚本在没开set -u的旧服务器上跑得好好的换到新环境里加了这行注释那里立刻炸。Git Bash也是同理。它内置的是Mingw环境下的Bash语法行为跟Linux上的Bash完全一致兼容性差异几乎都来自Windows侧的外在因素——CRLF行尾、路径分隔符、权限模型。所以遇到Git Bash下多行注释报错先查行尾再查定界符基本就能定位。5. 延伸一步从多行注释到多行字符串与模板生成5.1 用$(cat EOF)生成多行配置内容多行注释的本质是把一段不执行的文本喂给空命令。反过来想如果把这段文本喂给能捕获输出的地方你就得到了多行字符串——这正是Shell里字符串换行符问题的标准解法之一。我经常在脚本里这样拼配置nginx_conf$(cat CONF server { listen 80; server_name example.com; root /var/www/html; location / { try_files $uri $uri/ 404; } } CONF ) echo $nginx_conf /etc/nginx/conf.d/site.conf注意这里定界符CONF带单引号所以模板里的$uri不会被提前展开写入文件时还是字面量$uri交给Nginx运行时去解析。如果你希望内容里的变量被当场展开就改成不带引号的定界符——这是模板输出模式不是注释模式两者的区分就是定界符引号和前面讲的完全是一套逻辑。除了heredocShell本身也允许单引号字符串跨行msg第一行 第二行 第三行这种写法简单场景够用但一旦内容里有单引号就要转义多行模板一般还是heredoc更顺手。5.2 注释一段本身包含here-doc的代码嵌套的边界条件工作里有个高频需求把一段旧代码整体注释掉而旧代码里恰好包含heredoc。很多人到这里就慌了其实嵌套是允许的。看这个例子: OUTER 这段是注释。旧代码原来长这样 cat INNER hello world INNER 现在整段都被OUTER注释掉了。 OUTER外层定界符是OUTER带单引号于是从起始OUTER到结束OUTER之间的一切都是字面文本。内层的INNER行只是普通文本不会触发任何解析。嵌套成立的前提只有一个内层内容里不能出现与外层定界符完全一样的独立行。所以外层定界符一定要选独特词别跟内层的常见EOF、END撞车。顺带一提注释掉一个包含重定向的代码块时同理只要外层带引号内层全部是文本不用做任何转义。这是方案A相对方案B的又一个优势方案B要求块内语法完整而方案A连残缺的代码片段都能吞下去。5.3 在注释块里放代码示例时引号选择决定展示效果写脚本自带的帮助文档时我经常需要在注释块里放一段示例用法。这里对引号的选择就得非常清楚: HELP 示例 deploy.sh --env prod --tag v1.2.3 HELP用单引号定界符示例里的$、反引号、管道符全部原样展示这正是文档需要的效果。反过来如果你写的是动态帮助文本希望打印时带上真实的当前配置值那就用无引号定界符配合cat输出cat HELP 当前版本$VERSION 部署目录$DEPLOY_DIR HELP同为here-doc一个喂给:当注释一个喂给cat当输出差的只是定界符引号和目标命令。把这两个场景在脑子里分开你就彻底掌握here-doc的精髓了。6. 三年来我在脚本里沉淀的注释习惯附排查清单6.1 我最终固定下来的三条场景规则这么多方案、这么多坑走下来我现在的注释策略非常简单就三条。第一文件头部的说明文档用方案A。脚本名、功能摘要、依赖环境、调用方式用一次: DOC包起来干净整齐也不污染grep搜#的结果。定界符我固定用DOC或SCRIPT_DESC这种词并且保证正文里绝对不会出现独立成行的同名行。第二调试期间的临时屏蔽用方案B。if false; then ... fi切换执行状态只需要改一个词比批量加#快得多。但有个纪律调试结束前必须把这块清理掉。false块如果混进正式版本后人看着一团迷雾完全无法判断这是有意为之还是忘了删。第三提交进仓库的正式代码用方案C。正式代码里的注释要么是紧贴代码的单行#要么是文件头部的方法A文档。大段if false和诡异的here-doc注释块在多人协作的仓库里不是好公民——定界符状态看不见摸不着合并冲突时极易出错。另外还有个习惯任何heredoc注释写完立刻补上结尾定界符。手快先写起始行然后直接跳一行写结尾再回头填中间内容这样永远不会有写了起始忘了结尾的半成品状态。6.2 脚本突然报错时的多行注释快速排查链如果你现在正在对着一个诡异的Shell报错挠头怀疑是多行注释闯的祸按下面五步走定界符加引号了吗没加引号的话注释里的$()或反引号可能被真实执行。用bash -n script.sh做语法检查再全文搜一遍把所有不带引号的heredoc都改掉。结尾定界符有隐藏字符吗用cat -A script.sh | tail看结尾行如果出现^M就是CRLF残留sed -i s/\r$// script.sh处理后重跑。结尾定界符是顶格写的吗标准heredoc要求结尾定界符独占一行且行首无空格用了-则允许Tab但不允许空格。缩进风格不一致最容易让Shell扫不到结尾。正文里是否出现了与定界符相同的独立行grep -n ^COMMENT$ script.sh扫一遍。如果撞了把定界符改成更独特的词比如_MY_DOC_END_。脚本是否启用了set -u启用且定界符没加引号时注释里的未定义变量会直接报错。加了引号就没有这个问题这也再次说明单引号是注释的唯一正确姿势。哪怕你完全不懂heredoc原理按这个清单走一遍绝大多数注释引发的灵异事件都能定位。6.3 最后再分享一个小技巧写到这里补充一个我踩过多次坑后固定下来的习惯全局搜索with检查脚本里所有here-doc的定界符状态。执行grep -n script.sh然后逐个确认每个heredoc是有意展开模板生成还是应该引用注释/字面输出。这条检查我已经写进了自己的验收流程每次提交脚本前跑一遍省掉了无数线上事故。多行注释在Bash/KSH里不是一门高尚的语法它就是个实用主义的补丁。但正因为Shell社区几十年没有官方解法这套补丁才值得被彻底研究清楚——知道它为什么能跑知道它会在哪里翻车你才算真正驾驭了Shell脚本而不是被一长串不明所以的EOF牵着鼻子走。
返回列表