Shell 是用户与 Linux 或 Unix 内核通信的工具,shell 编程指的并不是编写这个工具,而是指利用现有的 shell 工具进行编程,写出来的程序是轻量级的脚本,我们叫做 shell 脚本。
Shell 的语法是从C语言继承过来的,因此我们在写 shell 脚本的时候往往能看到C语言的影子。
Shell 脚本实在是太灵活了,相比标准的 Java、C、C++ 等,它不过是一些现有命令的堆叠,这是他的优势也是他的劣势,太灵活导致不容易书写规范。以下整理本人在写 shell 脚本的过程中形成了自己一些规范,这些规范还在实践中,在此分享出来,以期更多的人来帮助我完善。
# 命名
- 命名只能使用字母,数字和下划线,首个字符不能以数字开头
- 中间不能有空格,不能使用标点符号,不能使用汉字,可以使用下划线
_
,所以我们往往使用_
作为分词的标识,例如user_name
、city_id
等等 - 不能使用 bash 里的关键字(可用
help
命令查看保留关键字) - 脚本中的所有变量风格统一使用下划线命名风格(不强制,视情况而定)
统一的风格是好的编程习惯的开始,这样程序给人一种清爽的感觉,至于使用
驼峰
格式还是使用下划线
格式,仁者见仁智者见智。
# 首行
使用 #!/usr/bin/env bash
我们看到大多数 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 是什么,并加载相应的环境变量。
# 注释
- 除脚本首行外,所有以
#
开头的语句都将成为注释 - 变量的注释紧跟在变量的后面
- 函数内注释
#
与缩进格式对整齐 - 函数必须有注释标识该函数的用途、入参变量、函数的返回值类型,且必须简单在一行内写完
- 函数的注释
#
顶格写,井号后面紧跟一个空格,对于该格式的要求是为了最后生成函数的帮助文档(markdown语法),然后是注释的内容,注释尽量简短且在一行,最后跟的是函数的类型
# 主函数 []<-() <-------函数注释这样写
function main(){
local var="Hello World!!!"
echo ${var}
}
# info级别的日志 []<-(msg:String) <-------带入参的函数注释
log_info(){
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')][$$]: [info] $*" >&2
}
[]<-()
:<-
左侧表的是函数的返回值类型,用[]
包裹,右侧是函数的参数类型,用()
包裹,多个参数用,
分隔
函数注释的几种常见写法:
[]<-()
[String]<-(var1:String,var2:String)
[Boolean]<-(var1:String,var2:Int)
[]<-(var1:String)
# 缩进
- 使用2/4空格进行缩进(二选一,一致即可),不使用 tab 缩进
- 不在一行的时候使用
\
进行换行,使用\
换行的原则是整齐美观
#!/usr/bin/env bash
# 脚本使用帮助文档 []<-()
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
}
function search_user_info(){
local result=$(httpclient_get --cookie "${cookie}" \
"${url}/userName=${user_name}")
}
# 变量
变量赋值使用
=
等号,左右不能留有空格使用变量的值用
$
取值符号使用变量的时候,变量名一定要用
{}
包裹使用变量的时候一定要用双引号
"${}"
包裹var1="Hello World" # 正确,推荐使用双引号 var2='Hello World' # 正确,不推荐使用单引号 var3="${var1}" # 应用前面定义的变量的时候也要使用双引号包裹 var4=6 var5=6.70 # 小数 var3=${var1} # 正确,不推荐
常量一定要定义成
readonly
,这种变量不能使用source
跨 shell 使用 比如一个a.sh
中定义了一个全局的变量readonly TURE=0
,b.sh
中在一开始使用source a.sh
引入的a.sh
的内容,则在b.sh
中无需重复定义readonly local TRUE=0
,否则会报错函数中的变量要用
local
修饰,定义成局部变量,这样在外部遇到重名的变量也不会影响web="www.chen-shang.github.io" function main(){ # 这里使用 local 定义一个局部变量 local name="chenshang" # 这里 ${} 内的 web 是全局变量,之后在函数中在使用 web 变量都是使用的局部变量 local web="${web}" # 对于全局变量,虽然在使用的时候直接使用即可,但还是推荐使用一个局部变量进行接收,然后使用局部变量,以防止在多线程操作的时候出现异常 local web2="${web}" }
变量一经定义,不允许删除(也就是禁用
unset
命令,因为到目前我还没遇到过什么情况必须unset
的)
# 变量类型
先在这儿声明一下,shell 脚本并没有什么复杂的数据类型和数据结构,shell 的数据类型仅仅包括 字符串、数值
。shell 并没有像 布尔类型、长整型、短整形
等 Java 中的基本数据类型,数值类型也没有明确的 float 和 double 之分。shell 比较复杂的数据类型就是数组
和 Map
了,但Map
这种数据类型或者说数据结构只有在高版本的 shell 解析器才拥有,像我们现在使用的 Linux 系统往往是不支持 Map
的。
shell 中变量的基本类型就是 String
、数值
(可以自己看做 Int、Double之类的) 、Boolean
。Boolean
其实是 Int
类型的变种,在 shell 中 0代表真、非0代表假
, 所以往往我会在 shell 脚本中用 readonly TURN=0 && readonly FALSE=1
- 定义在函数中的我们称之为函数局部变量;定义在函数外部,shell 脚本中变量我们称之为脚本全局变量
环境变量
:所有的程序,包括 shell 启动的程序,都能访问环境变量,有些程序需要环境变量来保证其正常运行。必要的时候 shell 脚本也可以定义环境变量。
# 函数
函数定义的形式是:
function main(){
# 函数执行的操作
# 函数的返回结果
}
或:
main(){
# 函数执行的操作
# 函数的返回结果
}
使用关键字
function
显示定义的函数为public
的函数,可以供外部脚本以sh 脚本 函数 函数入参
的形式调用未使用关键字
function
显示定义的函数为 private 的函数,仅供本脚本内部调用,注意这种private 是人为规定的,并不是 shell 的语法,不推荐以sh 脚本 函数 函数入参
的形式调用,注意是不推荐而不是不能本 shell 规约这样做的目的就在于使脚本具有一定的封装性,看到
function
修饰的就知道这个函数能被外部调用,没有被修饰的函数就仅供内部调用。你就知道如果你修改了改函数的影响范围,如果是 被function
修饰的函数,修改后可能影响到外部调用他的脚本,而修改未被function
修饰的函数的时候,仅仅影响本文件中其他函数。栗子,
core.sh
脚本内容如下:# 重新设置DNS地址 []<-() function set_name_server(){ > /etc/resolv.conf echo nameserver 114.114.114.114 >> /etc/resolv.conf echo nameserver 8.8.8.8 >> /etc/resolv.conf cat /etc/resolv.conf } # info级别的日志 []<-(msg:String) log_info(){ echo -e "[$(date +'%Y-%m-%dT%H:%M:%S%z')][$$]: \033[32m [info] \033[0m $*" >&2 } # error级别的日志 []<-(msg:String) log_error(){ # todo [error]用红色显示 echo -e "[$(date +'%Y-%m-%dT%H:%M:%S%z')][$$]: \033[31m [error] \033[0m $*" >&2 }
则我可以使用
sh core.sh set_name_server
的形式调用set_name_server
函数,但就不推荐使用sh core.sh log_info "Hello World"
的形式使用log_info
和log_error
函数,注意是不推荐不是不能。在函数内部首先使用有意义的变量名接受参数,然后在使用这些变量进行操作,禁止直接操作
$1
、$2
等,除非这些变量只用一次函数的注释,函数类型的概念是从函数编程语言中的概念偷过来的,shell函数的函数类型指的是函数的输入到函数的输入的映射关系
# 返回值
shell 函数的返回值比较复杂,获取函数的返回值又有多种方式。我们先来说,我们执行一条命令的时候,比如 pwd
正常情况下它输出的结果是当前所处的目录
xiejiancong@xiejc-pro ~ $ pwd
/Users/xiejiancong
xiejiancong@xiejc-pro ~ $
注意我说的是正常情况下,那异常情况下呢?输出的结果又是什么?输出的结果有可能五花八门!
所以,shell 中必然有一种状态来标识一条命令是否执行成功,也就是命令执行结果的状态。
那这种状态是怎么标识的的,这就引出了 shell 中一个反人类的规定,也不算反人类,只能是在变种语言中算是另类,就是 0
代表真、成功的含义。非零
代表假、失败的含义。
所以 pwd
这条命令如果执行成功的话,命令的执行结果状态一定是 0
,然后返回值才是当前目录。如果这条命令执行失败的话,命令的执行结果状态一定不是0
,有可能是 1
代表命令不存在,然后输出 not found
,也有可能执行结果状态是 2
代表超时,然后什么也不输出。(不要以为 pwd
这种 linux 内带的命令就一定执行成功,有可能你拿到的就是一台阉割版的 linux 呢),那怎么获取这个命令的执行结果和执行结果的状态呢?
function main(){
pwd
}
执行 main 函数就会在控制台输出当前目录。如果想要将 pwd
的内容获取到变量中以供后续使用呢:
function main(){
local dir=$(pwd)
echo "dir is ${dir}"
}
如果想要获取 pwd
的执行结果的状态呢:
function main(){
local dir=$(pwd)
local status=$?
echo "pwd run status is ${status}" #这个stauts一定有值,且是int类型,取值范围在0-255之间
echo "dir is ${dir}"
}
显式 return
:
return
用来显式的返回函数的返回结果,例如:
# 检查当前系统版本 [Integer]<-()
function check_version(){
(log_info "check_version ...") # log_info是我写的工具类中的一个函数
local version # 这里是先定义变量,在对变量进行赋值,我们往往是直接初始化,而不是像这样先定义在赋值,这里只是告诉大家可以这么用
version=$(sed -r 's/.* ([0-9]+)\..*/\1/' /etc/redhat-release)
(log_info "centos version is ${version}")
return "${version}"
}
这样这个函数的返回值是一个数值类型,我在脚本的任何地方调用 check_version
这个函数后,使用 $?
获取返回值
check_version
local version=$?
echo "${version}"
注意这里不用 local version=$(check_version)
这种形式获取结果,这样也是获取不到结果的,因为显示的 return
结果,返回值只能是 [0-255] 的数值,这对于我们一般的函数来说就足够了,因为我们使用显示 return
的时候往往是知道返回结果一定是数字且在 [0-255] 之间的,常常用在状态判断的时候。所以,本 shell 规约规定:
- 明确返回结果是在 [0-255] 之间的数值类型的时候使用显示
return
返回结果 - 返回结果类型是
Boolean
类型,也就是说函数的功能是起判断作用,返回结果是真或者假的时候使用显示return
返回结果
栗子:
# 检查网络 [Boolean]<-()
function check_network(){
(log_info "check_network ...")
for((i=1;i<=3;i++));do
http_code=$(curl -I -m 10 -o /dev/null -s -w %\{http_code\} www.baidu.com)
if [[ ${http_code} -eq 200 ]];then
(log_info "network is ok")
return ${TRUE}
fi
done
(log_error "network is not ok")
return ${FALSE}
}
# 获取数组中指定元素的下标 [int]<-(TABLE:Array,target:String)
function get_index_of(){
readonly local array=($1)
local target=$2
local index=-1 # -1其实是255
local size=${#array[@]}
for ((i=0;i<${size};i++));do
if [[ ${array[i]} == ${target} ]];then
return ${i}
fi
done
return ${index}
}
# 计算
- 整数计算使用
$(())
- 小数计算使用 bc 计算器
# 分支
HEAD_KEYWORD parameters; BODY_BEGIN
BODY_COMMANDS
BODY_END
- 将
HEAD_KEYWORD
和初始化命令或者参数放在第一行 - 将
BODY_BEGIN
同样放在第一行 - 复合命令中的
BODY
部分以 2/4 个空格缩进 BODY_END
部分独立一行放在最后
if
:
if [[ condition ]]; then
# statements
fi
if [[ condition ]]; then
# statements
else
# statements
fi
if [[ condition ]]; then
# statements
elif [[ condition ]]; then
# statements
else
# statements
fi
if
后面的判断使用双中括号[[]]
if [[ condition ]]; then
写在一行
while
:
while [[ condition ]]; do
# statements
done
while read -r item ;do
# statements
done < 'file_name'
until
:
until [[ condition ]]; do
# statements
done
for
:
for (( i = 0; i < 10; i++ )); do
# statements
done
for item in ${array}; do
# statements
done
case
:
case word in
pattern )
#statements
;;
*)
#statements
;;
esac