浅墨散人 浅墨散人
  • 基础
  • 设计模式
  • JVM
  • Maven
  • SpringBoot
  • 基础
  • Flask
  • Diango
  • Pandas
  • SqlAlchemy
  • Sqoop
  • Flume
  • Flink
  • Hadoop
  • Hbase
  • Hive
  • Kafka
  • Kylin
  • Zookeeper
  • Tez
  • MySQL
  • Doris
  • Chrome
  • Eclipse
  • IDEA
  • iTerm2
  • Markdown
  • SublimeText
  • VirtualBox
  • WebStrom
  • Linux
  • Mac
  • Hexo
  • Git
  • Vue
  • VuePress
  • 区块链
  • 金融
数据仓库
数据治理
读书笔记
关于我
GitHub (opens new window)
  • 基础
  • 设计模式
  • JVM
  • Maven
  • SpringBoot
  • 基础
  • Flask
  • Diango
  • Pandas
  • SqlAlchemy
  • Sqoop
  • Flume
  • Flink
  • Hadoop
  • Hbase
  • Hive
  • Kafka
  • Kylin
  • Zookeeper
  • Tez
  • MySQL
  • Doris
  • Chrome
  • Eclipse
  • IDEA
  • iTerm2
  • Markdown
  • SublimeText
  • VirtualBox
  • WebStrom
  • Linux
  • Mac
  • Hexo
  • Git
  • Vue
  • VuePress
  • 区块链
  • 金融
数据仓库
数据治理
读书笔记
关于我
GitHub (opens new window)
  • Linux

    • Linux
    • Shell基础
    • Linux中yum与rpm区别
    • Linux的date常用操作
    • Shell编码规范
      • 命名规范
        • 文件
        • 全局变量
        • 函数名
        • 局部变量
      • 格式
        • 解释器(shebang)
        • 注释
        • 行宽
        • 缩进
        • 空行和空格
        • 全局选项
        • 函数定义
        • 分支
        • 循环
        • Case
        • 变量引用
        • 引号
        • 导入其他文件
        • 命令替换(subshell)
        • 条件测试
        • 算术运算
        • Here document
      • 惯例
        • 环境
        • 错误输出
        • 当前目录
        • 临时文件
        • 检查输入
        • Main 函数
        • '&&' 和 '||'
        • Exit vs return
        • Builtin vs 外部命令
      • 陷阱
        • 选项 errexit
        • Subshell 给 local 变量赋值
        • Shift
      • 参考
    • Shell批量执行脚本
  • System
  • Linux
2018-10-10
目录

Shell编码规范

# Shell编码规范

shell-style-guide-cn (opens new window)

[Google 开源项目风格指南] (opens new window)

# 命名规范

# 文件

  • 文件名:全小写,扩展名为 .sh,文件名要有明确含义
    • 主文件的文件名必要时采用连接符分隔,如 build-foobar.sh
    • 函数库文件名以 lib 开始,如 liblog.sh
  • 文件编码:utf-8,以 LF (\n) 分隔行,CR (\r) 将引起错误
  • 权限位:主文件加执行权限(chmod +x),但函数库文件不加

# 全局变量

定义在所有函数之外的变量为全局变量。

  • 全部大写,单词间以下划线连接
  • 定义后不会修改的以 readonly 修饰

例如:

readonly TOP_DIR="/opt/myapp"

COMMAND_ARGS="$*"
1
2
3

# 函数名

全部小写,单词间以下划线连接。

# 局部变量

定义在函数内部的变量为“局部变量”,但如果不加 local 修饰将全局可见。

  • 全部小写,单词间以下划线连接
  • 在函数开始处统一以 local 先声明后使用
  • 整型数额外加 -i 修饰
  • 数组额外加 -a 修饰

例如:

function status_check
{
    local prog=${1:?}
    local -i counter
    local -a args

    ...
}
1
2
3
4
5
6
7
8

# 格式

# 解释器(shebang)

用 #!/bin/bash,无空格,不带选项。

# 注释

注释用 # 紧跟一个空格开始,用英文。

紧接 shebang 后之后要撰写头部注释:

  • 解释文件的用途
  • 简要解释如何使用(详细的写 usage 函数提供)
  • 公司版权声明(如果有)

其他约定:

  • 代码中的注释,多行的在最后一行注释后额外加入一个空注释行
  • 行尾注释,# 符号之前至少留出两个空格
  • 必要时使用特殊标记:TODO,FIXME,XXX,多数编辑器能高亮显示它们

例如:

# This file stores the pid of the parent process
PID_FILE="/var/run/foo_daemon.pid"

# This function probes a tcp port by trying to establish
# a connection without sending any data. Returns 0 on
# success, 1 otherwise. TODO: implement an udp version
#
function tcp_port_test
{
    local timeout=5  # in second
    ...

    # FIXME: detect whether nc is installed
    nc -w $timeout $host $port

    # XXX: on older system '-w' is not for connection timeout
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

提示:vimrc 中设置 set formatoptions=tcroqnmMB 后,编辑多行注释时可在 Normal 模式下按 V 进入行模式,j 和 k 选择多行,然后 gq 即可按当前行宽重排注释。

# 行宽

尽可能保持在 80 列以内,以便适应并排对比和打印输出的场合,使用 backslash \ 断行。参考一些断行的写法:

echo "Here we output a very very long message need to break into multiple" \
     "lines with backslashes"

tar -Pzcf $backup_dir/host-config.tar.gz \
    /etc/ssh/ssh_host* \
    /etc/ssh/sshd_config \
    /etc/hosts \
    /etc/postfix/main.cf \
    /etc/postfix/relay_passwd
1
2
3
4
5
6
7
8
9

以下 vimrc 配置可将第 80 列的字符标成红色。更多请见 参考 vimrc 配置 (opens new window)。

highlight! link CharAtCol80 WarningMsg
match CharAtCol80 /\%80v/
1
2

# 缩进

用四个空格缩进。以下示例 vimrc 配置设定 tab 为 soft tab 四个空格,为 Makefile 保持 hard tab。更多请见 参考 vimrc 配置 (opens new window)。

set expandtab softtabstop=4 shiftwidth=4 tabstop=8
autocmd! BufEnter *[Mm]akefile*,[Mm]ake.*,*.mak,*.make setlocal filetype=make
autocmd! FileType make setlocal noexpandtab shiftwidth=8
1
2
3

# 空行和空格

使用单行空行分隔代码中的小段逻辑增加可读性。不留多余空格。使用下面 vim 配置可以高亮显示去多余空白,且 Normal 模式下按空格开关显示。更多请见 参考 vimrc 配置 (opens new window)。

set list listchars=tab:▸\ ,trail:▌
nmap <Space> :set list!<CR>
1
2

注:Windows 系统下对 utf-8 字符显示支持不完善,可考虑更换上面的两个特殊字符为 > 和 _。

# 全局选项

  • 始终使用 set -o nounset 确保变量已经定义
  • 在主文件且只在主文件中使用 set -o errexit 第一时间捕获错误

注:参考后面的“陷阱”一节。

# 函数定义

采用 function func_name 并将开括弧放在下一行,不用多余的 () 记号。这样至少有两个好处:

  • 从文件中查找所有函数定义只需要 grep ^function 即可
  • Vim 中设置 smartindent 之后下一行会正确自动缩进
function func_name
{
    ...
}
1
2
3
4

# 分支

将 then 放在和 if 同一行。

if condition 1; then
    ...
elif condition 2; then
    ...
else
    ...
fi
1
2
3
4
5
6
7

# 循环

将 do 放在和 for 和 while 同一行。

for x in "foo" "bar" "quz"; do
    ...
done

while true; do
    ...
done
1
2
3
4
5
6
7

# Case

  • 分支缩进四格
  • 结束符 ;; 单独一行
case "$OS" in
    Linux)
        echo "This is a Linux system"
        ;;
    Darwin)
        echo "This is a Mac system"
        ;;
    *)
        echo "Unknown OS $OS" >&2
        ;;
esac
1
2
3
4
5
6
7
8
9
10
11

# 变量引用

可能引起阅读困难时使用 ${VAR},例如对比下面两种写法,使用第一种。

# Good
LOG_FILE="${LOG_PREFIX}-${PROG_NAME}-${LOG_SUFFIX}"

# Bad
LOG_FILE="$LOG_PREFIX-$PROG_NAME-$LOG_SUFFIX"
1
2
3
4
5

# 引号

因为需要引用变量的场合占多数,字符串使用双引号,除非下面两种情况:

  • 需要规避变量展开
  • 字符串内部有多处双引号需要转义
readonly TOP_DIR="/opt/myapp"
readonly CONFIG="$TOP_DIR/myapp.conf"
1
2

使用单引号场合示例:

function start
{
    local banner='Welcome to the "Awesome Portal"'
    local pattern='loaded$'
1
2
3
4

# 导入其他文件

用 source 而不是点 . 因为前者可读性更好。

# 命令替换(subshell)

使用 $(..) 而不是 `..` 因为前者可读性更好并且能嵌套。

SELF_DIR=$(cd $(dirname $0) && pwd)
1

# 条件测试

使用 [[ .. ]] 而不是 [ .. ] 或 test,因为第一种容错性最好。

if [[ -n "$FOO" ]]; then
    ..
fi
1
2
3

不用 [[ "x$FOO" == "x" ]] 这种传承自古老 Unix 系统的写法,改用 [[ -z "$FOO" ]]。

# 算术运算

使用 (( .. )),注意类似 [[ .. ]] 内部前后各留一个空格。括弧内的变量不使用 $ 引用。

(( COUNTER += 1 ))

if (( COUNTER > TIMEOUT )); then
    ..
fi
1
2
3
4
5

# Here document

需要写大段文本或模板时采用 here document。无需变量展开时结束符使用双引号。

function usage
{
    cat << EOT

Usage: $0 [options]

--help          Display this message and exit
--file FILE     Specify target filename, default is /etc/myapp.conf

EOT
}

MESSAGE=$(cat << "EOT"
Prices list:

- Foo: $2.99
- Bar: $1.49
EOT)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 惯例

# 环境

注意很多系统下 /usr/sbin 和 /sbin 不在默认路径中,如果程序里引用到它们下的命令,将它们加进 PATH。安全性敏感的脚本,应显式设置 PATH,如:

export PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
1

小心处理一些常见的环境变量,例如启动一个 Java 应用时,如果依赖系统默认安装的 Java 环境,应 unset JAVA_HOME 等避免受用户环境干扰。

# 错误输出

错误信息输出到 &2 (stderr),仅当条件测试时才考虑重定向至 /dev/null。

if ! which nc > /dev/null 2>&1; then
    echo "ERROR: nc is not installed, aborting" >&2
    exit 1
fi
1
2
3
4

# 当前目录

不要改变当前目录,也不采用 pushd 和 popd,因为会给维护者增加负担。

如果需要得到当前脚本的绝对路径,采用下面方法:

SELF_DIR=$(cd $(dirname $0) && pwd)
1

尽量使用一些程序自己的特性改变目录,如 tar -C,patch -d。如果需要临时改变当前目录,采用子 shell:

(
    cd /tmp/foo
    ...
)
1
2
3
4

# 临时文件

使用 mktemp 创建临时文件和目录,采用 trap EXIT 做清理。

TMP_DIR=$(mktemp -d /tmp/foo.XXXXXXXXXX)
TMP_FILE=$(mktemp /tmp/bar.XXXXXXXXXX)
trap "rm -rf $TMP_DIR $TMP_FILE" EXIT
1
2
3

# 检查输入

  • 采用全局选项 set -o nounset 确保变量展开时已定义
  • 使用 ${VAR:?} 或 ${VAR:?"Error mssage"} 确保关键变量已经定义且非空
  • 用 ${VAR:-"default value"} 设置变量默认值

考虑下面的例子,如果变量名不慎写错,运行时前一种就会及时报错,后一种则将会发生惨剧 😃。

# Good
rm -rf ${TEMPORARY_DATA_DIR:?}/*

# Bad
rm -rf $TEMPORARY_DATA_DIR/*
1
2
3
4
5

检查函数输入参数的例子:

function status_check
{
    local prog_name=${1:?}
    local timeout=${2:-5}    # In second

    ...
}
1
2
3
4
5
6
7

# Main 函数

尽量用函数组织代码中的独立逻辑,函数外的代码留给全局变量定义,其他代码变多时放进 main 函数,然后按如下方式调用:

main "$@"
1

# '&&' 和 '||'

仅将 && 和 || 用于连接多个测试条件。|| 适用于“断言(assertion)”时方可接命令语句。一些例子如下:

# Good: assertion
(( $# > 1 )) || { usage; exit 1; }

# Good
if [[ -r $conf ]] && grep -q "foobar" $conf; then
    load_config $conf
fi

# Bad: may be suffered by "set -o errexit" when the config file does not exist
[[ -r $config ]] && grep -q "foobar" $conf && load_config $conf
1
2
3
4
5
6
7
8
9
10

# Exit vs return

函数内部只使用 return,返回 0 代表成功,返回 1 代表失败。exit 只用于主程序。

# Builtin vs 外部命令

优先使用 builtin (内置) 命令,如

# Good
SUM=$(( x + y ))
FILE_SUFFIX=${PATHNAME%%*.}

# Bad
SUM=$(expr $x + $y)
FILE_SUFFIX=$(echo "$PATHNAME | sed -e 's/.*\.//')
1
2
3
4
5
6
7

# 陷阱

# 选项 errexit

启用了 set -o errexit 时小心每条命令的返回值,它的作用是任何命令返回值 $? 非 0 时即让程序将直接退出。但它在下面几种情形时不起作用:

  • 命令跟在 while, until, if 关键字之后
  • 命令是 && 或 || 语句的一部分
  • ! foo

应当显示中断执行,例如 ! foo 该写成:

! foo || return 1
1

对于不会输出错误信息的命令,还要显式输出,以便容易知道程序终止在何处。

nc -w 5 $host $port || {
    echo "$host:$port seems unreachable" >&2
    exit 1
}
1
2
3
4

# Subshell 给 local 变量赋值

注意 local output=$(foo_command) 赋值始终会成功(返回 0),若依赖命令返回值,要分开两行写:

    local output

    output=$(foo_command)  # errexit will capture the return value
1
2
3

# Shift

小心 shift 2 在参数个数小于 2 时的行为将是一个也不移,在某些系统上并不返回错误,应改成 shift; shift。

# 参考

[1] Google Shell Style Guide (opens new window)

#Linux#Shell
最后更新时间: 2022/7/23 10:17:11
Linux的date常用操作
Shell批量执行脚本

← Linux的date常用操作 Shell批量执行脚本→

最近更新
01
分区分桶
08-21
02
数据模型(重要)
08-21
03
安装和编译
08-21
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式