【动机】
常见的需求是为了减小脚本体积而去除脚本注释, 近日发现对于一些复杂的脚本, 使用 shc 工具编译脚本为可执行文件, 在去除注释前编译失败, 去除注释后能顺利编译. 如何分离瘦身脚本, 以便可用于任何 linux shell 脚本?
【意图】
将去除注释的逻辑, 写入 awk 脚本, 由 linux shell 按需调用即可.
【实现】
新建 slim.awk 文件, 写入脚本如下:
#
# 一. 作用: 给 linux shell 脚本瘦身(去除注释), 包括
# 1. 删除注释块.
# 注释块: 确定关键字, 忽略当前行到仅包含关键字一行之间的所有行, 及边界两行
# 首行格式:
# :(冒号可选) << flag this is tail...
# 特点:
# 1. 冒号左右, << 与 flag 之间, 空格数为 0-n.
# 2. 首行 flag, 如果要在其首尾或者中间包含空格, 可以用单(双)引号包围,
# 对数为 0-3. 例如:
# 1) ' Name '
# 2) ''' Name '''
# 此时要求结束 flag, 空格数要与首行对应, 关于这一点后面还有论述.
# 2. 当缺少 : 时, flag 后面不能有任何字符, 包括空格, 否则脚本自身报错
# 所以, 从首字符开始查找较快
# 注意:
# 注释块还有两种形式, 但个人感觉它们因本质上是源码而不易区分,
# 故不建议剔除:
# 1. 空命令带参数形式:
# : " 末尾注释自动生成, 无需 #
# 起始行: 冒号与(单/双)引号之间最少一个空格
# first
# second
# "
# 2. 条件满足执行, 或条件满足但忽略执行
# [ '
# hello
# world
# ' ] && echo "hey"
# [ 'wol' ]
# 2. cat 做写入文件时, 内部的任何行(包括空行, # 开头的行等)均需要保留
# 目前知道的 cat 语句分两种, 普通打印和写文件, 后者内部包含的注释不能删除, 因为它是输入内容的一部分
# 展开到一行后的格式可能如下:
# 1) cat abc.txt (作为普通代码行输出即可)
# 考虑到极端特殊情况下, 以免处理换行书写:
# cat\
# abc.txt
# 2) cat >> mnk.tso <<- AMD
# one
# tow
# # this coment is importanty
# woo
# foo
# AMD
# 标志 AMD 与 注释块的 flag 具有一致的特点, 即需要时可以包括空格. 关于这一点后面还有论述.
# 同样, 首行换行书写也已经处理
# cat\
# >>\
# mnk.tso\
# <<-\
# AMD
# 也是需要考虑的.
#
# 3) cat 的其他格式, 暂时没用到, 用到时再补充......
# 3. 调用 awk 命令时, 通过配置 awk 字符串变量 -v keep_flags, 允许指定 1 个或多个(中间用空格分隔)字符串标志
# 以此标志的行, 表示强制保留在瘦身文件中, 该变量默认为 keep_line. 这在某些仅需要在瘦身文件中才运行代码
# 或者强制保留某些注释的场景, 可能会有用.
# 详情参看 BEGIN{ } 处代码注释.
# 4. 尽可能删除代码行末尾注释
# 代码行末尾注释:
# 1. 由于涉及代码的 单引号/双引号, 引号转义, 引号嵌套, 比较复杂
# 2. 匹配正则考虑不周, 可能会删除正常使用的 # 及其以后, 例如位于多层嵌套, 附带转义的单/双引号时.
# 3. 但有一种情况, 正则表达式为: /^\s*[^ #]+#/, 为简化判断分支, 已将其合并到行处理
# 1) # 位于其他字符中间或最末尾
# 1) # 左边至少有一个空格
# 2) # 右边没有单引号和双引号
# 例如:
# echo hello "'one # \"two '' ;' "' # ' three" \\' r'\four \' " five" " # \r \n \' here, " , ' and # is very important
# 运行一遍就知道, 实际的注释应从左起第 3 个 # 开始, 按上述第 3 条的匹配原则, 则匹配第 4 个 # ,
# 有漏删, 但不会误删. 基本满足.
# 5. 删除单行注释和空白行
# 1) 由于"删除"目的是通过"忽略打印, 不调用 print"实现的, 而单行注释/空白行的删除, 依赖于
# 处理与 /^\s*(\s*|#.*)$/ 不匹配的行.
# 2) 上述正则取反, 不但匹配普通代码行(可能带末尾注释), 还可能匹配注释块和 cat 格式代码, 即与
# 注释块/cat 格式代码的匹配正则有重合的部分. 故必须将之放在最后
# 6. 保留首行 shengba 行: shengba 行
# 1) shengba 行正则与注释块和 cat 格式代码均不相容, 原则上其判断可以放在任何位置, 但为了减少纯 print 语句
# 次数, 故将其与 4 一起放在最后.
# 二. 关于一些特殊的注释块和 cat 命令写文件标志
# 1. 假设下面的 # 代表行首, 则下面的注释块虽然很怪, 但不影响脚本执行, 注意末行的标志末尾还有几个空格:
# : << """ Com me nt """ the rest words
# this is a comment content
# Com me nt
# 2. 类似 1, cat 也有相应的写法, 只不过首行标志末尾不能有多余字符, 即使是空格. 而标志是否被引号包围,
# 作用又有所不同( # 代表行首):
# cat >> $file <<- """ EOF"""
# this is content of $file
# one
# two three
# # five six.
# 123
# EOF
# 以上, 所有的奇葩注释块以及 cat 命令块, 程序均已做了兼容处理.
# 三. 其他:
# 1. echo 多行字符串, 如果其中附带 # 开头的行, 我没有做处理. 按逻辑将会被当作注释行删除. 例如:
# echo "
# # this is a comment
# echo \"hello world\"
# "
# 作为补救, 可借助于 keep_flags 变量以保留, 举例如下:
# awk -v keep_flags="echo_inner_comment" ...
# echo "
# # echo_inner_comment# this is a comment
# echo \"hello world\"
# "
#
# @include 语句目前只能从 shell 入口脚本, 而不是 awk 调用方算起, 不科学, 几乎没有存在的意义
# @include "../../library/awk/function/ttt.awk"
function debug(desc, value){
if (__RELEASE__ == 0)
printf "%s:<%s>\n", desc, value > "/dev/stderr"
}
# 为指定的注释块行或者 cat 命令行 获取用于与结尾行对应的标志字符串(可能会包含空格)
# 不是合法的块或者其他语句, 则返回空字符串
# 参数: 代码块的首行, 或者是概念上的首行(例如 cat 首行折行写, 但此处使用的整理到一行的代码)
function getflag(line){
result=""
start_idx = match(line, /<<-?/) # << 或者 <<- 在字串中的起始位置
# debug("<< 位置", start_idx)
if (start_idx != 0){
temp = substr(line, start_idx)
start_idx = match(temp, /[^-< ]/) # << 或者 <<- 之后, 第一个非空格字符的位置
# debug("<<之后第一个非空格字符位置",start_idx)
if (start_idx != 0){
result=substr(temp,start_idx)
char = substr(result,1,1)
if (char == "'" || char == "\""){
start_idx = match(result, /[^'"]/)
result = substr(result, start_idx)
end_idx = match(result, /['"]/)
result = substr(result, 1, end_idx-1)
}else{
end_idx = match(result, /[ ]/)
if (end_idx != 0){
result = substr(result, 1, end_idx-1)
}
}
}
}
return result
}
# 处理注释块或者 cat 命令块, 如果块不完整, 则报错退出
# 参数:
# flag_desc 对块的简短表述
# show 是否要显示(打印), 例如
# cat 要整块打印, 即使块内包含注释;
# 注释块要隐藏, 即使块内部包含代码
# line 首行或概念首行代码
function handle_block(flag_desc, show, line){
flag = getflag(line)
debug(flag_desc,flag)
# 隐藏范围内的注释
start_line=FNR
while($0 != flag){
success=getline
# 1:成功读取一行。到达文件末尾(EOF)。-1:发生错误。
if (success != 1){
error_msg=creat_error(""flag_desc" 标志 <"flag"> 找不到匹配的结束句,",start_line)
exit
}
if (show != 0)
print
}
}
function creat_error(msg,line){
return sprintf("%s\nLine number: %d", msg, line)
}
# 从给定的字符串, 创建用于强制保存匹配行注释到瘦身文件的正则表达式
function reg_by_keep_flags(flags){
if (flags == "")
flags = "keep_line"
debug("flag", flags)
split(flags, arr_flag, " ")
reg = "^\\s*#\\s*("
for(idx in arr_flag){
reg = sprintf("%s|%s", reg, arr_flag[idx])
}
reg = sprintf("%s)", reg)
# 第一个 | 不能要
sub(/\|/, "", reg)
return reg
}
# 参数:
# keep_flags 行首带此字符串包含的标志之一, 表示强制保持该行(去掉 # 与字符串标志后).
# 默认为 keep_line. 多个标志用空格隔开, 所以一个单独标志无法包含空格,
# 否则被当成多个标志.
# 脚本要自己保证该标志不影响脚本执行, # 号的前后可以有 0 或多个空格. 例如, 调用
# awk -v keep_flags preserved_comment -f <...> , 则对于以下注释
# #preserved_comment # comment1
# # preserved_comment # comment2
# # preserved_comment # comment3
# 甚至下面的注释也合法, 虽然可读性不好:
# #preserved_comment#comment4
# 以上四种情况, 得到的代码行分别是(在行内的具体起始位置, 由标志去除后决定):
# # comment1 和 # comment2 和 # comment3 和 #comment4
# append_remark 是否在末尾添加操作摘要(成功或失败). 注意, 如果操作失败, 控制台上总是有操作失败摘要
# 任何非空字符串, 表示添加; 其他, 表示不添加. 默认空字符串.
BEGIN{
#test()
__RELEASE__ = 1
reg_keep = reg_by_keep_flags(keep_flags)
debug("reg_keeping",reg_keep)
#exit
error_msg = ""
}{
# 注意: awk 不支持前向声明/后向声明, 故 match 定位只能从前往后一点一点定位
if ($0 ~ /^\s*:?\s*<<\s*[^< ]+\s*[^ ]+\s*.*$/){
# 注释块:
handle_block("注释块", 0, $0)
}
else if ($0 ~ /^\s*cat\s*/){
# cat 语句 用来存放可能有 \ 换行的完整的 cat 命令
# 如果 cat 是写文件, 则内部内容, 包括注释都要保护好
# 格式: cat >> mnk.tso <<- AMD (-号不一定有), >> mnk.tso 可能会在末尾
print $0
while ($0 ~ /\\\s*$/){
sub(/\\\s*$/, " ", $0)
prev = $0
getline
print $0
$0 = sprintf("%s%s", prev, $0)
}
if ($0 ~ /<<-?/){
handle_block("cat 命令", 1, $0)
}
}
else if ( $0 ~ reg_keep){
sub(reg_keep, "")
print
}
else if (NR == 1 && $0 ~ /^\s*#\s*!\s*\/bin\/bash\s*$/ || $0 !~ /^\s*(\s*|#.*)$/){
# 单行处理过程:
# 1. 首行 shengba 行, 非空行: 打印
# 2. 空白行, 即匹配正则的 | 之前半部分 /^\s*\s*$/ , 简化得到 /^\s*$/: 忽略
# 3. 代码行, 不匹配正则的剩余行(行尾可能带注释): 保守删除行尾注释(尾部做替换空字符处理而实现删除. 见说明)
# 替换只对上述 3(代码行)起作用.
sub(/\s+#[^'"]*$/, "")
print
}
}END{
cur_time = strftime("%Y-%m-%d %H:%M:%S", systime())
info = error_msg
if (error_msg == "")
info = "All comments of script are cleared."
summary = sprintf("# [%s] %s. \n# source: %s.", cur_time, info, FILENAME)
# 只要有错误发生, 则控制台必须打印
if (error_msg != "")
print summary > "/dev/stderr"
if (append_remark != "")
print summary
}
脚本有详尽注释, 在此不赘述, 基本原理是考虑存在的多种注释的同时, 又不能误删一些合理的以 # 开头的行, 例如包含于 cat 命令内的代码, 以及为精简逻辑, 调整了 if / else if / else 的顺序.
新建 compile.sh 文件, 写入函数 create_slim:
#!/bin/bash
:<<COMMENT
去除注释, 详情见 doc/compile.txt.
用法:
create_slim --source-path(-s) <=self_entry> \
--target-path(-t) <=''>
--keep-flags(-k) <='keep_line'> \
--append-remark(-a) ?
保持默认值的简单使用(静态调用方式, 仅打印到控制台):
Scripter.create_slim
COMMENT
create_slim(){
local source_path target_path keep_flags
local to_where
eval $PASX
params "'source-path s' '$(readlink -f $0)'" \
"'target-path t'" \
"'keep-flags k' keep_line"
local apd
param_appear append_remark a && apd=y
ensure_par "$target_path"
[ "$target_path" ] && to_where="$target_path" || to_where="/dev/stdout"
awk -v keep_flags="$keep_flags" -v append_remark="$apd" \
-f "$(dirname "${BASH_SOURCE[0]}")/awk/slim.awk" \
"$source_path" > "$to_where"
}
【测试】
先创建包含各种注释(包括虽然不影响脚本执行, 但可能一辈子也不会采用的奇葩注释)的 sh 代码文件 bingo.sh:
#!/bin/bash
# keep_line # calling me using keep_line
# keep_line #calling you using keep_line
: << """ Co mm ent1 """ tail tail ...
this is a comment content
Co mm ent1
miracle1(){
echo "mirocle comment1 : << 'XXX' , with tail"
}
: <<- ''' Co mm ent2 ''' tail tail ...
this is a comment content
Co mm ent2
miracle2(){
echo "mirocle comment2 : <<- 'YYY' , with tail "
}
#
# self_compile_in_slim_file# call compile self
<< ''' Co mm ent3 '''
this is a comment content
Co mm ent3
miracle3(){
echo "mirocle comment3 << ' PPP' , without tail except spaces"
}
# another_keep_line # hwo are you by another_keep
<<- ''' Co mm ent4 '''
this is a comment content
Co mm ent4
miracle4(){
echo "mirocle comment4 <<- 'QQQ' , without tail except spaces"
}
#tip "-------------------------"
: << Comment5 tail tail ...
this is a comment content
Comment5
normal1(){
echo "normal comment1 : << MMM , with tail."
}
: <<- Comment6 tail tail ...
this is a comment content
Comment6
normal2(){
echo "normal comment2 : << NNN , without tail "
}
<< Comment7
this is a comment content
Comment7
normal3(){
echo "normal comment3 : << TTT, without tail except spaces."
}
<<- Comment8
this is a comment content
Comment8
normal4(){
echo "normal comment4 : << RRR , without tail except spaces"
}
bingo(){
way1(){
local file=$ROOT_DIR_PATH/test/test.txt
ensure_par "$file"
cat >> $file \
<<- \
""" EOF """
# this is a comment, whick must not be deleted
this is a content of $file
one
two three
# five six.
123
EOF
echo " this is the first line ,
the second line
# keep_comment_in_echo # You must rely on \"--keep_flags\" in order to retain this comment
the third line
"
}
way1
# micracle testing
# miracle1
# miracle2
# miracle3
# miracle4
# # normal testing
# normal1
# normal2 # some other tail comment
# normal3
# normal4
unset -f way1
}
bingo
现在新建测试文件 test.sh, 键入以下代码:
way1(){
create_slim --source-path "$ROOT_DIR_PATH/test/lnx-code/bingo.sh" \
--target-path "$ROOT_DIR_PATH/test/slim_result.sh" \
--keep-flags "self_compile_in_slim_file keep_line
another_keep_line keep_comment_in_echo" -a
}
clear
way1
得到的最终瘦身文件 slim_result.sh 内容如下:
#!/bin/bash
# calling me using keep_line
#calling you using keep_line
miracle1(){
echo "mirocle comment1 : << 'XXX' , with tail"
}
miracle2(){
echo "mirocle comment2 : <<- 'YYY' , with tail "
}
# call compile self
miracle3(){
echo "mirocle comment3 << ' PPP' , without tail except spaces"
}
# hwo are you by another_keep
miracle4(){
echo "mirocle comment4 <<- 'QQQ' , without tail except spaces"
}
normal1(){
echo "normal comment1 : << MMM , with tail."
}
normal2(){
echo "normal comment2 : << NNN , without tail "
}
normal3(){
echo "normal comment3 : << TTT, without tail except spaces."
}
normal4(){
echo "normal comment4 : << RRR , without tail except spaces"
}
bingo(){
way1(){
local file=$ROOT_DIR_PATH/test/test.txt
ensure_par "$file"
cat >> $file \
<<- \
""" EOF """
# this is a comment, whick must not be deleted
this is a content of $file
one
two three
# five six.
123
EOF
echo " this is the first line ,
the second line
# You must rely on \"--keep_flags\" in order to retain this comment
the third line
"
}
way1
unset -f way1
}
bingo
# [2025-05-27 17:17:11] All comments of script are cleared..
# source: /mnt/d/Estate/asset/OS/linux/application/blogging/test/lnx-code/bingo.sh.
注意:
- #!/bin/bash 得以保留;
- 通过 keep_flags 定义的注释保持行得以保留, 例如为了防止误删 echo 多行字符串时, 中间可能存在的以 # 开头的文本;
- cat 命令内部包含的 # 开始的语句得以保留, 注意这并不依赖于 keep_flags 的定义;
- 最末添加两行说明(可选), 标志瘦身成功(通过 –append-remark 或 -a 选项指定).
当然还通过忽略选项参数 –target-path 和 -t , 将结果打印到控制台, 而不是生成文件. 篇幅关系, 测试从略.
谢谢收看!