BASH的保護性編程技巧

jopen 10年前發布 | 12K 次閱讀 Linux Bash

這是我寫BASH程序的招式。這里本沒有什么新的內容,但是從我的經驗來看,人們愛濫用BASH。他們忽略了計算機科學,而從他們的程序中創造的是“大泥球”(譯注:指架構不清晰的軟件系統)。

在此我告訴你方法,以保護你的程序免于障礙,并保持代碼的整潔。

 

不可改變的全局變量

  • 盡量少用全局變量
  • 以大寫命名
  • 只讀聲明
  • 用全局變量來代替隱晦的$0,$1等
  • 在我的程序中常使用的全局變量:
    </li> </ul>

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

     

    一切皆是局部的

    所有變量都應為局部的。

    change_owner_of_file() {
        local filename=$1
        local user=$2
        local group=$3

    chown $user:$group $filename
    

    }</pre>

    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
    

    }</pre>

    • 自注釋(self documenting)的參數
    • 通常作為循環用的變量i,把它聲明為局部變量是很重要的。
    • 局部變量不作用于全局域。
    • </ul>

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

      main()

      </div>

      • 有助于保持所有變量的局部性
      • 直觀的函數式編程
      • 代碼中唯一的全局命令是:main
        </li> </ul>

        main() {
            local files="/tmp/a /tmp/b"
            local i

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

        } main</pre>

        一切皆是函數

        • 唯一全局性運行的代碼是:
        • </ul>

          - 不可變的全局變量聲明

          - main()函數

          • 保持代碼整潔
          • 過程變得清晰
          • </ul>

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

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

            }

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

            • 第二個例子好得多。查找文件是temporary_files()的問題而非main()的。這段代碼用temporary_files()的單元測試也是可測試的。
              </li>

            • 如果你一定要嘗試第一個例子,你會得到查找臨時文件以和main算法的大雜燴。
            • </ul>

              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
              

              }</pre>

              如你所見,這個測試不關心main()。

              調試函數

              • 帶-x標志運行程序:
              • </ul>

                bash -x my_prog.sh
                只調試一小段代碼,使用set-x和set+x,會只對被set -x和set +x包含的當前代碼打印調試信息。

                temporary_files() {
                    local dir=$1

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

                }</pre></div>

                打印函數名和它的參數:

                temporary_files() {
                    echo $FUNCNAME $@
                    local dir=$1

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

                }</pre></div> </div>

                調用函數:

                temporary_files /tmp

                會打印到標準輸出:

                temporary_files /tmp

                代碼的清晰度

                </div>

                這段代碼做了什么?

                main() {
                    local dir=/tmp

                [[ -z $dir ]] \
                    && do_something...
                
                [[ -n $dir ]] \
                    && do_something...
                
                [[ -f $dir ]] \
                    && do_something...
                
                [[ -d $dir ]] \
                    && do_something...
                

                } main</pre>

                讓你的代碼說話:

                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</pre>

                每一行只做一件事

                • 用反斜杠\來作分隔符。例如:
                  </li> </ul>

                  temporary_files() {
                      local dir=$1

                  ls $dir | grep pid | grep -v daemon
                  

                  }</pre>

                  可以寫得簡潔得多:

                  temporary_files() {
                      local dir=$1

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

                  }</pre>

                  • 符號在縮進行的開始
                  • </ul>

                    符號在行末的壞例子:(譯注:原文在此例中用了temporary_files()代碼段,疑似是貼錯了。結合上下文,應為print_dir_if_not_empty())

                    </div>

                    print_dir_if_not_empty() {
                        local dir=$1

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

                    }</pre>

                    好的例子:我們可以清晰看到行和連接符號之間的聯系。

                    print_dir_if_not_empty() {
                        local dir=$1

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

                    }</pre>

                    打印用法

                    不要這樣做:

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

                    它應該是個函數:

                    usage() {
                        echo "this prog does:..."
                        echo "flags:"
                        echo "-h print help"
                    }

                    echo在每一行重復。因此我們得到了這個文檔:

                    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
                    

                    }</pre>

                    注意在每一行的行首應該有一個真正的制表符‘\t’。

                    在vim里,如果你的tab是4個空格,你可以用這個替換命令:

                    :s/^    /\t/

                    命令行參數

                    這里是一個例子,完成了上面usage函數的用法。我從Kirk’s blog post – bash shell script to use getopts with gnu style long positional parameters得到這段代碼

                    cmdline() {

                    # got this idea from here:
                    # http://kirk.webfinish.com/2009/10/bash-shell-script-to-use-getopts-with-gnu-style-long-positional-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 &amp;&amp; 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 ]] \
                            &amp;&amp; eexit "You must provide --config file"
                    fi
                    return 0
                    

                    }</pre>

                    你像這樣,使用我們在頭上定義的不可變的ARGS變量:

                    main() {
                        cmdline $ARGS
                    }
                    main

                    單元測試

                    • 在更高級的語言中很重要。
                    • 使用shunit2做單元測試
                      </li> </ul>

                      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</pre>

                      這里是另一個使用df命令的例子:

                      DF=df

                      mock_df_with_eols() { cat &lt;&lt;- EOF Filesystem 1K-blocks Used Available Use% Mounted on /very/long/device/path 124628916 23063572 100299192 19% / 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
                      

                      }</pre>

                      這里我有個例外,為了測試,我在全局域中聲明了DF為非只讀。這是因為shunit2不允許改變全局域函數。


                      原文鏈接: Kfir Lavi   翻譯: 伯樂在線 - cjpan
                      譯文鏈接: http://blog.jobbole.com/73257/

                   本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
                   轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
                   本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!