ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Shell 单行与多行注释:语法、实现、编辑器批量操作与踩坑排查

Shell 单行与多行注释:语法、实现、编辑器批量操作与踩坑排查 Linux 下的 shell 注释单行和多行其实是两套完全不同的逻辑单行注释简单到一个#就能解决多行注释却因为 shell 解释器压根没有提供原生块注释语法只能靠几种土办法绕路实现。这件事看着小但凡写过几十行以上脚本的人都会撞上——临时屏蔽一段循环调试、给函数写一段说明、在团队脚本里留个改动记录全都要靠注释。可一旦处理不好比如 here-document 的分界符没顶格、注释行末尾多了个反斜杠、Windows 编辑过的脚本带回车符脚本报的错会让你找半天。这篇就把 shell 单行注释和多行注释从语法规则、实现方式、编辑器批量操作到踩坑排查完整讲一遍。不管你是刚开始看 shell 脚本入门教程的新手还是已经能熟练摆弄 for 循环、shift 处理参数、${}和$()切换的老手这里应该都有能直接抄走的东西。1. 先把 shell 注释的底层逻辑搞明白1.1 注释在解释器眼里到底是怎么消失的shell 执行脚本分几个阶段读取输入、做分词token 化、识别特殊字符、展开变量和命令替换、执行。注释是在分词阶段就被判定并整段丢弃的它不会进入后续的展开和执行环节。这件事的意义在于注释里的内容不参与变量展开、不参与命令替换、不会被执行你可以放心地把$HOME、$(date)、反引号、引号这些东西写进注释里它们不会被当成代码跑起来。但这里有个反直觉的点——不执行和不被解析是两码事。有些多行注释的写法后面会讲的if false那种虽然不执行内容但内容依然要经过语法解析只要里面有语法错误脚本照样报错退出。理解这条边界基本就理解了一半的多行注释坑。shell 之所以把注释设计得这么抠门和它的出身有关。它诞生于上世纪七十年代的 Unix 环境那时候的设计哲学是工具只做一件事越简单越好脚本是用来自动化命令的胶水不是用来写大型程序的。所以它给了你一个极简的单行注释剩下的都交给你自己组合命令去解决。理解了这层背景你就不会纠结为什么别的语言有/* */shell 没有了。1.2 单行管一行多行为什么要绕路单行注释的规则一句话就能说完#出现的位置如果在一个词的开头那么从这个#到行尾全都是注释。注意是词的开头不是任意位置。所以echo a#b会原样输出a#b因为#夹在词中间不算注释起点而echo a #b里#b前面有空格#b就是一个新词从它开始被当注释丢掉最终只输出a。这个细节是 shell 面试题里出现频率不低的一类考点。多行注释则完全没有官方语法。你想屏蔽五行的内容shell 不提供/* */这种块注释。于是社区演化出四种主流替代方案连续写单行#、用: EOF的 here-document 技巧、用if false; then ... fi包起来、以及用: ...单引号包一大段。这四种方案没有绝对的好坏只有适用场景不同后面会逐个拆。对于只写几行临时脚本的人来说多行注释可能一辈子用不上但对于维护几百行以上的运维脚本、构建脚本、部署脚本的人来说选错写法带来的维护成本是实实在在的。这也是为什么 shell 脚本基础知识里注释语法虽然排在前几页但真正写起工程来细节比想象中多。1.3 注释规范背后其实是维护成本很多人对注释的态度是代码写清楚就行注释无所谓这话在个人小脚本里成立在团队协作里就未必。shell 脚本有个特点它大量依赖外部命令和全局状态cd一下当前目录就变了export一个变量整个环境都受影响set -e、set -u一开行为全变。这类隐式上下文是代码本身表达不出来的只能靠注释说明。一个成熟的 shell 脚本注释习惯通常包括这么几块文件头的用途说明和用法示例、每个函数的入参和副作用说明、那些看起来多余但删了会出事的操作的原因标注比如某个sleep 1到底在等什么、以及被临时屏蔽掉的大段逻辑。这几块里文件头和函数说明主要靠单行注释堆叠临时屏蔽则经常需要多行注释。所以你会发现单行注释是日常主食多行注释是临时工具。把这两件事分开看你的脚本注释风格会清晰很多。2. 单行注释# 背后的完整规则2.1 # 到底从哪个位置开始生效再复述一遍核心规则并补充边界#只有在作为词的首字符时才开始注释。具体判断标准是它前面必须是空白空格、制表符、行首或者是命令分隔符;、、|、(等控制符之后的位置。下面这几种情况值得单独拎出来echo hello # 这里的 # 前面有空格# 及其后是注释 echo hello#world # 输出 hello#world# 不是注释起点 echo hello ;# 拼接写法;# 这里 # 前是控制符算注释 echo a # b # 引号内部# 只是普通字符原样输出 echo a # b # 单引号同理原样输出 echo a\ #b # 转义后的空格后面 #b 依然是独立词被注释echo a # b和echo a # b这两行特别容易搞混双引号里的#不做注释但双引号里的$、反引号、\依然有特殊含义单引号里的所有字符都是字面量。写注释时如果你想把某行临时停用直接在前面加#最干净别去动引号。还有一个高频误区#出现在${}参数展开里的时候不是注释。看这几个varabcdef echo ${#var} # ${#var} 是取字符串长度不是注释 echo ${var#abc} # ${var#abc} 是从左边删掉匹配 abc 的最短部分 echo $# # $# 是位置参数个数和注释无关${#var}和${var#pattern}里的#以及$#都属于特殊参数语法shell 在分词时已经把它们当作整体 token 处理了不会被误判成注释。这三个是新手最容易看花眼的地方$#和${#var}之间差了一个大括号含义天差地别。2.2 shebang 是注释里唯一的例外脚本第一行常见的#!/bin/bash或#!/usr/bin/env bash习惯上叫 shebang。它长得像注释确实是注释——对 shell 解释器来说它会被当注释忽略但在你直接用./script.sh方式执行脚本时操作系统的exec系列调用会读第一行的#!据此决定用哪个解释器来跑这个文件。这里要分清两种执行方式的差异./script.sh脚本有可执行权限内核读 shebang调用指定的解释器。bash script.sh显式指定解释器shebang 那一行就是纯粹的注释被完全忽略用哪个解释器由你在命令行指定。这个区别在排查问题时特别有用。比如你写了个依赖 bash 特性的脚本第一行却写成#!/bin/sh本地 Ubuntu 上/bin/sh指向 dash脚本里的[[ ]]、数组、echo -e行为就会跟你预期不一样报一些莫名其妙的错。这时候把执行方式改成bash script.sh能立刻验证是不是 shebang 的问题。shebang 还有个要求#!必须是文件的前两个字节前面不能有任何空行、空格或 BOM。有些编辑器保存 UTF-8 时加了 BOM或者文件是 Windows 的 CRLF 行尾都会让 shebang 失效具体报错长什么样在后面第 5 节展开。2.3 注释行末尾的反斜杠最容易埋雷shell 里反斜杠\是转义字符出现在行尾表示续行把下一行接到当前行一起解析。这个机制遇到注释行会出怪事。看下面这段echo before # 这是一行注释 \ echo after echo end在 bash 里实测你会发现echo after也被一起吃掉了只有before和end被打印。原因就是行尾那个\把下一行拼了上来而下一行被并进注释里去了。不同 shell 实现对这个行为的处理细节略有差别但结论是一致的注释行的末尾不要随手留反斜杠。这个坑在日常写脚本时不算高频但在复制粘贴场景里很常见。比如你从别处粘一段多行命令进注释原命令每行以\结尾你只把第一行加了#后面几行就会被悄悄吞掉调试时能让人怀疑人生。稳妥做法是要么把整段都加#要么把结尾的\全删掉。2.4 那些看着像注释其实不是的东西最后区分几个形似注释的家伙避免混淆#!/bin/bashshebang前面说过了。#!开头的其他行第二行及以后的#!就是普通注释没有特殊效果但有脚本作者用它写元信息纯粹是约定。交互式 shell 的#提示符root 用户的命令提示符常配成#那是提示符不是注释。$#、${#var}、${var#pat}参数语法前面讲过了。#[或# ]在某些工具配置里是注释标记但那是那些工具自己的语法shell 不管。把这些和注释分清楚再加一条注释行末尾不留反斜杠你的单行注释基本不会出问题。3. 多行注释四种落地方式逐一拆解3.1 连续单行注释最朴素也最稳这是最没有技术含量但最可靠的方式每一行前面都加#。# 下面这段是旧逻辑暂时保留 # for f in *.log; do # gzip $f # done优点很明确任何 shell 都支持不受解释器差异影响内容里可以随便出现单引号、双引号、反引号、EOF、fi这些容易冲突的东西编辑器批量加注也方便第 4 节会讲。缺点也很明显临时屏蔽一大段时手动加#麻烦取消注释更麻烦尤其是内容里有空行和嵌套结构的时候容易漏行。这里有个小经验批量加注释之后匹配的结束行也要记得加上#否则后面可能出现半个块被注释、半个块还在跑的状态报错定位反而更困难。另外值得注意的一点连续单行注释是唯一一种内容可以完全不合法语法的方式——你把一段语法错误的代码整段加#脚本照样能跑因为它压根不进解析阶段。3.2 here-document 配合冒号工程里最常见这是 shell 多行注释里流传最广的写法: EOF 这一整段都不会被执行 变量 $HOME 不会被展开 命令 date 也不会执行 中文、单引号 双引号 都可以放 EOF拆开看:是 shell 的一个内建命令功能就是什么都不做返回成功。EOF是 here-document 重定向语法表示把后面直到单独一行EOF之前的所有内容作为标准输入喂给前面的命令。因为前面的命令是什么都不做的:所以这段内容就被完整读走然后丢弃了效果等同于注释。分界符为什么要加单引号这是整个写法的关键。EOF带单引号是不展开模式内容里的$变量、$(命令)、反引号都当字面量处理而EOF不带引号是展开模式内容里的变量会被替换、命令替换会真的执行。想象一下你在注释里写了一句示例$(rm -rf /tmp/xxx)用了不带引号的 here-doc这段命令就真跑了。所以写多行注释时分界符务必带上单引号EOF是标准姿势。分界符必须独占一行且顶格或只用 tab 缩进这是最常踩的坑。如果你用-EOF这种带减号的写法允许分界符行用制表符缩进注意是制表符不是空格。很多人编辑器里一按 Tab 插的是四个空格分界符加空格缩进后 shell 认不出来就会一路往下找EOF直到文件末尾都没找到报出类似warning: here-document at line N delimited by end-of-file的警告然后整段后续代码全被吞进 here-doc。这个坑我在第一次写部署脚本时踩过排查了半天。3.3 if false; then ... fi方便但有个大坑第三种写法长这样if false; then echo 这段不会执行 for i in 1 2 3; do echo $i done fi因为条件false永远不成立then和fi之间的内容永远不会被执行。这种写法看起来很像其他语言的块注释阅读上也自然不过它有个必须知道的坑这段内容依然会被语法解析。shell 在执行if语句之前要先把它解析成完整的语法树所以只要then ... fi里有语法错误括号不配对、引号没闭合、done写成了fi整个脚本直接报语法错退出根本到不了执行阶段。换句话说连续#和 here-doc 可以注释掉坏代码但if false不行它只能用来屏蔽语法正确但暂时不想跑的代码。调试时如果你想把一段报语法错的代码先停掉用if false反而会继续报错得改用前面两种方式。另外这个写法的优势是不用管分界符也不用处理缩进和EOF冲突的问题内容里可以随便出现单引号、EOF字样只要不破坏if结构。所以它适合包裹结构完整、语法正常的逻辑块。3.4 冒号加单引号一行搞定的临时方案第四种是用冒号命令配单引号参数: 这一段被单引号包起来作为参数传给 : 命令 $HOME 不会被展开 但是这个区块里不能出现单引号字符 原理是:忽略所有参数所以单引号里那一大段作为参数被读入后直接丢弃。它的好处是写起来短开头结尾各一行就行不用关心分界符是否顶格。缺点也很突出内容里不能出现单引号。要放单引号得拆开字符串或用其他转义手段一旦内容里混进一个后面的内容就会提前结束字符串、跑到命令解析阶段报出各种奇怪的语法错。所以这个方式我一般只在临时演示、给一段不含引号的示例文本加注释时用正式脚本里更愿意用 here-doc。3.5 四种方式横向对比与选型建议把四个方案摆在一起看会更清楚方式典型写法内容是否被解析内容可含任意字符主要限制推荐场景连续单行注释每行前加#否是加/取消麻烦易漏行长期保留的说明、语法有误的代码here-document: EOF ... EOF否是分界符行除外分界符须顶格或 tab 缩进临时屏蔽大段、含命令示例的说明if falseif false; then ... fi是仅解析否必须是合法语法语法错误仍会报错屏蔽语法正确的完整逻辑块冒号加单引号: ... 否否不能含单引号内容不能有单引号短文本、临时演示选型上我自己的习惯是这样的长期存在的注释用连续单行#配编辑器批量操作临时屏蔽大段代码用 here-document分界符统一用COMMENT或EOF并顶格需要屏蔽的是语法正常的完整块、又懒得管分界符用if false:加单引号基本只当快速草稿用。一个容易被忽视的经验是here-doc 的分界符最好选一个内容里绝对不会出现的词。有人习惯用EOF结果注释内容里正好有一行EOF示例注释就提前结束了。换成__COMMENT__或BLOCK_END这类不常见标识能省去不少麻烦。4. 编辑器里的批量注释与折叠实战4.1 Vim / Neovim 下的批量加注与取消真正在服务器上改脚本Vim 出场频率很高批量注释是必备技能。最通用的方式是用:命令配合替换:2,15s/^/#/ 给第 2 到 15 行行首加 # :2,15s/^#// 反向操作去掉行首 # :,s/^/#/ 可视模式选中后加注释可视模式选中一段后按:命令行会自动补上,直接输入s/^/#/回车即可。批量取消注释用s/^#//但要注意它会把所有以#开头的行都改包括原本的注释。更精准的做法是s/^\([ \t]*\)#/\1/只去掉行首注释符号并保留缩进。喜欢插件的话vim-commentary提供了非常顺手的映射可视模式选中后gc即注释再gc取消。它懂不同语言的注释规则在 shell 里就是加#用熟了比敲:,s/^/#/快很多。另一款NERD Commenter也类似映射是\cc加注释、\cu取消。还有一种纯手工但很直观的方式可视选中后按I大写 i进入行首插入敲一个#然后按Esc。Vim 会把#应用到选中的每一行。取消就选中后按d删除行首那个字符或者用x。这个方法不需要记正则新手也能立刻上手。4.2 VS Code 的注释切换与 region 折叠在 VS Code 里Ctrl /macOS 是Cmd /是切换注释的快捷键。选中多行再按就是逐行加#。注意 shell 语言本身不提供块注释所以 VS Code 不会给你加/* */它是老实给每一行加#这也是为什么上面第 3 节的连续单行注释方式需要编辑器帮衬——手工加太累编辑器一键就完成。关于多行注释折叠这是个常被问到的点VS Code 默认不认识 shell 里的注释块它不会把连续几行#识别成可折叠区域。想折叠大段注释可靠的办法是用 region 折叠标记#region 旧部署逻辑保留备查 # ... 一大段注释 #endregion#region和#endregion配成一对VS Code 会在行号旁出现折叠箭头点一下就能把整段收起来。这个方法对注释和正常代码都有效是管理长脚本的实用技巧。还有个细节VS Code 打开 shell 脚本时右下角会显示当前语言模式。如果它把.sh文件识别成Shell Script而不是Shell部分语法高亮和折叠行为会有差异。手动点右下角切一下语言模式注释折叠相关功能的体验会更正常。4.3 shellcheck 注释指令让注释参与静态检查注释除了给人看还能给工具看。shellcheck 是 shell 脚本静态检查里绕不开的工具它能通过特定格式的注释指令来按行或按脚本调整检查规则#!/bin/bash # shellcheck shellbash # shellcheck disableSC2086 echo $unquoted_var # shellcheck disableSC2046 rm -f $(ls *.tmp)# shellcheck disableSC2086告诉 shellcheck 忽略这一行的未加引号变量警告。用这种注释指令的前提是注释必须写在被检查行的上一行或者写在文件顶部作为全局声明。指令格式写错比如#shellcheck中间少了空格不会报错但也不生效这是很常见的隐性坑。这里要提醒一句disable只是我知道这里有告警是有意为之不是这里没问题。滥用 disable 会让 shellcheck 的告警被大面积屏蔽脚本质量反而下降。我的做法是每次加 disable 都在同一行末尾补一句人话说明为什么忽略比如# shellcheck disableSC2086 # 这里就是要让变量按空格分词。4.4 shfmt 与格式化时的注释保留shfmt 是另一个常用工具用来统一 shell 脚本的缩进和排版。它对注释的处理总体是保留的但有个细节行尾注释和行首注释的位置可能被重排某些情况下注释会被挪到不同行的位置。所以如果你的注释跟它所在的行强相关比如解释某个参数为什么这么写格式化之前最好先看一眼 diff确认注释没被挪错位置。调用方式一般是shfmt -w -i 4 script.sh # 4 空格缩进直接改写文件 shfmt -d script.sh # 只输出差异不改文件-i 4表示用 4 个空格缩进团队里最好统一风格。格式化和注释之间的配合原则是让注释独立成行不要老挂在代码行尾。行尾注释在多行命令、管道、续行场景里很容易被重排或错位独立成行则稳定得多。5. 常见问题与踩坑实录5.1 常见问题速查表把注释相关的高频问题整理成一张表方便对照排查现象可能原因排查与解决注释后面几行代码没执行注释行末尾有\吞掉了下一行删除行尾反斜杠或整段加#脚本报 here-document 警告分界符没顶格或用空格缩进分界符顶格或改用 tab -多行注释内容里的命令真的执行了用了EOF而非EOF分界符加单引号禁用展开if false里语法错误仍报错内容会进语法解析阶段改用连续#或 here-doc./script.sh报 bad interpreterCRLF 行尾或 BOM 导致 shebang 失效转成 LF去掉 BOM注释里中文显示乱码文件编码非 UTF-8 或终端编码不符统一 UTF-8检查 localeVS Code 折叠不了注释段缺#region/#endregion加上 region 标记shellcheck 指令不生效指令格式或位置不对放被检查行上一行注意空格5.2 CRLF 行尾导致的 shebang 失效这个问题在跨平台协作里特别典型。你在 Windows 上编辑 shell 脚本换行是\r\nCRLF拷到 Linux 上执行时报bash: ./script.sh: /bin/bash^M: bad interpreter: No such file or directory注意报错里那个^M就是回车符\r。原因在于内核读 shebang 那一行时把/bin/bash\r整个当成解释器路径这个路径当然不存在。解决方式是用dos2unix转换或者用 sed 批量去掉回车符sed -i s/\r$// script.sh顺便提一句文件压缩解压相关的乱码问题从 Windows 打包的文件解压到 Linux 后中文文件名乱码通常也是编码不一致GBK 与 UTF-8导致的处理思路是解压时指定编码或用convmv转文件名编码这和脚本本身的注释乱码是两类问题但底层都是编码不统一。5.3 中文注释乱码与文件编码写中文注释时最省心的做法是把脚本文件统一保存为UTF-8无 BOM并确认终端 locale 支持 UTF-8locale # 查看当前 locale echo $LANG # 通常应是 xx_XX.UTF-8如果文件是 UTF-8但终端 locale 是C或POSIX中文注释在某些命令输出里就会变成问号或乱码。脚本本身执行通常不受影响因为注释不参与执行但当你用cat、grep、less查看脚本时会很难受。把LANG和LC_ALL设成.UTF-8结尾的值即可。不要在脚本文件里混用编码有人从别处粘一段 GBK 的中文注释进来编辑器不提示保存后整个文件编码就乱了。稳妥办法是编辑器统一设成 UTF-8 保存粘贴时如果出现乱码说明源内容编码不同先转换再粘。5.4 一些实操心得关于注释的取舍我自己的几条原则是第一注释解释为什么代码表达是什么。i$((i1))这种一看就懂的不用注释但sleep 2 # 等数据落盘后再读这种必须写否则半年后你根本不知道为什么要有这两秒。第二多行注释的写法优先选 here-doc分界符统一成不常见的词。我现在固定用: __BLOCK__配__BLOCK__收尾几乎不可能和内容冲突。第三临时屏蔽用注释不要用删除。调参数时把旧值注释掉保留在旁边比直接删掉、事后从 git 里翻要快得多。但也别留太久——积累几十行注释掉的历史代码会让脚本变得难读定期清理是必要的。第四给脚本加个变更记录注释块用 here-doc 或者连续#都行写上日期、改动人、改动原因。这在多人维护的部署脚本里价值极高比任何 git blame 都直观。第五批量注释前先存盘。编辑器批量替换正则写错比如s/^/#/里把#敲成了别的字符一整段代码全被改花这时候有备份能救命。Vim 里u撤销、VS Code 的本地历史都能兜底但前提是你保存过。最后说个容易被忽略的点shell 脚本的注释规则在绝大多数解释器bash、dash、zsh、ksh里是一致的单行#的规则通用here-doc 和:命令也是标准内建。也就是说你把注释写规范了脚本在换个解释器跑的时候基本不会因为注释出问题。真正会因解释器不同而踩坑的是[[ ]]、数组、echo -e这些执行逻辑层面的差异注释反倒是 shell 世界里少有的跨实现稳定区。把注释这块基础打牢再把精力放到那些真正有兼容性差异的地方脚本的可维护性和移植性都会好一大截。
返回列表