我的 shell 最佳实践

shell 规范

可以使用 shellcheck 来检查 shell 的错误代码的警告和建议.

参照:https://www.jianshu.com/p/161618366866

  • 变量的命名
  • 头部引用:#!/usr/bin/env bash
  • 注释
  • 缩进
  • 变量的使用
  • 变量类型
  • 函数
  • 返回值
  • 计算
  • 分支

变量的命名

  1. 命名只能使用字母、数字,首个字符不能以数字开头。
  2. 单词均使用小写, 只读变量可使用大写。
  3. 变量中使用_来隔开字母,这是为了和数据库中的命名保持一致。
  4. 不能使用bash里的关键字,请在“附录1”中查看有哪些保留字

例如:

# 禁止:
local 123user="Tom"

# 推荐:
local user_123="Tom"


# 禁止:
User_Name="Tom"

# 推荐:
user_name="Tom"


# 禁止:
local userName="chenshang"

# 推荐:
local user_name="陈尚"

# 禁止:
local else="nihao"

# 推荐:
local nihao="nihao"

头部引用

我们往往看到大多数shell脚本的第一行是 #!/bin/bash 这句话,当然也有 #!/bin/sh、#!/usr/bin/bash,这几种写法也都算是正确,当然还有一些野路子的写法,为了避免误导这里就不示例了。本shell规约并不推荐使用上面的任何一种,而是使用 #!/usr/bin/env bash 这种。

#!/usr/bin/env bash
# 主函数 []<-()
function main(){
  echo "Hello World!!!"
}

shell脚本的第一行用来指定执行脚本的时候使用的默认解析器是什么, #!/bin/bash这样写就是指定使用 /bin 目录下的 bash来解析。大多数 linux 发行版中默认的shell就是bash,不同的shell下可用的命令不同,比如sh 就比bash 可用的基础命令少很多,这也就是为什么虽然sh是始祖却用的人很少,而它的增强版bash能够后来居上的原因。

shell脚本是逐行解释执行的,在遇到第一行是 #!/bin/bash 的时候就会加载 bash 相关的环境,在遇到 #!/bin/sh 就会加载 sh 相关的环境,避免在执行脚本的时候遇到意想不到的错误。但一开始我并不知道我电脑上安装了哪些shell,默认使用的又是哪一个shell,我脚本移植到别人的计算机上执行,我更不可能知道别人的计算机是Ubuntu还是Arch或是Centos。为了提高程序的移植性,本shell规约规定使用 #!/usr/bin/env bash, #!/usr/bin/env bash 会自己判断使用的shell是什么,并加载相应的环境变量。

我们看一下下面一段脚本,在改变第一行头部的时候,shellcheck给出的建议是什么

注释

  1. 除脚本首行外,所有以 # 开头的语句都将成为注释。
  2. 函数必须有注释标识该函数的用途、入参变量、函数的返回值类型,且必须简单在一行内写完。
  3. 函数的注释 # 顶格写, 井号后面紧跟一个空格。 这样可提供阅读体验。
  4. 函数内注释 # 与缩进格式对整齐
  5. 变量的注释紧跟在变量的后面

举例:

# 禁止:
                     <-------没有注释
function main(){
  local var="Hello World!!!"
  echo ${var}
}

# 必须:
# 主函数 []<-()
function main(){
  local var="Hello World!!!"
  echo ${var}
}


# 禁止:
# error级别的日志 []<-(msg:String) <-------带入参的函数注释
log_error(){
# todo [error]用红色显示
  # 将要输出的日志内容
  local msg=$1
  if [[ x"${msg}" != x"" ]];then
# 注释
    echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')][$$]:[error] $*" >&2
  fi

# 建议
# error级别的日志 []<-(msg:String) <-------带入参的函数注释
log_error(){
  # todo [error]用红色显示         <------函数内注释
  local msg=$1 # 将要输出的日志内容  <------变量的注释紧跟在变量的后面
  if [[ x"${msg}" != x"" ]];then
    # 注释                        <-------函数内注释 `#` 与缩进格式对整齐
    echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')][$$]:[error] $*" >&2
  fi
}

缩进

  1. 使用两个空格进行缩进,不适用tab缩进
  2. 不在一行的时候使用 \ 进行换行,使用 \ 换行的原则是整齐美观

# 禁止
# 脚本使用帮助文档 []<-()
manual(){
cat "$0"|grep -v "less \"\$0\"" |grep -B1 "function " |grep -v "\\--" |sed "s/function //g" |sed "s/(){//g" |sed "s/#//g"
}

# 鼓励
# 脚本使用帮助文档 []<-()
function manual(){
  cat "$0"|grep -v "less \"\$0\"" \ <----在函数体里面使用两个空格,来代表缩进
          |grep -B1 "function " \   <--- 使用 `\` 将很长的命令拆成多行,增强阅读体验
          |grep -v "\\--" \
          |sed "s/function //g" \
          |sed "s/(){//g" \
          |sed "s/#//g"
}

变量的使用

  1. 变量赋值使用 = 等号,左右不能留有空格
  2. 使用变量的时候,变量名一定要用{}包裹
  3. 使用变量的时候一定要用 双引号 "${}"包裹,防止变量中有空格
  4. 常量一定要定义成readonly。避免子 shell 中更改常量。
  5. 函数中的变量要用local修饰,定义成局部变量,这样在外部遇到重名的变量也不会影响
  6. 变量一经定义,不允许删除
# 禁止:
local user_name = "Tom"

# 必须
local user_name="Tom" <--- 将`=`号前后的空格删除掉


# 不建议:
another_var=$user_name

# 建议:
another_var="${user_name}" <-- 这里需要使用双引号

# 建议:
TURE=0 <-- 定义常量 TRUE

# 建议:
readonly TURE=0 <-- 定义常量 TRUE

# 不建议:
# 主函数:[]<-()
function main(){
  name="chenshang" #这里使用local定义一个局部变量
  web="${web}"     #这里${}内的web是全局变量,之后在函数中在使用web变量都是使用的局部变量
  web2="${web}"    #对于全局变量,虽然在使用的时候直接使用即可,但还是推荐使用一个局部变量进行接收,然后使用局部变量,以防止在多线程操作的时候出现异常(相当于java中的静态变量在多线程中的时候需要注意线程安全一样,但常量除外)
}

# 建议:
# 主函数:[]<-()
function main(){
  local name="chenshang" #这里使用local定义一个局部变量
  local web="${web}"     #这里${}内的web是全局变量,之后在函数中在使用web变量都是使用的局部变量
  local web2="${web}"    #对于全局变量,虽然在使用的时候直接使用即可,但还是推荐使用一个局部变量进行接收,然后使用局部变量,以防止在多线程操作的时候出现异常(相当于java中的静态变量在多线程中的时候需要注意线程安全一样,但常量除外)
}

注意: 单引号和双引号的区别
单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的,单引号字串中不能出现单引号(对单引号使用转义符后也不行)。
双引号中的普通字符都会原样输出,单引号中的使用$取值的变量会替换成响应变量的真实值得,然后在进行输出,双引号中可以出现单引号

函数

  1. 函数的定义中,必须使用 function 关键字,目前没有定义 private 的需求
  2. 在函数内部首先使用有意义的变量名接受参数,然后在使用这些变量进行操作,禁止直接操作$1,$2 等,使用见名知意的变量可以增强阅读体验。
  3. 函数入参和返回值类型,使用 []->() 表示。
# 建议:
# 函数备注
function main(){
  #函数执行的操作
  #函数的返回结果
}

# 不建议:
# 函数备注
main(){
  #函数执行的操作
  #函数的返回结果
}



# 建议:
# 函数备注
main(){
  start_date=$1
  end_date=$2
  #函数执行的操作
  hive --hivevar start_date="${start_date}"\
       --hivevar end_date="${end_date}"\
       -f xx.sql
  #函数的返回结果
}
 # 不建议:
# 函数备注
main(){
  #函数执行的操作
  hive --hivevar start_date="${1}"\
       --hivevar end_date="${2}"\
       -f xx.sql
  #函数的返回结果
}

# 函数入参和返回值类型举例子


[]<-()      <-- 无入参和返回值
[String]<-(var1:String,var2:String) <-- 两个 string 类型入参,返回值类型了是 String
[Boolean]<-(var1:String,var2:Int) <-- 第一个入参是 string , 第二个入参是 int 类型,返回值类型是 Boolean
[]<-(var1:String) <-- 只有一个 string 类型入参,无返回值

返回值

请用隐方(echo+本地变量)的方式来返回“返回值”

# 不建议:
# 检查当前系统版本 [Integer]<-()
function check_version(){
  # 函数处理过程
  rs=$(数据处理命令)
  return "${rs}"

}


# 建议:
# 检查当前系统版本 [Integer]<-()
function check_version(){
  # 函数处理过程
  rs=$(数据处理命令)
  echo "${rs}"

}


计算

  1. 整数计算使用 $(())
  2. 小数计算使用 bc 计算器

分支

  1. if

## 只有 if 的写法

if [[ condition ]]; then
  # statements
fi

##  if else 的写法
if [[ condition ]]; then
  # statements
else
  # statements
fi

##  if elif else 的写法
if [[ condition ]]; then
  # statements
elif [[ condition ]]; then
  # statements
else
  # statements
fi

需要注意的两点:

  1. if 后面的判断 使用 双中括号[[]]

  2. if [[ condition ]]; then 写在一行

  3. while

while [[ condition ]]; do
  # statements
done

## 读出文件中的每一行

while read -r item ;do
  # statements
done < 'file_name'
  1. until

until [[ condition ]]; do
  # statements
done
  1. for
for (( i = 0; i < 10; i++ )); do
  # statements
done

for item in ${array}; do
  # statements
done

  1. case
case word in
  pattern )
    #statements
    ;;
    *)
    #statements
    ;;
esac

附录1

命令

含义

!

保留字,逻辑非

:

不做任何事,只做参数展开

.

读取文件并在shell中执行它

alias

设置命令或命令行别名

bg

将作业置于后台运行

bind

将关键字序列与readline函数或宏捆绑

break

保留字,跳出for、while、until、select循环

builtin

调用命令的内建命令格式,而禁用同名的函数。或者同名的扩展命令

case

保留字,多重选择

cd

切换当前工作目录

command

找出内建和外部命令;寻找内建命令而非同名函数

continue

保留字,到达下次for、while、until、select循环

declare

声明变量定义变量属性

dirs

显示当前存储的列表

disown

将作业从列表中移除

do

保留字,for、while、until、select循环的一部分

done

保留字,for、while、until、select循环的一部分

echo

打印参数

elif

保留字,if结构的一部分

else

保留字,if结构的一部分

enable

开启和关闭内建命令

esac

保留字,case的一部分

eval

将参数作为命令再次处理一遍

exec

以特定程序取代shell或为shell改变I/O

exit

退出shell

export

将变量声明为环境变量

fc

与历史命令一起运行

fg

将作业置于后台运行

fi

保留字,if循环的一部分

for

保留字,for循环的一部分

function

定义一个函数

getops

处理命令行选项

hash

记录并指定命令的路径名

help

显示内建命令的帮助信息

history

显示历史信息

if

保留字,if循环的一部分

in

保留字,case、for、select循环的一部分

jobs

显示后台运行的作业

kill

向进程传送信号

let

使变量执行算术运算

local

定义局部变量

logout

从Shell中注销

popd

从目录栈中弹出目录

pushd

将目录压入栈

pwd

显示当前工作目录

read

从标准输入中读取一行

readonly

将变量定义为只读

return

从函数或脚本返回

select

保留字,生成菜单

set

设置Shell选项

shift

变换命令行参数

suspend

终止Shell的执行

test

评估条件表达式

then

保留字,if结构的一部分

time

保留字,输出统计出来的命令执行时间,其输出格式由TIMEFORMAT变量来控制

times

针对Shell及其子Shell,显示用户和系统CPU的时间和

trap

设置扑捉程序

type

确定命令的源

typeset

声明变量,定义变量属性,与declare等价

ulimit

设置和显示进程占用的资源限制

umask

设置和显示文件权限码

unalias

取消别名定义

unset

取消变量或函数定义

until

保留字,一种循环结构

wait

等待后台作业完成

while

保留字,一种循环结构

附录2

如何使用脚本单独调用函数中的某个函数


#!/usr/bin/env bash
# shellcheck disable=SC1091,SC2155
readonly local TRUE=0 && readonly local FALSE=1
# 脚本使用帮助文档
manual(){
  cat "$0"|grep -v "less \"\$0\"" \
          |grep -B1 "function "   \
          |grep -v "\\--"         \
          |sed "s/function //g"   \
          |sed "s/(){//g"         \
          |sed "s/#//g"           \
          |sed 'N;s/\n/ /'        \
          |column -t              \
          |awk '{print $1,$3,$2}' \
          |column -t
}
######################################################################
# 主函数
main(){
  (manual)
}
######################################################################
# 执行函数 [Any]<-(function_name:String,function_parameters:List<Any>)
execute(){
  function_name=$1
  shift # 参数列表以空格为分割左移一位,相当于丢弃掉第一个参数
  function_parameters=$*
  (${function_name} "${function_parameters}")
}
case $1 in
  "-h" | "--help" | "?") (manual);;
  "") (main) ;;
  *) (execute "$@") ;;
esac

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值