64

数以百万计的开发人员编写 shell 脚本来解决各种类型的任务。我使用 shell 脚本来简化部署、生命周期管理、安装或简单地用作胶水语言

我注意到没有人真正关心 shell 脚本的样式和质量。许多团队花费大量时间来修复 Java、C++、... 样式问题,但完全忽略了他们的 shell 脚本中的问题。顺便说一句,通常没有标准的方法来在特定项目中实现 shell 脚本,因此人们可能会发现许多不同的、丑陋的和有缺陷的脚本,散布在代码库中。

为了在我的项目中克服这个问题,我决定创建一个通用且足够好的 shell 脚本模板。我将按原样提供我的模板,以使这个问题更有用。开箱即用的这些模板提供:

  • 命令行参数处理
  • 同步
  • 一些基本的帮助

参数处理:getopts(最新版本:shell-script-template@github

#!/bin/bash
# ------------------------------------------------------------------
# [Author] Title
#          Description
# ------------------------------------------------------------------

VERSION=0.1.0
SUBJECT=some-unique-id
USAGE="Usage: command -ihv args"

# --- Options processing -------------------------------------------
if [ $# == 0 ] ; then
    echo $USAGE
    exit 1;
fi

while getopts ":i:vh" optname
  do
    case "$optname" in
      "v")
        echo "Version $VERSION"
        exit 0;
        ;;
      "i")
        echo "-i argument: $OPTARG"
        ;;
      "h")
        echo $USAGE
        exit 0;
        ;;
      "?")
        echo "Unknown option $OPTARG"
        exit 0;
        ;;
      ":")
        echo "No argument value for option $OPTARG"
        exit 0;
        ;;
      *)
        echo "Unknown error while processing options"
        exit 0;
        ;;
    esac
  done

shift $(($OPTIND - 1))

param1=$1
param2=$2

# --- Locks -------------------------------------------------------
LOCK_FILE=/tmp/$SUBJECT.lock
if [ -f "$LOCK_FILE" ]; then
   echo "Script is already running"
   exit
fi

trap "rm -f $LOCK_FILE" EXIT
touch $LOCK_FILE

# --- Body --------------------------------------------------------
#  SCRIPT LOGIC GOES HERE
echo $param1
echo $param2
# -----------------------------------------------------------------

Shell Flags (shFlags) 允许大量简化命令行参数的处理,所以在某个时刻我决定不忽略这种可能性。

参数处理:shflags(最新版本:shell-script-template@github

#!/bin/bash
# ------------------------------------------------------------------
# [Author] Title
#          Description
#
#          This script uses shFlags -- Advanced command-line flag
#          library for Unix shell scripts.
#          http://code.google.com/p/shflags/
#
# Dependency:
#     http://shflags.googlecode.com/svn/trunk/source/1.0/src/shflags
# ------------------------------------------------------------------
VERSION=0.1.0
SUBJECT=some-unique-id
USAGE="Usage: command -hv args"

# --- Option processing --------------------------------------------
if [ $# == 0 ] ; then
    echo $USAGE
    exit 1;
fi

. ./shflags

DEFINE_string 'aparam' 'adefault' 'First parameter'
DEFINE_string 'bparam' 'bdefault' 'Second parameter'

# parse command line
FLAGS "$@" || exit 1
eval set -- "${FLAGS_ARGV}"

shift $(($OPTIND - 1))

param1=$1
param2=$2

# --- Locks -------------------------------------------------------
LOCK_FILE=/tmp/${SUBJECT}.lock

if [ -f "$LOCK_FILE" ]; then
echo "Script is already running"
exit
fi

trap "rm -f $LOCK_FILE" EXIT
touch $LOCK_FILE

# -- Body ---------------------------------------------------------
#  SCRIPT LOGIC GOES HERE
echo "Param A: $FLAGS_aparam"
echo "Param B: $FLAGS_bparam"
echo $param1
echo $param2
# -----------------------------------------------------------------

我确实认为可以改进这些模板以进一步简化开发人员的生活。

所以问题是如何改进它们以具有以下特性:

  • 内置日志记录
  • 更好的错误处理
  • 更好的便携性
  • 占地面积更小
  • 内置执行时间跟踪
4

6 回答 6

29

这是我的脚本外壳模板的标题(可以在这里找到:http ://www.uxora.com/unix/shell-script/18-shell-script-template )。

man与usage() 习惯于使用的外观相似,也用于diplsay 帮助。

#!/bin/ksh
#================================================================
# HEADER
#================================================================
#% SYNOPSIS
#+    ${SCRIPT_NAME} [-hv] [-o[file]] args ...
#%
#% DESCRIPTION
#%    This is a script template
#%    to start any good shell script.
#%
#% OPTIONS
#%    -o [file], --output=[file]    Set log file (default=/dev/null)
#%                                  use DEFAULT keyword to autoname file
#%                                  The default value is /dev/null.
#%    -t, --timelog                 Add timestamp to log ("+%y/%m/%d@%H:%M:%S")
#%    -x, --ignorelock              Ignore if lock file exists
#%    -h, --help                    Print this help
#%    -v, --version                 Print script information
#%
#% EXAMPLES
#%    ${SCRIPT_NAME} -o DEFAULT arg1 arg2
#%
#================================================================
#- IMPLEMENTATION
#-    version         ${SCRIPT_NAME} (www.uxora.com) 0.0.4
#-    author          Michel VONGVILAY
#-    copyright       Copyright (c) http://www.uxora.com
#-    license         GNU General Public License
#-    script_id       12345
#-
#================================================================
#  HISTORY
#     2015/03/01 : mvongvilay : Script creation
#     2015/04/01 : mvongvilay : Add long options and improvements
# 
#================================================================
#  DEBUG OPTION
#    set -n  # Uncomment to check your syntax, without execution.
#    set -x  # Uncomment to debug this shell script
#
#================================================================
# END_OF_HEADER
#================================================================

这是要使用的使用功能:

  #== needed variables ==#
SCRIPT_HEADSIZE=$(head -200 ${0} |grep -n "^# END_OF_HEADER" | cut -f1 -d:)
SCRIPT_NAME="$(basename ${0})"

  #== usage functions ==#
usage() { printf "Usage: "; head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#+" | sed -e "s/^#+[ ]*//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g" ; }
usagefull() { head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#[%+-]" | sed -e "s/^#[%+-]//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g" ; }
scriptinfo() { head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#-" | sed -e "s/^#-//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g"; }

这是您应该获得的:

# Display help
$ ./template.sh --help

    SYNOPSIS
    template.sh [-hv] [-o[file]] args ...

    DESCRIPTION
    This is a script template
    to start any good shell script.

    OPTIONS
    -o [file], --output=[file]    Set log file (default=/dev/null)
                                  use DEFAULT keyword to autoname file
                                  The default value is /dev/null.
    -t, --timelog                 Add timestamp to log ("+%y/%m/%d@%H:%M:%S")
    -x, --ignorelock              Ignore if lock file exists
    -h, --help                    Print this help
    -v, --version                 Print script information

    EXAMPLES
    template.sh -o DEFAULT arg1 arg2

    IMPLEMENTATION
    version         template.sh (www.uxora.com) 0.0.4
    author          Michel VONGVILAY
    copyright       Copyright (c) http://www.uxora.com
    license         GNU General Public License
    script_id       12345

# Display version info
$ ./template.sh -v

    IMPLEMENTATION
    version         template.sh (www.uxora.com) 0.0.4
    author          Michel VONGVILAY
    copyright       Copyright (c) http://www.uxora.com
    license         GNU General Public License
    script_id       12345

您可以在此处获取完整的脚本模板:http ://www.uxora.com/unix/shell-script/18-shell-script-template

于 2015-04-11T14:52:27.210 回答
20

我会避免依赖于shell,并在POSIX 定义的 shell 语法bash之上为您的解决方案建模并在 shebang 上使用。最近,当Ubuntu 更改./bin/sh/bin/shdash

shell 世界的另一个流行病是对退出状态代码的普遍误解。以可理解的代码退出是让其他 shell 脚本以编程方式对特定故障做出反应的原因。不幸的是,除了“sysexits.h”头文件之外,没有太多的指导。

如果您正在寻找有关良好 shell 脚本编写实践的更多信息,请关注 Korn shell 脚本资源。Ksh 编程倾向于专注于真正的编程,而不是编写随意的脚本。

就个人而言,我没有发现 shell 模板有太多用处。不幸的事实是,大多数工程师只会简单地复制和粘贴您的模板,然后继续编写同样草率的 shell 代码。更好的方法是创建一个具有明确语义的 shell 函数库,然后说服其他人使用它们。这种方法也将有助于变更控制。例如,如果您在模板中发现缺陷,那么基于它的每个脚本都会损坏并且需要修改。使用库可以在一个地方修复缺陷。

欢迎来到 shell 脚本的世界。编写 shell 脚本是一门失传的艺术,似乎正在进入复兴时期。90 年代后期有一些关于这个主题的好书 - Burns 和 Arthur的UNIX Shell Programming浮现在脑海中,尽管亚马逊对这本书的评论让它看起来很糟糕。恕我直言,有效的 shell 代码包含 Eric S. Raymond 在The Art of Unix Programming中描述的 UNIX 哲学。

于 2012-12-23T14:04:16.263 回答
6

如果您担心可移植性,请不要==在测试中使用。改为使用=。不要显式检查是否$#为 0。而是${n?error message}在第一次引用所需参数时使用(例如${3?error message})。这可以防止发出使用语句而不是错误消息的极其烦人的做法。最重要的是,始终将错误消息放在正确的流上并以正确的状态退出。例如:

echo "Unknown error while processing options" >&2
exit 1;

执行以下操作通常很方便:

die() { echo "$*"; exit 1; } >&2
于 2012-12-23T12:56:15.870 回答
5

这是我的 bash 样板,在评论中解释了一些理智的选项

#!/usr/bin/env bash

set -e  # Abort script at first error, when a command exits with non-zero status (except in until or while loops, if-tests, list constructs)
set -u  # Attempt to use undefined variable outputs error message, and forces an exit
set -x  # Similar to verbose mode (-v), but expands commands
set -o pipefail  # Causes a pipeline to return the exit status of the last command in the pipe that returned a non-zero return value.
于 2019-09-24T22:11:36.933 回答
3

我也会分享我的结果。所有这些例子背后的想法是鼓励整体质量。确保最终结果足够安全也很重要。

日志记录

从一开始就提供适当的日志记录非常重要。我只是想考虑生产使用情况。

TAG="foo"
LOG_FILE="example.log"

function log() {
    if [ $HIDE_LOG ]; then
        echo -e "[$TAG] $@" >> $LOG_FILE
    else
        echo "[`date +"%Y/%m/%d:%H:%M:%S %z"`] [$TAG] $@" | tee -a $LOG_FILE
    fi
}

log "[I] service start"
log "[D] debug message"

命令测试

这是关于安全、现实生活环境和适当的错误处理。可以是可选的。

function is_command () {
    log "[I] check if commad $1 exists"
    type "$1" &> /dev/null ;
}

CMD=zip

if is_command ${CMD} ; then
   log "[I] '${CMD}' command found"
else
   log "[E] '${CMD}' command not found"
fi

模板处理

可能只是我的主观意见,但无论如何。我使用了几种不同的方法来直接从脚本生成一些配置/等。Perl、sed 和其他人做的工作,但看起来有点吓人。

最近我注意到一个更好的方法:

function process_template() {
    source $1 > $2

    result=$?
    if [ $result -ne 0 ]; then
        log "[E] Error during template processing: '$1' > '$2'"
    fi
    return $result
}

VALUE1="tmpl-value-1"
VALUE2="tmpl-value-2"
VALUE3="tmpl-value-3"

process_template template.tmpl template.result

模板示例

echo "Line1: ${VALUE1}
Line2: ${VALUE2}
Line3: ${VALUE3}"

结果示例

Line1: tmpl-value-1
Line2: tmpl-value-2
Line3: tmpl-value-3
于 2012-12-25T17:42:16.810 回答
1

对于 shell 脚本来说,没有比使用示例和已知错误列表的有据可查的行为更有帮助的了。我认为没有一个程序可以称得上是防弹的,并且每时每刻都可能出现错误(尤其是当您的脚本被其他人使用时),所以我唯一要注意的是良好的编码风格并且只使用这些脚本真正需要的东西。你站在聚合的道路上,它总是会变成一个带有大量未使用模块的大型系统,这些模块很难移植,也很难支持。系统越是便携,它就越大。说真的,shell 脚本不需要以这种方式实现。它们必须尽可能小以简化进一步的使用。

如果系统真的需要大而坚固的东西,那么是时候考虑 C99 甚至 C++了。

于 2012-12-26T00:48:49.680 回答