设为首页 收藏本站
查看: 528|回复: 0

[经验分享] bash编程技巧(实用)

[复制链接]
累计签到:10 天
连续签到:1 天
发表于 2014-10-30 11:07:15 | 显示全部楼层 |阅读模式
BASH 的保护性编程技巧                这是我写BASH程序的招式。这里本没有什么新的内容,但是从我的经验来看,人们爱滥用BASH。他们忽略了计算机科学,而从他们的程序中创造的是“大泥球”(译注:指架构不清晰的软件系统)。

在此我告诉你方法,以保护你的程序免于障碍,并保持代码的整洁。

不可改变的全局变量
  • 尽量少用全局变量
  • 以大写命名
  • 只读声明
  • 用全局变量来代替隐晦的$0,$1等
  • 在我的程序中常使用的全局变量:

    1
    2
    3
    readonly PROGNAME=$(basename $0)
    readonly PROGDIR=$(readlink -m $(dirname $0))
    readonly ARGS="$@"


    一切皆是局部的所有变量都应为局部的。


    1
    2
    3
    4
    5
    6
    7
    change_owner_of_file() {
        local filename=$1
        local user=$2
        local group=$3

        chown $user:$group $filename
    }



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    change_owner_of_files() {
        local user=$1; shift
        local group=$1; shift
        local files=$@
        local i

        for i in $files
        do
          chown $user:$group $i
        done
    }


  • 自注释(self documenting)的参数
  • 通常作为循环用的变量i,把它声明为局部变量是很重要的。
  • 局部变量不作用于全局域。

    1
    2
    kfir@goofy ~ $ local a
    bash: local: can only be used in a function


    main()
  • 有助于保持所有变量的局部性
  • 直观的函数式编程
  • 代码中唯一的全局命令是:main

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    main() {
        local files="/tmp/a /tmp/b"
        local i

        for i in $files
        do
          change_owner_of_file kfir users $i
        done
    }
    main


    一切皆是函数
  • 唯一全局性运行的代码是:
    - 不可变的全局变量声明- main()函数
  • 保持代码整洁
  • 过程变得清晰

    1
    2
    3
    main() {
        local files=$(ls /tmp | grep pid | grep -v daemon)
    }



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    temporary_files() {
        local dir=$1

        ls $dir \
          | grep pid \
          | grep -v daemon
    }

    main() {
        local files=$(temporary_files /tmp)
    }


  • 第二个例子好得多。查找文件是temporary_files()的问题而非main()的。这段代码用temporary_files()的单元测试也是可测试的。
  • 如果你一定要尝试第一个例子,你会得到查找临时文件以和main算法的大杂烩。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    test_temporary_files() {
        local dir=/tmp

        touch $dir/a-pid1232.tmp
        touch $dir/a-pid1232-daemon.tmp

        returns "$dir/a-pid1232.tmp" temporary_files $dir

        touch $dir/b-pid1534.tmp

        returns "$dir/a-pid1232.tmp $dir/b-pid1534.tmp" temporary_files $dir
    }


    如你所见,这个测试不关心main()。
    调试函数
  • 带-x标志运行程序:

    1
    bash -x my_prog.sh


    只调试一小段代码,使用set-x和set+x,会只对被set -x和set +x包含的当前代码打印调试信息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    temporary_files() {
        local dir=$1

        set -x
        ls $dir \
          | grep pid \
          | grep -v daemon
        set +x
    }


    打印函数名和它的参数:

    1
    2
    3
    4
    5
    6
    7
    8
    temporary_files() {
        echo $FUNCNAME $@
        local dir=$1

        ls $dir \
          | grep pid \
          | grep -v daemon
    }


    调用函数:

    1
    temporary_files /tmp


    会打印到标准输出:

    1
    temporary_files /tmp


    代码的清晰度
    这段代码做了什么?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    main() {
        local dir=/tmp

        [[ -z $dir ]] \
          && do_something...

        [[ -n $dir ]] \
          && do_something...

        [[ -f $dir ]] \
          && do_something...

        [[ -d $dir ]] \
          && do_something...
    }
    main


    让你的代码说话:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    is_empty() {
        local var=$1

        [[ -z $var ]]
    }

    is_not_empty() {
        local var=$1

        [[ -n $var ]]
    }

    is_file() {
        local file=$1

        [[ -f $file ]]
    }

    is_dir() {
        local dir=$1

        [[ -d $dir ]]
    }

    main() {
        local dir=/tmp

        is_empty $dir \
          && do_something...

        is_not_empty $dir \
          && do_something...

        is_file $dir \
          && do_something...

        is_dir $dir \
          && do_something...
    }
    main


    每一行只做一件事
  • 用反斜杠\来作分隔符。例如:

    1
    2
    3
    4
    5
    temporary_files() {
        local dir=$1

        ls $dir | grep pid | grep -v daemon
    }


    可以写得简洁得多:

    1
    2
    3
    4
    5
    6
    7
    temporary_files() {
        local dir=$1

        ls $dir \
          | grep pid \
          | grep -v daemon
    }


  • 符号在缩进行的开始
    符号在行末的坏例子:(译注:原文在此例中用了temporary_files()代码段,疑似是贴错了。结合上下文,应为print_dir_if_not_empty())


    1
    2
    3
    4
    5
    6
    7
    print_dir_if_not_empty() {
        local dir=$1

        is_empty $dir && \
          echo "dir is empty" || \
          echo "dir=$dir"
    }


    好的例子:我们可以清晰看到行和连接符号之间的联系。

    1
    2
    3
    4
    5
    6
    7
    print_dir_if_not_empty() {
        local dir=$1

        is_empty $dir \
          && echo "dir is empty" \
          || echo "dir=$dir"
    }


    打印用法不要这样做:

    1
    2
    3
    echo "this prog does:..."
    echo "flags:"
    echo "-h print help"


    它应该是个函数:

    1
    2
    3
    4
    5
    usage() {
        echo "this prog does:..."
        echo "flags:"
        echo "-h print help"
    }


    echo在每一行重复。因此我们得到了这个文档:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    usage() {
        cat <<- EOF
        usage: $PROGNAME options

        Program deletes files from filesystems to release space.
        It gets config file that define fileystem paths to work on, and whitelist rules to
        keep certain files.

        OPTIONS:
           -c --config            configuration file containing the rules. use --help-config to see the syntax.
           -n --pretend             do not really delete, just how what you are going to do.
           -t --test                run unit test to check the program
           -v --verbose             Verbose. You can specify more then one -v to have more verbose
           -x --debug               debug
           -h --help                show this help
              --help-config         configuration help

        Examples:
           Run all tests:
           $PROGNAME --test all

           Run specific test:
           $PROGNAME --test test_string.sh

           Run:
           $PROGNAME --config /path/to/config/$PROGNAME.conf

           Just show what you are going to do:
           $PROGNAME -vn -c /path/to/config/$PROGNAME.conf
        EOF
    }


    注意在每一行的行首应该有一个真正的制表符‘\t’。
    在vim里,如果你的tab是4个空格,你可以用这个替换命令:

    1
    :s/^    /\t/


    命令行参数这里是一个例子,完成了上面usage函数的用法。我从Kirk’s blog post – bash shell script to use getopts with gnu style long positional parameters得到这段代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    cmdline() {
        # got this idea from here:
        # http://kirk.webfinish.com/2009/1 ... itional-parameters/
        local arg=
        for arg
        do
          local delim=""
          case "$arg" in
                #translate --gnu-long-options to -g (short options)
                --config)         args="${args}-c ";;
                --pretend)      args="${args}-n ";;
                --test)         args="${args}-t ";;
                --help-config)    usage_config && exit 0;;
                --help)         args="${args}-h ";;
                --verbose)      args="${args}-v ";;
                --debug)          args="${args}-x ";;
                #pass through anything else
                *) [[ "${arg:0:1}" == "-" ]] || delim="\""
                    args="${args}${delim}${arg}${delim} ";;
          esac
        done

        #Reset the positional parameters to the short options
        eval set -- $args

        while getopts "nvhxt:c:" OPTION
        do
             case $OPTION in
             v)
                 readonly VERBOSE=1
                 ;;
             h)
                 usage
                 exit 0
                 ;;
             x)
                 readonly DEBUG='-x'
                 set -x
                 ;;
             t)
                 RUN_TESTS=$OPTARG
                 verbose VINFO "Running tests"
                 ;;
             c)
                 readonly CONFIG_FILE=$OPTARG
                 ;;
             n)
                 readonly PRETEND=1
                 ;;
          esac
        done

        if [[ $recursive_testing || -z $RUN_TESTS ]]; then
          [[ ! -f $CONFIG_FILE ]] \
                && eexit "You must provide --config file"
        fi
        return 0
    }


    你像这样,使用我们在头上定义的不可变的ARGS变量:

    1
    2
    3
    4
    main() {
        cmdline $ARGS
    }
    main


    单元测试
  • 在更高级的语言中很重要。
  • 使用shunit2做单元测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    test_config_line_paths() {
        local s='partition cpm-all, 80-90,'

        returns "/a" "config_line_paths '$s /a, '"
        returns "/a /b/c" "config_line_paths '$s /a:/b/c, '"
        returns "/a /b /c" "config_line_paths '$s   /a:    /b : /c, '"
    }

    config_line_paths() {
        local partition_line="$@"

        echo $partition_line \
          | csv_column 3 \
          | delete_spaces \
          | column 1 \
          | colons_to_spaces
    }

    source /usr/bin/shunit2


    这里是另一个使用df命令的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    DF=df

    mock_df_with_eols() {
        cat <<- EOF
        Filesystem         1K-blocks      Used Available Use% Mounted on
        /very/long/device/path
                             12462891623063572 10029919219% /
        EOF
    }

    test_disk_size() {
        returns 1000 "disk_size /dev/sda1"

        DF=mock_df_with_eols
        returns 124628916 "disk_size /very/long/device/path"
    }

    df_column() {
        local disk_device=$1
        local column=$2

        $DF $disk_device \
          | grep -v 'Use%' \
          | tr '\n' ' ' \
          | awk "{print \$$column}"
    }

    disk_size() {
        local disk_device=$1

        df_column $disk_device 2
    }


    这里我有个例外,为了测试,我在全局域中声明了DF为非只读。这是因为shunit2不允许改变全局域函数。


  • 运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
    2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
    3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
    4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
    5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
    6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
    7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
    8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

    所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-26710-1-1.html 上篇帖子: bash shell 内部命令及添加 下篇帖子: 入侵删除内容
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    扫码加入运维网微信交流群X

    扫码加入运维网微信交流群

    扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

    扫描微信二维码查看详情

    客服E-mail:kefu@iyunv.com 客服QQ:1061981298


    QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


    提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


    本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



    合作伙伴: 青云cloud

    快速回复 返回顶部 返回列表