V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Cloudpods
V2EX  ›  推广

记一次符合 Google Coding Style 的 Bash 脚本重构

  •  
  •   Cloudpods · 2023-05-25 15:07:58 +08:00 · 2351 次点击
    这是一个创建于 578 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近我在思考这样一个问题,顺便看一下 gpt 对这个问题的解释。搜索发现:

    团队写代码,为什么要遵循 coding guideline ?

    1. 一致性:编码准则确保整个团队的代码风格和格式是一致的,这使得团队成员之间更易于交流和协作。这也使得代码更容易维护和更新,因为其他人可以更轻松地理解代码。

    2. 可读性:编码准则有助于提高代码的可读性。通过对代码进行格式化和注释,代码会更易于阅读和理解,从而减少错误和缺陷,并提高代码的质量。

    3. 可维护性:编码准则有助于提高代码的可维护性。通过使用一致的格式和风格,团队成员可以更轻松地理解代码,并进行更快速的故障排除和修复。

    4. 代码质量:编码准则有助于提高代码的质量。通过要求团队成员编写可读性高、可维护性强的代码,编码准则可以确保代码更加健壮、可靠和安全。这也有助于减少代码缺陷和错误,提高代码的可靠性和可用性。

    5. 代码重用:编码准则有助于提高代码的重用性。通过编写符合标准的代码,团队成员可以更容易地将代码重用在其他项目中,从而提高生产力和效率。

    继续搜索:

    对于一个使用 bash 为主要语言的项目,业界推荐的 coding guideline ?

    1. 使用有意义的变量名:变量名应该具有描述性,以便其他人可以轻松地理解代码。

    2. 缩进:使用 4 个空格作为缩进,而不是制表符。

    3. 注释:对代码进行注释,以解释代码的目的和功能。注释应该清晰、简洁,并且易于阅读。

    4. 函数:编写短小精悍的函数,每个函数都应该具有一个特定的目的。函数应该使用有意义的名称,并且应该清晰地描述其目的和功能。

    5. 错误处理:在代码中包含错误处理,以确保代码在遇到错误时不会崩溃或产生不可预测的结果。

    6. 使用双引号:在引用变量时,使用双引号而不是单引号,以确保变量扩展能够正常工作。

    7. 使用全局变量:尽量避免使用全局变量,因为它们很难调试和维护。

    8. 使用命令替换:使用$()语法而不是反引号 ``来执行命令替换。

    9. 使用数组:使用数组来管理相关变量,而不是使用多个单独的变量。

    10. 使用“set -u”:使用“set -u”来检查未定义的变量,以避免在引用未定义的变量时产生错误。

    这些规范有助于提高 Bash 代码的可读性、可维护性和可靠性。

    然后我搜索 "bash script style guideline",最上面的结果是:

    image-20230430140433698

    即:代码规范: https://google.github.io/styleguide/shellguide.html

    我仔细阅读了这份风格指南,对其中的“局部变量”的章节很感兴趣。

    文中说:「最好把局部变量的定义与赋值,换行实现,不要写到同一行上」,以免掩盖报错状态码。

    原文

    Declare function-specific variables with local. Declaration and assignment should be on different lines.

    Ensure that local variables are only seen inside a function and its children by using local when declaring them. This avoids polluting the global name space and inadvertently setting variables that may have significance outside the function.

    Declaration and assignment must be separate statements when the assignment value is provided by a command substitution; as the local builtin does not propagate the exit code from the command substitution.

    我动手验证这个细节,发现果然如此:

    image-20230430103921020

    然后我开始自查当前的项目,寻找类似于如下风格的代码:

    local my_var="$(my_func)"
    

    优化后的预期结果:

    local my_var
    my_var="$(my_func)"
    

    https://regex101.com/ 测试代码的运行。给出范例

    regex:  
      local fn=$(echo $name_ver| tr ':' '-').tar.xz
    test string
      local fn=$(echo $name_ver| tr ':' '-').tar.xz		#普通
        local fn=$(echo $name_ver| tr ':' '-').tar.xz	# 模拟多个空格
    	local fn=$(echo $name_ver| tr ':' '-').tar.xz		# 模拟 tab 缩进
    	local fn="$(echo $name_ver| tr ':' '-').tar.xz" # 模拟带引号的变量声明
    

    测似乎生成的代码

    $1local $2\n$1$2=$3
    

    生成的代码

    $re = '/^(\s*)local\s+(\w+)=("?\$\(.*)/m';
    $str = '  local fn=$(echo $name_ver| tr \':\' \'-\').tar.xzt
        local fn=$(echo $name_ver| tr \':\' \'-\').tar.xzt
    	local fn=$(echo $name_ver| tr \':\' \'-\').tar.xz
    	local fn="$(echo $name_ver| tr \':\' \'-\').tar.xz"';
    $subst = "$1local $2\n$1$2=$3";
    
    $result = preg_replace($re, $subst, $str);
    
    echo "The result of the substitution is ".$result;
    

    精简为 perl_oneliner:

    image-20230430103045113

    perl -pe 's/^(\s*)local\s+(\w+)=("?\$\(.*)/$1local $2\n$1$2=$3/g' -i file.txt
    

    测试的场景:

    搜索代码

    pcregrep -lr '^(\s*)local\s+(\w+)=("?\$\(.*)' *

    批量修正:

    perl -pi -e 's#^(\s*)local\s+(\w+)=("?\$\(.*)#$1local $2\n$1$2=$3#' $(pcregrep -l -r '^(\s*)local\s+(\w+)=("?\$\(.*)' * )
    

    修正之后,仔细阅读diff,检验效果,发现符合预期。

    后续:增加 git hook 检测代码

    为了让以后新增的代码,也都符合上述规范,我增加了这样一个 pre-commit脚本。这样,每次提交之前,它都会帮我确保代码合规。

    同时,我在编辑器里,设置了 shfmt 、shellcheck 之类的规范,并设置为format on save,即,保存时自动格式化,来自动处理格式问题。

    # test code 
    if ! grep -wq 'Code violates rules' .git/hooks/pre-commit; then
    cat >> .git/hooks/pre-commit <<'GIT_PRE_COMMIT_EOF'                                                                                                                                        
    #!/usr/bin/env bash
    if find . -name '*.sh'| xargs pcregrep '^\s+local\s+\w+="?(`|\$\()'; then
      echo "Error: Code violates rules"
      echo 'use: local var'
      echo 'var="$(...")'
      echo 'instead of local var=``'
      echo 'or local var="$(...)"'
      echo 'as of explained in https://google.github.io/styleguide/shellguide.html'
      exit 1
    fi
    GIT_PRE_COMMIT_EOF
    chmod +x .git/hooks/pre-commit
    fi
    
    

    总结:

    • 寻找业界规范
    • 遵循规范
    • 修改过去不合规范的代码
    • 新增代码确保合规
    • 将代码的规范检查,加入到日常的流程里。( goimport check)
    • 越早做,历史包袱越少。越晚做,历史包袱越沉重。

    links

    原文地址: https://www.yunion.cn/article/index.html

    28 条回复    2023-05-26 08:56:32 +08:00
    aloxaf
        1
    aloxaf  
       2023-05-25 17:40:21 +08:00
    1. 文中提到的问题,shellcheck 就能检查,为啥要自造一串正则表达式来处理……
    2. 自定义 lint rule ,推荐轻量易用的 semgrep: https://semgrep.dev/playground/s/E2Jo

    rules:
    - id: declaration-assignment
    patterns:
    - pattern: local $A=$B
    - pattern-not: local $A="..."
    message: Declaration and assignment should be on different lines.
    languages: [bash]
    severity: WARNING
    fix: |
    $A
    $A=$B

    (不过 semgrep 的 autofix 一直有点问题,比如上面的规则不能正确处理第二行的缩进
    james122333
        2
    james122333  
       2023-05-25 19:46:35 +08:00 via Android
    这东西可以 但功效比 hack 语法更小 纯用 bash hack

    我已经这样写

    #!/bin/bash

    source init_file module_path

    ns test #namespace

    import {
    core/str #default namespace in file
    rand2 core/rand #namespace rand2
    }

    import core/ini
    import num2 core/num

    fn run a b=1 @c @d=^Rabc @e:key::node f...
    bgn
    err $0: error
    echo ok
    end

    rec abc a
    bgn
    return a=1 b=2 c=3
    end

    const {
    var1=a
    var2=b
    }

    const var3=c

    a 没指定 b 如果带入为空则为 1 c 为 pass by reference 的参数 d 同 c 但有预设值 e 为一结构体结构类型为 key f 剩余参数
    函数内 err 后 返回错误 echo ok 不执行
    rec 是定义结构 回传阵列 参数方法与 fn 同
    const 指定常数

    当然不开源 XD
    james122333
        3
    james122333  
       2023-05-25 19:56:39 +08:00 via Android
    为何 fn 不用 block({})呢

    因为可以
    if [ ${abc} -eq 1 ] ; then
    fn abc a=1
    else
    fn abc b=1
    fi
    bgn
    ....
    end

    猫熊万岁 XD
    mohumohu
        4
    mohumohu  
       2023-05-25 20:01:24 +08:00
    local 甚至不符合 POSIX sh 标准
    james122333
        5
    james122333  
       2023-05-25 20:04:30 +08:00 via Android
    @mohumohu

    然而如果你只用 posix sh 会很难维护大型 shell 专案
    效能也差
    james122333
        6
    james122333  
       2023-05-25 20:06:52 +08:00 via Android
    一堆项目内的 configure 脚本很烂就是这样
    autotools 完全是悲剧
    mohumohu
        7
    mohumohu  
       2023-05-25 20:26:12 +08:00   ❤️ 1
    @james122333 用 shell 的初衷就是为了跨平台跨系统兼容,真要大型效能就用 python 去了
    james122333
        8
    james122333  
       2023-05-25 20:33:55 +08:00 via Android
    @mohumohu

    现在哪个发行版还在 posix sh 所以兼容没问题
    python 太臃肿 shell 单执行档就好了 光论效能 shell 是垫底的 但 posix sh 是最底的 但很多任务效能需求不高 用 shell 写可以 但 posix sh 就会明显卡顿 技术原理问题
    james122333
        9
    james122333  
       2023-05-25 20:49:16 +08:00 via Android
    差点忘了 csh 也是效能最差的 语法最神奇的 会出神秘现象
    mohumohu
        10
    mohumohu  
       2023-05-25 21:19:33 +08:00   ❤️ 1
    @james122333 很多嵌入式设备只有 busybox ,没有 bash ,而且都跑 shell 了,我不觉得需要有什么效能的差别,shell 核心都是调用别的程序完成任务,像 grep ,sed ,能效跟 shell 本身关系不大。
    nuk
        11
    nuk  
       2023-05-25 21:21:42 +08:00
    用数组限制太大了,至今都没什么机会用上,sh 主要就是写一些安装脚本,复杂一点我会用 lua ,table 基本上可以解决一切需要存储的地方,string.match 能解决大概 90%的正则需求,关键是非常可靠而且安全。大概十年前见过一个台湾生产的路由器,进去看才发现里面所有的页面都是用 shell 实现的 cgi ,当时震惊到我了。
    mohumohu
        12
    mohumohu  
       2023-05-25 21:25:04 +08:00
    @james122333 你真要这么说,一堆 docker 常用的发行版就是没 bash 的,比如 alpine
    nuk
        13
    nuk  
       2023-05-25 21:30:09 +08:00   ❤️ 1
    用 shell 实现太容易出现注入漏洞了,几年前的 360 路由器第一代,我逆向固件后,发现其中一个一百行不到的 shell 实现的 cgi 都有注入漏洞,可以直接远程执行命令,而这仅仅是因为 escape 字符串不到位而已,还有 ruckus 路由器也有注入漏洞,我拿来解除国家码限制。这都还是会考虑到安全因素的路由器,所以用 shell 写东西不难,难的是要全面的考虑到安全。
    james122333
        14
    james122333  
       2023-05-25 21:42:39 +08:00 via Android
    @mohumohu

    即便你嵌入式 bash 还是比 python 小 至于 bash 与 posix sh 当然有效能差异 例如你说的 local 没有不是得多加判别不然就是写的很小心 还有简单数值计算 你用 expr 每次都得启动程序一次怎么无关效能 读档案每次调用 cat? 当然有差 grep sed 命令堆叠是最伤效能的 configure 就是一例 没有这些 啪 一下就出来的 这些命令一次处理大量内容才会有效能优势
    james122333
        15
    james122333  
       2023-05-25 21:46:48 +08:00 via Android
    @mohumohu

    因为这些发行版以最简化为准则 跑的也是其它语言写的大型程序 当然 shell 什么的不重要 但其实这些东西很强大的
    swulling
        16
    swulling  
       2023-05-25 21:49:08 +08:00 via iPhone
    为啥不用 shell 的 lint 工具,比如 shellcheck 。

    难道别的语言的 lint 工具都自己写么……
    james122333
        17
    james122333  
       2023-05-25 21:52:50 +08:00 via Android
    @nuk

    bash 有关联数组 其实还好 也有正则 但其实不怎么用正则 通常有更好实现 我都是自己研究自己搞 没看过别人用 cgi
    注入漏洞我这写好的函数没有 当然我自己用
    mohumohu
        18
    mohumohu  
       2023-05-25 21:53:58 +08:00
    @james122333 就是因为要保证兼容嵌入式设备才更要考虑 POSIX sh 标准
    mohumohu
        19
    mohumohu  
       2023-05-25 21:55:14 +08:00
    @james122333 很多 docker 的 init 就是用 shell 写的
    james122333
        20
    james122333  
       2023-05-25 22:05:33 +08:00 via Android
    @mohumohu

    能运行不就是兼容 有哪个专属硬件不能跑?
    docker init 用 shell 写的 然后呢? 大部分都有 bash 可用
    apline 类的装一下也不花多少时间
    mohumohu
        21
    mohumohu  
       2023-05-25 22:09:38 +08:00
    @james122333 不能跑:比如光猫上跑 ddns 脚本
    我能用 sh 实现的为啥要多装一个软件增大容器镜像体积
    james122333
        22
    james122333  
       2023-05-25 22:17:51 +08:00 via Android
    @mohumohu
    整个工具很小的 再嫌大拿直接二近制单档放入
    sh 与 bash 都是几百 k 你不能跑的理由不是真的不能跑
    nuk
        23
    nuk  
       2023-05-25 22:22:26 +08:00
    @james122333 configure 慢不是因为 shell 去 fork 进程慢,而是它为了检测一堆没用到的 api 去不断的去测试 compile ,如果没有 compile 的话应该也是很快的。个人感觉 bash 最缺的就是两个,一个数值系统和一个正则系统,这个 expr 稍微让它多干点都不行,只能动用 bc 或者 dc 。正则的 capture 得挖空心思来弄,用 substitution 吧它只能匹配简单的,其他完全就是各种花式杂耍,什么扩展 grep ,sed 匹配整行替换,awk 调用 match ,perl 还简单点就是宿主机得先装个 perl 。
    james122333
        24
    james122333  
       2023-05-25 22:30:43 +08:00 via Android
    @nuk

    compile 测试是可一定得要的 但没有这阶段还是慢
    就是很多 很多套件 configure 都很久
    bash 有减单数值计算和 left shift right shift
    perl 本来就是另外的大怪物 以前觉得不错 但现在不觉得 大部分用 oneliner 的不会正确用这些指令
    oneliner 不适合写脚本 只适合临时操作
    james122333
        25
    james122333  
       2023-05-25 22:34:20 +08:00 via Android
    bash 还有数值进制转换和位元运算 bitwise
    只差没有浮点数
    james122333
        26
    james122333  
       2023-05-25 22:37:39 +08:00 via Android
    还有没 int64 只能简单运算
    iyeatse
        27
    iyeatse  
       2023-05-26 00:06:08 +08:00
    试过用 ChatGPT 优化吗,效果咋样
    beixiao
        28
    beixiao  
       2023-05-26 08:56:32 +08:00 via iPhone
    @iyeatse rss
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3259 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 12:16 · PVG 20:16 · LAX 04:16 · JFK 07:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.