【shell】shell脚本模板和规范

参考:cShell脚本模板_运维@小兵的博客-CSDN博客

#!/bin/bash
################################文件头############################
# Copyright 2022, xxxxxx Co. Ltd.
# All rights reserved.
# FileName:    case001.sh
# Description: first case for test.
# Author:      Michael
# http://www.xxxxxx.com 
# Revision: 1.0.0
#################################################################

set -e            	#打开异常退出功能
# set -x            #打开Debug功能

######################定义变量######################
source /etc/profile		#避免用contab、ansible、Jenkins执行shell脚本出现环境变量不对的问题
WORKDIR="$(cd $(dirname "$0") || exit 1;pwd)"		#脚本所在路径
echo "Current Excute: bash ${WORKDIR}/$0 $@"

function_dir=${WORKDIR}/myfunction.lib

#创建日志文件
if [[ ! -d ${WORKDIR}/logs ]];then
    mkdir -p ${WORKDIR}/logs
else
    rm -f ${WORKDIR}/logs/*.log
fi
[[ $UID -ne 0 ]] && echo "[ERROR] Please Use root Excute......" && exit 1

#输出信息
# ${FUNCNAME[0]代表当前函数名,$LINENO代表当前代码行号
echo "$(date "+%Y-%m-%d %T.%N")[ERROR ${FUNCNAME[0]}:$LINENO] Invalid Param"
echo "$(date "+%Y-%m-%d %T.%N")[INFO]:Install Success"



#加载函数库
if [[ -f "$function_dir" ]];then
 	source $function_dir
else
	echo -e "\033[31m函数库不存在\033[0m"
	exit 71
fi




######################功能函数######################
#检查环境
Check_Env() {
    echo "[INFO] Begin Check_Env..."
    [[ $UID -ne 0 ]] && echo "[ERROR] Please Use Admin(root) Excute......" && exit 1
    #检查命令是否存在
    for cmd_bin in curl mvn
    do
        if ! command -v ${cmd_bin} &> /dev/null;then
            echo "[ERROR] ${cmd_bin} command Not Exist" && exit 1
        fi
    done
    echo "[INFO] Check_Env Success"
}



#帮助信息
Help() {
	cat << EOF
Usage: 
=======================================================================
optional arguments:
	-h	提供帮助信息
	-num	虚拟机编号
EXAMPLE:
	bash $0 -num 10 web1 eth0 192.168.4.1/24	
EOF
}



#######################主函数#######################
#参数校验
[[ $# -ne 1 ]] && echo "[ERROR] Invalid Param!!! eg:bash $0 ansible_path" && exit 1
[[ $# -le 5 ]] && echo "[ERROR] Invalid Param!!!,Please Excute:bash $0 -h" && exit 1


#主函数
main() {
    Print
    cecho 32 "开始执行......"
    echo "1.本地源"
    echo "2.网络源"
    read -p  "请选择:" choice
    case ${choice} in
    1)
        Conf_Apt;;
    2)
        echo "1.ubuntu14.04"
        echo "2.ubuntu16.04"
        echo "3.ubuntu18.04"
        read -p "请选择系统版本:" choice
        case ${choice} in
        1)
            Conf_Ubuntu14.04;;
        2)
            Conf_Ubuntu16.04;;
        3)
            Conf_Ubuntu18.04;;
        *)
            cecho 31 "Invalid option!"
        esac
        ;;	    
    *)
        cecho 31 "Invalid option!"
    esac		
}

main

if [[ $# -eq 0 ]];then
    Excute_All
elif [[ "$1" == "-c" -a "$#" -eq 2 ]];then
    case $2 in
    system)
        Init_System;;
    *)
        cecho 31 "Invalid option:bash `basename $0` [-h]"
    esac
elif [[ "$1" == "-h" ]];then
    Help
else
    Help && exit 1
fi

#读取短参数
[[ $# -eq 0 ]] && HELP
while getopts :hnum::a: ARGS
do
	case $ARGS in
	h)
		HELP;;
	nu|m)
		Name=rh7_node$OPTARG;;	
	\?)
		cecho 31 "Invalid option:bash `basename $0` [-h]"
	esac
done

启动行参数解析

使用getopts进行参数解析

  • 如果一个字母后面有一个":",表示该命令行选项后面要跟一个参数。
  • 如optsting写成"b:o:h",表示支持-b、-o、-h选项识别,-b和-o选项后面需要跟一个参数
  • 输出-b 但是又没有指定参数,就走到这里 :)
while getopts "b:o:h" opt_name
do
    case $opt_name in
        b) echo "-b Option is recognized, argument=$OPTARG"
            build
           ;;
        o) echo "-o Option is recognized, argument=$OPTARG"
            update $OPTARG
           ;;
        h) echo "-h Option is recognized"
            package
           ;;
        :) echo "$OPTARG Option need a argument"   # 比如输出-b 但是又没有指定参数,就走到这里
            print_help
           ;;
    esac
done

参数调用

  • 普通的函数调用
function build()
{
    echo "building ..."
}

build 
  • 函数带参数
function update()
{
    update_mode=$1
    # “STRING” 的长度为零则为真
    if [ -z "${update_mode}" ]
    then
        echo ">> ERROR: build mode required."
        return 1
    fi

    if [[ ${update_mode} != "release" ]] && [[ ${update_mode} != "debug" ]]
    then
        echo ">> ERROR: invalid build mode: ${update_mode}"
        return 1
    fi

    if [ ${update_mode} == "release" ]
    then
        echo ">>>> relese update mode ..."
    fi

    if [ ${update_mode} == "debug" ]
    then
        echo ">>>> debug update mode ..."
    fi
    return 0
}

update debug

执行shell命令

function build_svr() {
    core=$(grep -c ^processor /proc/cpuinfo)
    echo ${core}

    res=$(make svr)
    if [ $? != 0 ] 
    then 
        echo "build svr fail"
    fi
    echo "build svr successfully"
    echo ${res}
}

文本比较

function update() {
    update_mode=$1
    # # “STRING” 的长度为零则为真
    if [ -z "${update_mode}" ]
    then
        echo ">> ERROR: build mode required."
        return 1
    fi

    if [[ ${update_mode} != "release" ]] && [[ ${update_mode} != "debug" ]]
    then
        echo ">> ERROR: invalid build mode: ${update_mode}"
        return 1
    fi

    if [ ${update_mode} == "release" ]
    then
        echo ">>>> relese update mode ..."
    fi

    if [ ${update_mode} == "debug" ]
    then
        echo ">>>> debug update mode ..."
    fi
    return 0
}

获得脚本的路径和脚本名称

SCRIPT_DIR="$(cd `dirname $0`; pwd)"
SCRIPT_NAME=$(basename $0)
OUTPUT_PATH=${SCRIPT_DIR}/data

function show_path() {
    echo ${SCRIPT_DIR}
    echo ${SCRIPT_NAME}

    cd ${OUTPUT_PATH}
    echo "hello" > 1.log
    
    cd ${SCRIPT_DIR}
}

show_path

从代码仓库里面拉取代码

function svn_checkout() {
    
    if [ ! -d ${OUTPUT_PATH} ]
    then
        mkdir ${OUTPUT_PATH}
        echo "create dir"
    fi
    echo "dir created"

    rm -rf ${OUTPUT_PATH}
    cd ${OUTPUT_PATH}
    svn checkout ${SVN_URL} . --username ${USERNAME} --password ${PASSWORD}
    cd ${SCRIPT_DIR}
}

读取配置ini文件

SCRIPT_DIR="$(cd `dirname $0`; pwd)"
SCRIPT_NAME=`basename $0`
OUTPUT_PATH=${SCRIPT_DIR}/data
SHELL_CFG=${SCRIPT_DIR}/cfg.ini

function load_cfg() {

    if [ -f "${SHELL_CFG}" ]
    then
        source ${SHELL_CFG}
    fi

    echo ${OUTPUT_CFG_PATH}
}

load_cfg

echo ${OUTPUT_CFG_PATH}

对应的ini文件(cfg.ini)

OUTPUT_CFG_PATH=./data

打印时间

print_date() {
    echo "========date========" 
    date 
}

输出重定向

LOG_FILE="build.log"
ERR_FILE="build.err"
TEE="/usr/bin/tee"

set_env() {
    if [ ${SILENT} == 'true' ] 
    then
        exec 1>${LOG_FILE}
        exec 2>${ERR_FILE}
    else
        NPIPE=/tmp/$$.tmp
        trap "rm -f ${NPIPE}" EXIT
        mknod $NPIPE p
        ${TEE} <$NPIPE ${LOG_FILE} &
        exec 1>&-         # &- 关闭标准输出  # n&- 表示将 n 号输出关闭
        exec 1>$NPIPE

        exec 2>${ERR_FILE}
    fi
}

脚本debug

脚本debug神器,把每一行的脚本执行了什么都打印出来

#!/bin/sh -x


作者:DayDayUpppppp
链接:https://www.jianshu.com/p/34c76024c0ef
 

vim自动给shell文件添加文件头

修改文件 /etc/vimrc

追加到末尾就好

set ignorecase 
autocmd BufNewFile *.py,*.cc,*.sh,*.java exec ":call SetTitle()"

func SetTitle()
if expand("%:e") == 'sh'
call setline(1, "#!/bin/bash")
call setline(2,"##############################################################")
call setline(3, "# File Name:".expand("%"))
call setline(4, "# Version:V1.0")
call setline(5, "# Author:quanheng")
call setline(6, "# Organization:www.quanheng77.top")
call setline(7, "# Desc:")
call setline(8,"##############################################################")
endif
endfunc

代码规范

(摘自:Shell编程之代码规范_shell脚本指定编码格式

1.2. 基本原则

  1. 参考主流Shell编程命名代码风格。
  2. 代码规范借鉴工具ShellCheck。
  3. 在性能足够的情况下,可读性优先考虑。

1.3. 预定义

  1. 小写驼峰命名,如pathName。
  2. 大写驼峰命名,如PathName。
  3. 大写加下划线,如MAX_DEV_CNT=32。
  4. 小写加下划线,如path_name=/dev/nvme0。

2. 代码风格

2.1. 文件头

#!/bin/bash
################################################################ 
# Copyright 2022, xxxxxx Co. Ltd.
# All rights reserved.
# FileName:    case001.sh
# Description: first case for test.
# Author:      Michael
# http://www.xxxxxx.com 
# Revision: 1.0.0
#################################################################

2.2. 注释

  1. 单行注释,#后面要空一格。
# Delete a file in a sophisticated manner.
  1. 函数注释
#######################################
# Get configuration directory.
# Globals:
#   SOMEDIR
# Arguments:
#   None
# Outputs:
#   Writes location to stdout
#######################################
get_dir() {
  echo "${SOMEDIR}"
}

#######################################
# Delete a file in a sophisticated manner.
# Arguments:
#  $1: File to delete, a path.
# Returns:
#   0 if thing was deleted, otherwise non-zero.
#######################################
del_thing() {
  rm "$1"
}

2.3. 缩进

tab键设置为4个空格,默认缩进为4个空格。

main() {
    # 缩进4个空格
    say="hello World."
    echo "${say}"
}

2.4. 函数

function定义,默认不需要加function修饰。函数统一放在源文件的全局变量之后,可执行代码之前,函数之间不放置可执行代码。代码功能比较少时,可以不定义main函数。

main() {
    echo "hello World."
    exit 0
}

2.5. 最大行数

代码一行的最大长度限定在120个字符左右。

2.7. 循环

让; do和; then和while for 以及if在同一行

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 (( $? != 0 )); then
      error_message
    fi
  else
    mkdir -p "${dir}/${ORACLE_SID}"
    if (( $? != 0 )); then
      error_message
    fi
  fi
done

2.8. case语句

可选项中的多个命令应该被拆分成多行,模式表达式、操作和结束符 ;; 在不同的行。

case "${expression}" in
    a)
        variable="..."
        some_command "${variable}" "${other_expr}" ...
        ;;
    absolute)
        actions="relative"
        another_command "${actions}" "${other_expr}" ...
        ;;
    *)
        error "Unexpected expression '${expression}'"
        ;;
esac

如果表达式非常简单,可以使用简单模式:

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

2.9. 命名

  1. 文件名使用小写字母加下划线的形式,且以.sh结尾。
  2. 函数名使用小写字母加下划线的形式,包名使用::。
  3. 包中使用小写字母驼峰形式。
  4. 变量名使用小写字母加下划线的形式,局部变量尽量使用local修饰,减少变量名冲突。
  5. 常量使用大写字母加下划线形式,并且添加readonly修饰。
ysUtil::is_boot(){
    return 1
}

get_path() {
    echo "/dev/nvme0"
}

readonly MAX_PATH_LEN=256
test_dir() {
    local path_name
    path_name="$(get_path)" || return 1
    if [ ${#path_name} -gt $MAX_PATH_LEN ]; then 
        return 0
    fi
    
    return 1
}

2.10. 变量引用

  1. 针对参数或内置变量,可以不用{}。
  2. 针对字符串变量,默认添加{}。
  3. 针对数字变量,引用可以不加{}和字符串变量区别开。
# Special variables
echo $1 $2 $3
echo $? $!

# 当位置变量大于等于10,则必须有大括号:
echo "many parameters: ${10}"

# 当出现歧义时,必须有大括号:
# Output is "a0b0c0"
set -- a b c
echo "${1}0${2}0${3}0"

# 使用变量扩展赋值时,必须有大括号:
DEFAULT_MEM=${DEFUALT_MEM:-"-Xms2g -Xmx2g -XX:MaxDirectMemorySize=4g"}

# 其他常规变量的推荐处理方式:
echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}"
while read f; do
    echo "file=${f}"
done < <(ls -l /tmp)

2.11. 引用

引用通常情况下应遵循以下原则:
● 默认情况下推荐使用引号引用包含变量、命令替换符、空格或shell元字符的字符串
● 在有明确要求必须使用无引号扩展的情况下,可不用引号
● 字符串为单词类型时才推荐用引号,而非命令选项或者路径名
● 不要对整数使用引号
● 特别注意 [[ 中模式匹配的引号规则
● 在无特殊情况下,推荐使用 $@ 而非 $*
 

# '单引号' 表示禁用变量替换
# "双引号" 表示需要变量替换

# 1: 命令替换需使用双引号
flag="$(some_command and its args "$@" 'quoted separately')"

# 2:常规变量需使用双引号
echo "${flag}"

# 3:整数不使用引号
value=32
# 示例4:即便命令替换输出为整数,也需要使用引号
number="$(generate_number)"
echo "$value"

# 5:单词可以使用引号,但不作强制要求
readonly USE_INTEGER='true'

# 6:输出特殊符号使用单引号或转义
echo 'Hello stranger, and well met. Earn lots of $$$'
echo "Process $$: Done making \$\$\$."

# 7:命令参数及路径不需要引号
grep -li Hugo /dev/null "$1"

# 8:常规变量用双引号,ccs可能为空的特殊情况可不用引号
git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"}

# 9:正则用单引号,$1可能为空的特殊情况可不用引号
grep -cP '([Ss]pecial|\|?characters*)$' ${1:+"$1"}

# 10:位置参数传递推荐带引号的"$@",所有参数作为单字符串传递用带引号的"$*"
# content of t.sh
func_t() {
    echo num: $#
    echo args: 1:$1 2:$2 3:$3
}

func_t "$@"
func_t "$*"
# 当执行 ./t.sh a b c 时输出如下:
num: 3
args: 1:a 2:b 3:c
num: 1
args: 1:a b c 2: 3:

工程实践推荐用法

(摘自:https://blog.csdn.net/feihe0755/article/details/126484099)

3.4. Error输出到STDERR

所有的错误信息都应该输出到STDERR。

err() {
  echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2
}

if ! do_something; then
  err "Unable to do_something"
  exit 1
fi

3.5. 命令替换

使用新式语法$(command),不使用老式语法反引号,新语法可读性更高。

# good
var=$(cat name.txt|grep "100")

# bad
var=`cat name.txt|grep "100"`

3.6. 字符串匹配测试

优先使用[[ … ]],而不是[ … ], test,因为在 [[ 和 ]] 之间不会出现路径扩展或单词切分,所以使用 [[ … ]] 能够减少犯错,且 [[ … ]] 支持正则表达式匹配,而 [ … ] 不支持。

3.11. 文件加载

载入外部文件推荐使用source,代码可读性更好。

# 推荐
source base.sh

# 不推荐
. base.sh

3.12. 管道与参数

非必要情况,不使用管道传递参数,直接使用参数,效率更高。

# 推荐
grep "main" main.cpp
wc -l log.config

# 不推荐
cat main.cpp | grep "main"
cat log.config | wc -l

3.13. 数学计算

简单的数学计算可以使用(()),复杂的计算使用awk或bc。

# 推荐
(( i = 10 * j + 400 ))

# 可行,但是不推荐
i=$( expr 4 + 4 )
 

3.14. 检查命令返回值

需要检查命令返回值

if ! mv "${file_list[@]}" "${dest_dir}/"; then
  echo "Unable to move ${file_list[*]} to ${dest_dir}" >&2
  exit 1
fi

# Or
mv "${file_list[@]}" "${dest_dir}/"
if (( $? != 0 )); then
  echo "Unable to move ${file_list[*]} to ${dest_dir}" >&2
  exit 1
fi

3.15. 内部命令和外部命令

有一些命令,即支持外部命令工具,也支持Shell自带语法,更推荐使用自带内部命令,效率更高。

# 推荐使用内建的算术扩展
addition=$((${X} + ${Y}))
# 推荐使用内建的字符串替换
substitution="${string/#foo/bar}"

# 不推荐调用外部命令进行简单的计算
addition="$(expr ${X} + ${Y})"
# 不推荐调用外部命令进行简单的字符串替换
substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"

4. 补充

  1. 推荐使用ShellCheck,VS Code可以下载ShellCheck插件,自动检测代码规范。
  2. 参考Google Shell Style Guild

猜你喜欢

转载自blog.csdn.net/bandaoyu/article/details/130178173
今日推荐