编程Linux Shell 入门:学习笔记 – 第二部分:Shell 脚本编程

什么是shell脚本

当命令或者程序语句写在文件中,我们执行文件,读取其中的代码,这个程序就称之为shell脚本。
有了shell脚本肯定是要有对应的解释器了,常见的shell脚本解释器有sh、python、perl、tcl、php、ruby等。一般这种使用文件方式来执行sh命令的方式被称为非交互方式。

  • Windows中存在的*.bat批处理脚本。
  • Linux中常用*.sh脚本文件。
  • shell脚本规则

    在Linux系统中,shell脚本或者称之为bash shell程序,通常都是vim编辑,有Linux命令、bash shell指令、逻辑控制语句和注释信息组成。
    google bash编程规范链接

  • 1.使用.sh这样特定语言后缀作为扩展名,可以快速识别文件。
  • 2.set可以设置shell的选项。SUID(Set User ID)和SGID(Set Group ID)在shell脚本中是被禁止的,如果你需要较高权限的访问请使用sudo
  • 3.建议使用STDERR打印错误信息。
  • err() {
        echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" >&2
    }
    
    if ! do_something; then
        err "Unable to do_something"
        exit "${E_DID_NOTHING}"
    fi
    
  • 4.文件头:指定脚本解释器,这里是用#!/bin/bash做解释器的。
  • #!/bin/bash
    #
    # Perform hot backups of Oracle databases.
    
  • 5.函数注释应该包括:
  • 函数的描述
  • 全局变量的使用和修改
  • 使用的参数说明
  • 返回值,而不是上一条命令运行后默认的退出状态
    例如:
  • #!/bin/bash
    #
    # Perform hot backups of Oracle databases.
    
    export PATH='/usr/xpg4/bin:/usr/bin:/opt/csw/bin:/opt/goog/bin'
    
    #######################################
    # Cleanup files from the backup dir
    # Globals:
    #   BACKUP_DIR
    #   ORACLE_SID
    # Arguments:
    #   None
    # Returns:
    #   None
    #######################################
    cleanup() {
      ...
    }
    
    
  • 6.TODO注释应该包含全部大写的字符串TODO,接着是括号中你的用户名。冒号是可选的。最好在TODO条目之后加上 bug或者ticket 的序号。
  • # TODO(mrmonkey): Handle the unlikely edge cases (bug ####)
    
  • 7.缩进一般使用两个空格,没有制表符。
  • 8.行的长度和长字符串最大长度为80个字符。
    使用下述两种方式规避长字符串:
  • # DO use 'here document's
    cat <<END;
    I am an exceptionally long
    string.
    END
    
    # Embedded newlines are ok too
    long_string="I am an exceptionally
      long string."
    
  • 9.管道尽量写在一行,否则应该将整个管道操作分割成每行一个管段,管道操作的下一部分应该将管道符放在新行并且缩进2个空格。这适用于使用管道符’|’的合并命令链以及使用’||’和’&&’的逻辑运算链。
  • # All fits on one line
    command1 | command2
    
    # Long commands
    command1 \
      | command2 \
      | command3 \
      | command4
    
  • 10.循环中,; do , ; then 应该和 if/for/while 放在同一行。 else 应该单独一行,结束语句应该单独一行并且跟开始语句垂直对齐。
    例如:
  • for dir in ${dirs_to_cleanup}; do
      if [[ -d "${dir}/${ORACLE_SID}" ]]; then
        log_date "Cleaning up old files in ${dir}/${ORACLE_SID}"
        rm "${dir}/${ORACLE_SID}/"*
        if [[ "$?" -ne 0 ]]; then
          error_message
        fi
      else
        mkdir -p "${dir}/${ORACLE_SID}"
        if [[ "$?" -ne 0 ]]; then
          error_message
        fi
      fi
    done
    
  • 11.case语句模式表达式前面不应该出现左括号。避免使用 ;&;;& 符号。关键规则如下:
    * 通过2个空格缩进可选项。
    * 在同一行可选项的模式右圆括号之后和结束符 ;; 之前各需要一个空格。
    * 长可选项或者多命令可选项应该被拆分成多行,模式、操作和结束符 ;; 在不同的行。
    例子1:
  • case "${expression}" in
      a)
        variable="..."
        some_command "${variable}" "${other_expr}" ...
        ;;
      absolute)
        actions="relative"
        another_command "${actions}" "${other_expr}" ...
        ;;
      *)
        error "Unexpected expression '${expression}'"
        ;;
    esac
    

    例子2:

    verbose='false'
    aflag=''
    bflag=''
    files=''
    while getopts 'abf:v' flag; do
      case "${flag}" in
        a) aflag='true' ;;
        b) bflag='true' ;;
        f) files="${OPTARG}" ;;
        v) verbose='true' ;;
        *) error "Unexpected option ${flag}" ;;
      esac
    done
    
  • 12.变量扩展应该保持跟你所发现的一致,引用你的变量,推荐用 ${var} 而不是 $var
    例如:
  • # Section of recommended cases.
    
    # Preferred style for 'special' variables:
    echo "Positional: $1" "$5" "$3"
    echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..."
    
    # Braces necessary:
    echo "many parameters: ${10}"
    
    # Braces avoiding confusion:
    # Output is "a0b0c0"
    set -- a b c
    echo "${1}0${2}0${3}0"
    
    # Preferred style for other variables:
    echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}"
    while read f; do
      echo "file=${f}"
    done < <(ls -l /tmp)
    
    # Section of discouraged cases
    
    # Unquoted vars, unbraced vars, brace-quoted single letter
    # shell specials.
    echo a=$avar "b=$bvar" "PID=${$}" "${1}"
    
    # Confusing use: this is expanded as "${1}0${2}0${3}0",
    # not "${10}${20}${30}
    set -- a b c
    echo "$10$20$30"
    
  • 13.引用
  • 除非需要小心不带引用的扩展,否则总是引用包含变量、命令替换符、空格或shell元字符的字符串
  • 推荐引用是单词的字符串(而不是命令选项或者路径名)
  • 千万不要引用整数
  • 注意 [[ 中模式匹配的引用规则
  • 请使用 $@ 除非你有特殊原因需要使用 $*
    例如:
  • # 'Single' quotes indicate that no substitution is desired.
    # "Double" quotes indicate that substitution is required/tolerated.
    
    # Simple examples
    # "quote command substitutions"
    flag="$(some_command and its args "$@" 'quoted separately')"
    
    # "quote variables"
    echo "${flag}"
    
    # "never quote literal integers"
    value=32
    # "quote command substitutions", even when you expect integers
    number="$(generate_number)"
    
    # "prefer quoting words", not compulsory
    readonly USE_INTEGER='true'
    
    # "quote shell meta characters"
    echo 'Hello stranger, and well met. Earn lots of $$$'
    echo "Process $$: Done making \$\$\$."
    
    # "command options or path names"
    # ($1 is assumed to contain a value here)
    grep -li Hugo /dev/null "$1"
    
    # Less simple examples
    # "quote variables, unless proven false": ccs might be empty
    git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"}
    
    # Positional parameter precautions: $1 might be unset
    # Single quotes leave regex as-is.
    grep -cP '([Ss]pecial|\|?characters*)$' ${1:+"$1"}
    
    # For passing on arguments,
    # "$@" is right almost everytime, and
    # $* is wrong almost everytime:
    #
    # * $* and $@ will split on spaces, clobbering up arguments
    #   that contain spaces and dropping empty strings;
    # * "$@" will retain arguments as-is, so no args
    #   provided will result in no args being passed on;
    #   This is in most cases what you want to use for passing
    #   on arguments.
    # * "$*" expands to one argument, with all args joined
    #   by (usually) spaces,
    #   so no args provided will result in one empty string
    #   being passed on.
    # (Consult 'man bash' for the nit-grits ;-)
    
    set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$*"; echo "$#, $@")
    set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$@"; echo "$#, $@")
    
  • 14.命令替换应使用$(command) 而不是反引号
  • # This is preferred:
    var="$(command "$(command1)")"
    
    # This is not:
    var="`command \`command1\``"
    
  • 15.test, [和[[时,推荐使用 [[ ... ]] ,而不是 [ , test , 和 /usr/bin/[
  • # This ensures the string on the left is made up of characters in the
    # alnum character class followed by the string name.
    # Note that the RHS should not be quoted here.
    # For the gory details, see
    # E14 at http://tiswww.case.edu/php/chet/bash/FAQ
    if [[ "filename" =~ [^[:digit:]]+name ]]; then
      echo "Match"
    fi
    
    # This matches the exact pattern "f*" (Does not match in this case)
    if [[ "filename" == "f*" ]]; then
      echo "Match"
    fi
    
    # This gives a "too many arguments" error as f* is expanded to the
    # contents of the current directory
    if [ "filename" == f* ]; then
      echo "Match"
    fi
    
  • 16.测试字符串尽可能使用引用,而不是过滤字符串
    例子1
  • # Do this:
    if [[ "${my_var}" = "some_string" ]]; then
      do_something
    fi
    
    # -z (string length is zero) and -n (string length is not zero) are
    # preferred over testing for an empty string
    if [[ -z "${my_var}" ]]; then
      do_something
    fi
    
    # This is OK (ensure quotes on the empty side), but not preferred:
    if [[ "${my_var}" = "" ]]; then
      do_something
    fi
    
    # Not this:
    if [[ "${my_var}X" = "some_stringX" ]]; then
      do_something
    fi
    

    例子2

    # Use this
    if [[ -n "${my_var}" ]]; then
      do_something
    fi
    
    # Instead of this as errors can occur if ${my_var} expands to a test
    # flag
    if [[ "${my_var}" ]]; then
      do_something
    fi
    
  • 17.文件名的通配符扩展,当进行文件名的通配符扩展时,请使用明确的路径
  • # Here's the contents of the directory:
    # -f  -r  somedir  somefile
    
    # This deletes almost everything in the directory by force
    psa@bilby$ rm -v *
    removed directory: `somedir'
    removed `somefile'
    
    # As opposed to:
    psa@bilby$ rm -v ./*
    removed `./-f'
    removed `./-r'
    rm: cannot remove `./somedir': Is a directory
    removed `./somefile'
    
  • 18.Eval应该避免使用
    eval简单例子
  • # What does this set?
    # Did it succeed? In part or whole?
    eval $(set_my_variables)
    
    # What happens if one of the returned values has a space in it?
    variable="$(eval some_function)"
    
  • 19.不建议管道导向while循环
  • 20.函数名,使用小写字母,并用下划线分隔单词。使用双冒号 :: 分隔库。函数名之后必须有圆括号。关键词 function 是可选的,但必须在一个项目中保持一致
  • # Single function
    my_func() {
      ...
    }
    
    # Part of a package
    mypackage::my_func() {
      ...
    }
    

    当函数名后存在 () 时,关键词 function 是多余的。但是其促进了函数的快速辨识。

  • 21.变量名,如函数名命名相同,循环的变量名应该和循环的任何变量同样命名
  • for zone in ${zones}; do
      something_with "${zone}"
    done
    
  • 22.常量和环境变量名,全部大写,用下划线分隔,声明在文件的顶部
  • # Constant
    readonly PATH_TO_FILES='/some/path'
    
    # Both constant and environment
    declare -xr ORACLE_SID='PROD'
    

    动态生成的常量,可以使用readonlyexport来进行设置,在函数中declare不会对全局变量进行操作。

    VERBOSE='false'
    while getopts 'v' flag; do
      case "${flag}" in
        v) VERBOSE='true' ;;
      esac
    done
    readonly VERBOSE
    
  • 23.源文件名使用小写,如果需要的话使用下划线分隔单词
  • 24.只读变量使用readonly 或者 declare -r 来确保变量只读
  • zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)"
    if [[ -z "${zip_version}" ]]; then
      error_message
    else
      readonly zip_version
    fi
    
  • 25.本地变量使用 local 声明特定功能的变量。声明和赋值应该在不同行。local作用域为函数内部和子函数中可见。
  • my_func2() {
      local name="$1"
    
      # Separate lines for declaration and assignment:
      local my_var
      my_var="$(my_func)" || return
    
      # DO NOT do this: $? contains the exit code of 'local', not my_func
      local my_var="$(my_func)"
      [[ $? -eq 0 ]] || return
    
      ...
    }
    
  • 26.函数位置:将文件中所有的函数一起放在常量下面。不要在函数之间隐藏可执行代码
  • 27.主函数main:为了方便查找程序的开始,将主程序放入一个称为 main 的函数,作为最下面的函数
  • 28.检查返回值:使用 $? 或直接通过一个 if 语句来检查以保持其简洁
    例子1
  • if ! mv "${file_list}" "${dest_dir}/" ; then
      echo "Unable to move ${file_list} to ${dest_dir}" >&2
      exit "${E_BAD_MOVE}"
    fi
    
    # Or
    mv "${file_list}" "${dest_dir}/"
    if [[ "$?" -ne 0 ]]; then
      echo "Unable to move ${file_list} to ${dest_dir}" >&2
      exit "${E_BAD_MOVE}"
    fi
    

    例子2(Bash内部PIPESTATUS变量)
    -作用

    tar -cf - ./* | ( cd "${dir}" && tar -xf - )
    if [[ "${PIPESTATUS[0]}" -ne 0 || "${PIPESTATUS[1]}" -ne 0 ]]; then
      echo "Unable to tar files to ${dir}" >&2
    fi
    

    例子3(PIPESTATUS优化)

    tar -cf - ./* | ( cd "${DIR}" && tar -xf - )
    return_codes=(${PIPESTATUS[*]})
    if [[ "${return_codes[0]}" -ne 0 ]]; then
      do_something
    fi
    if [[ "${return_codes[1]}" -ne 0 ]]; then
      do_something_else
    fi
    
  • 29.内建命令和外部命令选择上请使用内建命令
  • # Prefer this:
    addition=$((${X} + ${Y}))
    substitution="${string/#foo/bar}"
    
    # Instead of this:
    addition="$(expr ${X} + ${Y})"
    substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"
    
    物联沃分享整理
    物联沃-IOTWORD物联网 » 编程Linux Shell 入门:学习笔记 – 第二部分:Shell 脚本编程

    发表评论