Bash是唯一被允许执行的shell脚本语言。
可执行文件必须以 #!/bin/bash
和最小数量的标志开始。请使用 set
来设置shell的选项,使得用 bash <script_name>
调用你的脚本时不会破坏其功能。
限制所有的可执行shell脚本为bash使得我们安装在所有计算机中的shell语言保持一致性。
无论你是为什么而编码,对此唯一例外的是当你被迫时可以不这么做的。其中一个例子是Solaris SVR4包,编写任何脚本都需要用纯Bourne shell。
Shell应该仅仅被用于小功能或者简单的包装脚本。
尽管Shell脚本不是一种开发语言,但在整个谷歌它被用于编写多种实用工具的脚本。这个风格指南更多的是认同它的使用,而不是一个建议,即它可被用于广泛部署。
以下是一些准则:
${PHPESTATUS}
),那么你应该使用Python脚本。可执行文件应该没有扩展名(强烈建议)或者使用.sh扩展名。库文件必须使用.sh作为扩展名,而且应该是不可执行的。
当执行一个程序时,并不需要知道它是用什么语言编写的。而且shell脚本也不要求有扩展名。所以我们更喜欢可执行文件没有扩展名。
然而,对于库文件,知道其用什么语言编写的是很重要的,有时候会需要使用不同语言编写的相似的库文件。使用.sh这样特定语言后缀作为扩展名,就使得用不同语言编写的具有相同功能的库文件可以采用一样的名称。
SUID(Set User ID)和SGID(Set Group ID)在shell脚本中是被禁止的。
shell存在太多的安全问题,以致于如果允许SUID/SGID会使得shell几乎不可能足够安全。虽然bash使得运行SUID非常困难,但在某些平台上仍然有可能运行,这就是为什么我们明确提出要禁止它。
如果你需要较高权限的访问请使用 sudo
。
所有的错误信息都应该被导向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
每个文件的开头是其文件内容的描述。
每个文件必须包含一个顶层注释,对其内容进行简要概述。版权声明和作者信息是可选的。
例如:
#!/bin/bash
#
# Perform hot backups of Oracle databases.
任何不是既明显又短的函数都必须被注释。任何库函数无论其长短和复杂性都必须被注释。
其他人通过阅读注释(和帮助信息,如果有的话)就能够学会如何使用你的程序或库函数,而不需要阅读代码。
所有的函数注释应该包含:
例如:
#!/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() {
...
}
注释你代码中含有技巧、不明显、有趣的或者重要的部分。
这部分遵循谷歌代码注释的通用做法。不要注释所有代码。如果有一个复杂的算法或者你正在做一些与众不同的,放一个简单的注释。
使用TODO注释临时的、短期解决方案的、或者足够好但不够完美的代码。
这与C++指南中的约定相一致。
TODOs应该包含全部大写的字符串TODO,接着是括号中你的用户名。冒号是可选的。最好在TODO条目之后加上 bug或者ticket 的序号。
例如:
# TODO(mrmonkey): Handle the unlikely edge cases (bug ###)
缩进两个空格,没有制表符。
在代码块之间请使用空行以提升可读性。缩进为两个空格。无论你做什么,请不要使用制表符。对于已有文件,保持已有的缩进格式。
行的最大长度为80个字符。
如果你必须写长度超过80个字符的字符串,如果可能的话,尽量使用here document或者嵌入的换行符。长度超过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."
如果一行容不下整个管道操作,那么请将整个管道操作分割成每行一个管段。
如果一行容得下整个管道操作,那么请将整个管道操作写在同一行。
否则,应该将整个管道操作分割成每行一个管段,管道操作的下一部分应该将管道符放在新行并且缩进2个空格。这适用于使用管道符'|'的合并命令链以及使用'||'和'&&'的逻辑运算链。
# All fits on one line
command1 | command2
# Long commands
command1 \
| command2 \
| command3 \
| command4
请将 ; do
, ; then
和 while
, for
, if
放在同一行。
shell中的循坏略有不同,但是我们遵循跟声明函数时的大括号相同的原则。也就是说, ; 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
;;
之前各需要一个空格。;;
在不同的行。匹配表达式比 case
和 esac
缩进一级。多行操作要再缩进一级。一般情况下,不需要引用匹配表达式。模式表达式前面不应该出现左括号。避免使用 ;&
和 ;;&
符号。
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
按优先级顺序:保持跟你所发现的一致;引用你的变量;推荐用 ${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"
[[
中模式匹配的引用规则。$@
除非你有特殊原因需要使用 $*
。# '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 "$#, $@")
使用 $(command)
而不是反引号。
嵌套的反引号要求用反斜杠转义内部的反引号。而 $(command)
形式嵌套时不需要改变,而且更易于阅读。
例如:
# This is preferred:
var="$(command "$(command1)")"
# This is not:
var="`command \`command1\``"
推荐使用 [[ ... ]]
,而不是 [
, 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" =~ ^[[:alnum:]]+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
尽可能使用引用,而不是过滤字符串。
Bash足以在测试中处理空字符串。所以,请使用空(非空)字符串测试,而不是过滤字符,使得代码更易于阅读。
# 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
为了避免对你测试的目的产生困惑,请明确使用-z
或者-n
# 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
当进行文件名的通配符扩展时,请使用明确的路径。
因为文件名可能以 -
开头,所以使用扩展通配符 ./*
比 *
来得安全得多。
# 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'
应该避免使用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)"
请使用过程替换或者for循环,而不是管道导向while循环。在while循环中被修改的变量是不能传递给父shell的,因为循环命令是在一个子shell中运行的。
管道导向while循环中的隐式子shell使得追踪bug变得很困难。
last_line='NULL'
your_command | while read line; do
last_line="${line}"
done
# This will output 'NULL'
echo "${last_line}"
如果你确定输入中不包含空格或者特殊符号(通常意味着不是用户输入的),那么可以使用一个for循环。
total=0
# Only do this if there are no spaces in return values.
for value in $(command); do
total+="${value}"
done
使用过程替换允许重定向输出,但是请将命令放入一个显式的子shell中,而不是bash为while循环创建的隐式子shell。
total=0
last_file=
while read count filename; do
total+="${count}"
last_file="${filename}"
done < <(your_command | uniq -c)
# This will output the second field of the last line of output from
# the command.
echo "Total = ${total}"
echo "Last one = ${last_file}"
当不需要传递复杂的结果给父shell时可以使用while循环。这通常需要一些更复杂的"解析"。请注意简单的例子使用如awk这类工具可能更容易完成。当你特别不希望改变父shell的范围变量时这可能也是有用的。
# Trivial implementation of awk expression:
# awk '$3 == "nfs" { print $2 " maps to " $1 }' /proc/mounts
cat /proc/mounts | while read src dest type opts rest; do
if [[ ${type} == "nfs" ]]; then
echo "NFS ${dest} maps to ${src}"
fi
done
使用小写字母,并用下划线分隔单词。使用双冒号 ::
分隔库。函数名之后必须有圆括号。关键词 function
是可选的,但必须在一个项目中保持一致。
如果你正在写单个函数,请用小写字母来命名,并用下划线分隔单词。如果你正在写一个包,使用双冒号 ::
来分隔包名。大括号必须和函数名位于同一行(就像在Google的其他语言一样),并且函数名和圆括号之间没有空格。
# Single function
my_func() {
...
}
# Part of a package
mypackage::my_func() {
...
}
当函数名后存在 ()
时,关键词 function
是多余的。但是其促进了函数的快速辨识。
如函数名。
循环的变量名应该和循环的任何变量同样命名。
for zone in ${zones}; do
something_with "${zone}"
done
全部大写,用下划线分隔,声明在文件的顶部。
常量和任何导出到环境中的都应该大写。
# Constant
readonly PATH_TO_FILES='/some/path'
# Both constant and environment
declare -xr ORACLE_SID='PROD'
第一次设置时有一些就变成了常量(例如,通过getopts)。因此,可以在getopts中或基于条件来设定常量,但之后应该立即设置其为只读。值得注意的是,在函数中 declare
不会对全局变量进行操作。所以推荐使用 readonly
和 export
来代替。
VERBOSE='false'
while getopts 'v' flag; do
case "${flag}" in
v) VERBOSE='true' ;;
esac
done
readonly VERBOSE
小写,如果需要的话使用下划线分隔单词。
这是为了和在Google中的其他代码风格保持一致: maketemplate
或者 make_template
,而不是 make-template
。
使用 readonly
或者 declare -r
来确保变量只读。
因为全局变量在shell中广泛使用,所以在使用它们的过程中捕获错误是很重要的。当你声明了一个变量,希望其只读,那么请明确指出。
zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)"
if [[ -z "${zip_version}" ]]; then
error_message
else
readonly zip_version
fi
使用 local
声明特定功能的变量。声明和赋值应该在不同行。
使用 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
...
}
将文件中所有的函数一起放在常量下面。不要在函数之间隐藏可执行代码。
如果你有函数,请将他们一起放在文件头部。只有includes, set
声明和常量设置可能在函数声明之前完成。不要在函数之间隐藏可执行代码。如果那样做,会使得代码在调试时难以跟踪并出现意想不到的讨厌结果。
对于包含至少一个其他函数的足够长的脚本,需要称为 main
的函数。
为了方便查找程序的开始,将主程序放入一个称为 main
的函数,作为最下面的函数。这使其和代码库的其余部分保持一致性,同时允许你定义更多变量为局部变量(如果主代码不是一个函数就不能这么做)。文件中最后的非注释行应该是对 main
函数的调用。
main "$@"
显然,对于仅仅是线性流的短脚本, main
是矫枉过正,因此是不需要的。
总是检查返回值,并给出信息返回值。
对于非管道命令,使用 $?
或直接通过一个 if
语句来检查以保持其简洁。
例如:
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
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
可是,只要你运行任何其他命令, PIPESTATUS
将会被覆盖。如果你需要基于管道中发生的错误执行不同的操作,那么你需要在运行命令后立即将 PIPESTATUS
赋值给另一个变量(别忘了 [
是一个会将 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
可以在调用shell内建命令和调用另外的程序之间选择,请选择内建命令。
我们更喜欢使用内建命令,如在 bash(1)
中参数扩展函数。因为它更强健和便携(尤其是跟像 sed
这样的命令比较)
例如:
# Prefer this:
addition=$((${X} + ${Y}))
substitution="${string/#foo/bar}"
# Instead of this:
addition="$(expr ${X} + ${Y})"
substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"
使用常识并保持一致。
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。