拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
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'这两行特别容易搞混:双引号里的#不做注释,但双引号里的$、反引号、\依然有特殊含义;单引号里的所有字符都是字面量。写注释时如果你想把某行"临时停用",直接在前面加#最干净,别去动引号。

还有一个高频误区:#出现在${}参数展开里的时候不是注释。看这几个:

var=abcdef 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/bash:shebang,前面说过了。
  • #!开头的其他行:第二行及以后的#!就是普通注释,没有特殊效果,但有脚本作者用它写"元信息",纯粹是约定。
  • 交互式 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 shell=bash # shellcheck disable=SC2086 echo $unquoted_var # shellcheck disable=SC2046 rm -f $(ls *.tmp)

# shellcheck disable=SC2086告诉 shellcheck 忽略这一行的未加引号变量警告。用这种注释指令的前提是:注释必须写在被检查行的上一行,或者写在文件顶部作为全局声明。指令格式写错(比如#shellcheck中间少了空格)不会报错,但也不生效,这是很常见的隐性坑。

这里要提醒一句:disable只是"我知道这里有告警,是有意为之",不是"这里没问题"。滥用 disable 会让 shellcheck 的告警被大面积屏蔽,脚本质量反而下降。我的做法是每次加 disable 都在同一行末尾补一句人话说明为什么忽略,比如# shellcheck disable=SC2086 # 这里就是要让变量按空格分词。

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,检查 locale
VS Code 折叠不了注释段缺#region/#endregion加上 region 标记
shellcheck 指令不生效指令格式或位置不对放被检查行上一行,注意空格

5.2 CRLF 行尾导致的 shebang 失效

这个问题在跨平台协作里特别典型。你在 Windows 上编辑 shell 脚本,换行是\r\n(CRLF),拷到 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-8:

locale # 查看当前 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=$((i+1))这种一看就懂的不用注释,但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 世界里少有的"跨实现稳定区"。把注释这块基础打牢,再把精力放到那些真正有兼容性差异的地方,脚本的可维护性和移植性都会好一大截。

返回列表