zoukankan      html  css  js  c++  java
  • Shell脚本最佳实践

    Shell脚本最佳实践

    设置编码、缩进、文件命名和执行权限

    使用utf-8编码;
    统一使用tab缩进或空格缩进,不要混用;
    文件名以.sh结尾,并且统一风格;
    添加可执行权限:

    chmod +x [bash_script.sh]
    

    最后,在所有输出完毕后,添加一个空行。

    指定默认解释器

    也就是不要省略脚本第一行的shebang,一般默认是bash:

    #!/bin/bash
    

    或者更为通用一些:

    #!/usr/bin/env bash
    

    本机可用的shell解释器,可以通过以下命令查看:

    cat /etc/shells
    

    设置Shell环境

    设置命令回显:

    set -x
    

    shell默认设置不够友好,我们希望予以加强。

    # 遇到未声明的变量则报错停止
    set -u
    # 遇到执行错误则停止
    set -e
    

    由于set -e对管道命令无效,管道命令其中一步失败则中止,需要使用:

    set -o pipefail
    

    我们将这三条合并,构成 bash strict mode,添加在bash脚本的开始位置:

    set -euo pipefail
    

    因为这里都是shell环境设置,所以也可以在执行脚本的时候来使用:

    bash -euo pipefail [bash_sctipt.sh]
    

    总是使用main函数包裹执行体

    main() {
          func1 param1 param2
          func2 param
    }
    main "$@"
    

    与python类似,shell不需要函数入口,可以从第一条指令开始执行。但是为了可读性和方便调试,我们总是写一个命名为main的函数来作为全局入口。

    变量

    1)环境变量的设置和取消:

    # 设置环境变量
    export SKIP_BFS=1
    # 取消环境变量
    unset SKIP_BFS
    

    注意,由于前文启用了strict mode,受set -u影响,脚本中使用未设置的环境变量,会报unbound variable错误。
    可以通过-v来检测是否设置了环境变量:

    if [[ -v SKIP_BFS ]]; then
          echo 'environment variable SKIP_BFS is set'
    fi
    

    2)局部变量
    shell变量默认全局作用域,这一点与JavaScript类似,函数内声明局部变量,应该添加local关键字。

    3)使用变量时,总是用花括号和双引号把变量包起来,例如:

    # 带空格的路径
    cp -r "${src_dir}" "${dest_dir}"
    

    不适用双引号包裹变量的话,路径有空格会被作为两个参数来处理,从而导致很严重的bug,用"$var"这种写法,避免了这个问题。
    花括号则是避免避免变量名和下划线的拼接处出现歧义的问题。

    条件判断

    字符比较和文件测试使用双方括号 [[ ]],并在每个变量和运算符以及和括号之间加入一个空格,例如:

    if [[ $# > 1 ]] || [[ $# == 1 && $1 != 'PC' && $1 != 'server' ]]; then
          echo 'Invalid commandline arguments, you should use `./run.sh` or `./run.sh PC` or `./run.sh server`'
          exit 1
    fi
    

    其中,$#用于获取命令行参数个数,$N用于获取第N个命令行参数,参数$0指的是脚本文件名。
    相比单方括号,双方括号的优势在于可以直接使用比较运算符> < == !=等,而不是必须使用-gt -lt -eq -ne;此外双方括号可以使用&& ||来表达与和或,而不用必须写-a -o这种难以记忆的写法,并且拥有逻辑短路的功能。因此,强烈建议使用双方括号取代单方括号作为作为条件判断语句。

    数字的比较应该使用双小括号 (( )),并且不需要空格分隔各值和运算符。例如判断正在运行的进程个数:

    running=$(ps -aux -r | wc -l)
    if (( ${running} > 5 )); then
          echo "${running} processes running, please handle this problem. exit."
          exit 1;
    fi
    

    在双方括号中进行数字的比较也是可以的,但是直接使用比较运算符> < == !=等得到的常常是错误的结果,使用-gt -lt -eq -ne得到的总是正确的但难以记忆。使用双小括号则可以直接使用比较运算符进行判断。

    使用文件前做好异常处理

    # 判断文件夹存在
    if [[ ! -d 'src' ]]; then
          echo 'src dir not found'
          exit 1
    fi
    # 判断普通文件存在
    if [[ ! -f 'a.txt' ]]; then
          touch 'a.txt'
    fi
    # 判断可执行文件存在并且可执行
    if [[ ! -x "$(command -v java)" ]]; then
          echo 'java is not installed, or not execuatable'
    fi
    

    注意cp -r命令,在文件夹不存在时回创建文件夹并复制,而当文件夹存在时,会复制到子文件夹内。

    循环语句

    提倡使用for-in循环

    # C风格
    for (( i=0; i<10; i++)); do
          // echo $i
    done
    # for-in
    for i in $(seq 0 9); do
          // echo $i
    

    和 if 语句的 then 一样,for 语句的 do 也紧跟在语句后面,不单独占一行,这样显得比较紧凑。同样不要忘记加分号。

    这里补充说明一下seq语句用法,注意与python做好区分:

    # 单参数,输出 1 2 3 4
    $(seq 4)
    # 双参数,输出 2 3 4 5
    $(seq 2 5)
    # 三参数,输出 8 6 4 2
    $(seq 8 -2 1)
    

    这里三参数情况时的增量参数,可以正可以负,也可以是小数。
    更多用法可参考Bash Range: How to iterate over sequences generated on the shell

    ${arr[@]}${arr[*]} 进行列表循环

    $*$@ 的相同点都是引用所有参数;不同点则只有在双引号中体现出来。假设在脚本运行时写了三个参数 1、2、3,,则 $* 等价于 "1 2 3"(传递了一个参数),而 $@ 等价于 "1" "2" "3"(传递了三个参数)
    单个列表元素迭代:

    arr=(1 3 5 a)
    for s in ${arr[@]}; do
          echo $s
    done
    

    多个列表合并迭代:

    arr1=(1 3 5 a)
    arr2=(2 4 6 b)
    for s in ${arr1[@]} ${arr2[@]}; do
          echo $s
    done
    

    注意,花括号不可省略。
    如果需要进行函数传参,则需要使用使用 $*,并且在传参时使用双引号 " 把列表参数包起来作为一个参数整体传入。示例如下:

    deltas="0.1 0.2 0.3"
    run() {
        params=$1
        for x in ${params[*]}; do
            echo $x
        done
    }
    run "${deltas[*]}"
    

    注意,函数内参数列表不能用引号,函数调用处引号不可少。原因在于,函数外是要把列表作为一个参数整体传入(而不是分成多个参数传入),函数内是把列表拆成多个元素依次遍历。

    有时候,我们希望对列表中的元素整体加前缀或者加后缀,在Makefile里可以很方便地调用addprefixaddsuffix两个内置函数来完成,在bash里则需要使用Bash parameter expansion

    # addprefix
    for s in ${arr[@]/#/PREFIX}; do
          echo $s
    done
    # addsuffix
    arr_suffix=${arr[@]/%/SUFFIX}
    echo ${arr_suffix}
    

    使用$()而不是反引号获取表达式的值

    如for-in:

    # 建议使用 $(seq lb ub) 而不是 `seq lb ub` 获取范围
    for i in $(seq 0 10) do 
          echo $i
    done
    

    使用(())bc进行数学运算

    shell默认的都是文本操作,所以a=$b+$c并不能把两个数进行求和,需要数学运算的话,应该明确标明。
    分两种情况,整数运算和浮点运算:
    整数运算建议使用(()),不建议适用[]letexpr:

    (( a = $b + $c ))
    # 或者
    a=$( b + c ))
    

    浮点运算可以用bc:

    echo "$b + $c" | bc
    # 或者
    bc <<< "$b + $c"
    

    使用 /dev/null 过滤输出信息

    [expr] > /dev/null 2>&1
    

    命令解释:重定向到空设备,并把标准错误输出stderr也重定向为stdout。
    注意,2>&1应该总是放在命令的末尾。

    获取脚本所在目录

    有时候,需要适用脚本对同一份代码仓库下其他文件夹内的文件进行操作,如codegen、format、validate等工作。此时需要的是相对本脚本的路径,与调用脚本时的路径无关,所以需要先行获取脚本所在路径(绝对路径):

    # 在 `$()` 里面执行 cd 命令不会改变当前工作路径
    readonly __DIR__=$(cd $(dirname $0) && pwd)
    echo $__DIR__
    

    参考https://john-yuan.org/blog/how-to-get-the-dir-of-the-current-shell-script.html

    case语句等

    TBD

    进一步阅读

    Google Bash风格指南
    阮一峰 Bash 脚本教程

  • 相关阅读:
    js中const,var,let区别与用法
    poi excel 导出
    spring 实体类 date类型字段处理
    mysql 1449 : The user specified as a definer ('root'@'%') does not exist
    pjax学习
    上传文件 connection reset
    mysql连接问题
    Scala Actor Model
    Scala 隐式转换
    Scala Trait+Match+Case class+偏函数
  • 原文地址:https://www.cnblogs.com/zhcpku/p/13549117.html
Copyright © 2011-2022 走看看