Shell脚本及钩子

时间:2022-09-13 15:34:51

原文地址

Shell脚本

什么是Shell脚本&为什么要用脚本

Shell脚本的定义:

  • Shell:一类介于系统内核与用户之间的解释程序
  • 脚本:一类用特定语言,按顺序执行的文件

用脚本的原因:

  • 能快速的完成重复性的任务.
  • 封装了知识并充当一种形式的文档,它确切的记录了如何执行一项任务
  • 可以使用程序”内部”的功能.比如:Word和Excel

创建一个简单脚本

  1. 清理任务过程
    • 任务是什么
    • 先做什么
    • 再做什么
    • 每一步如何实现
  2. 整理执行语句
    • 按任务顺序
    • 必要的控制结构
    • 如何避免交互
  3. 完善文件结构
    • 执行环境
    • 必要的注释信息
    • 友好的提示

运行脚本

方法有三种:
第一种

chmod +x uad.sh
./uad.sh

第二种

sh uad.sh

第三种

source uad.sh

Shell命令的组合运用

  1. 管道操作:
    将一端的命令交给另一端的命令处理
    格式:命令1|命令2 //命令1命令的结果给命令2执行
example:
$ netstat -anp | grep 8001
$ ps aux | grep http
  1. 重定向操作:
    将输入输出的设备改变为文件,而不是屏幕或键盘
类型 操作符 用途
重定向输入 < 从指定文件读取数据,而不是从键盘输入
重定向输出 > 、 >> 将输出结果覆盖 、 追加到指定文件
标准错误输出 2>、2>> 将错误信息覆盖、追加到指定文件
混合输出 &>、&>> 将标准输出和错误信息覆盖、追加到制定文件

3. 逻辑分割
处理多条命令之间的逻辑关系

  • 逻辑与:&&
    两条执行语句都必须执行成功
$ echo "萝卜" && echo "白菜"
萝卜
白菜
$ mkdir /haha/a 2>/dev/error.txt && echo "成功"
//创建文件夹a 失败则记录在文件error.txt中 成功则显示成功
  • 逻辑或:||
$ echo "萝卜" ||echo "白菜"
萝卜
$ mkdir /haha/a 2>/dev/error.txt || echo "失败"
//创建文件夹a 失败则记录在文件夹爱error.txt中并显示"失败"

两条执行语句执行成功一条即可

  • 顺序执行:;
    同一行多个命令间加;等同于多行单个命令

Shell的变量

普通变量
- 定义及赋值:变量名=变量值
- 引用变量:$变量名${变量名}
环境变量:用来记录/设置运行参数
- 用户操作:PATH LANG CLASSPATH GOPATH
- 系统赋值:USER LOGNAME HOME SHELL
ps:可以用env来看环境变量
特殊变量:有系统或脚本控制,不可直接赋值
- $?:前一条命令的状态值,0为正常 非0为异常

$ mkdir /test
$ echo $?
0

$ mkdir /test/a
mkdir:无法创建目录"/test/a":没有那个文件或目录
$ echo$?
1
  • $0:脚本自身的程序名
  • $1-$9:第1-第9个位置参数
  • $*:命令行的所有位置参数的内容
  • $#:命令行的位置参数个数
$ cat test.sh
#! /bin/bash
echo "本程序名:$0"
echo "执行时一共输入$#个位置参数"
echo "其中第一个参数是:$1"
echo "所有参数如下:$*"
$ ./test.sh Hello World
本程序名:./test.sh
执行时一共输入2个位置参数
其中第一个参数是Hello
所有参数如下:Hello World

Shell控制语句

if then elif fi

和C语言类似,在Shell中用if、then、elif、else、fi这几条命令实现分支控制。这种流程控制语句本质上也是由若干条Shell命令组成的,例如先前讲过的

if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

其实是三条命令,if [ -f ~/.bashrc ]是第一条,then . ~/.bashrc是第二条,fi是第三条。如果两条命令写在同一行则需要用;号隔开,一行只写一条命令就不需要写;号了,另外,then后面有换行,但这条命令没写完,Shell会自动续行,把下一行接在then后面当作一条命令处理。和[命令一样,要注意命令和各参数之间必须用空格隔开。if命令的参数组成一条子命令,如果该子命令的Exit Status为0(表示真),则执行then后面的子命令,如果Exit Status非0(表示假),则执行elif、else或者fi后面的子命令。if后面的子命令通常是测试命令,但也可以是其它命令。Shell脚本没有{}括号,所以用fi表示if语句块的结束。见下例:

#! /bin/sh 
if [ -f /bin/bash ]
then echo "/bin/bash is a file"
else echo "/bin/bash is NOT a file"
fi
if :; then echo "always true"; fi

:是一个特殊的命令,称为空命令,该命令不做任何事,但Exit Status总是真。此外,也可以执行/bin/true或/bin/false得到真或假的Exit Status。再看一个例子:

#! /bin/sh 
echo "Is it morning? Please answer yes or no."
read YES_OR_NO
if [ "$YES_OR_NO" = "yes" ]; then
  echo "Good morning!"
elif [ "$YES_OR_NO" = "no" ]; then
  echo "Good afternoon!"
else
  echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
  exit 1
fi
exit 0

上例中的read命令的作用是等待用户输入一行字符串,将该字符串存到一个Shell变量中。

此外,Shell还提供了&&和||语法,和C语言类似,具有Short-circuit特性,很多Shell脚本喜欢写成这样:

test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1)

&&相当于“if…then…”,而||相当于“if not…then…”。&&和||用于连接两个命令,而上面讲的-a和-o仅用于在测试表达式中连接两个测试条件,要注意它们的区别,例如,

test "$VAR" -gt 1 -a "$VAR" -lt 3

和以下写法是等价的

test "$VAR" -gt 1 && test "$VAR" -lt 3

case esac

case命令可类比C语言的switch/case语句,esac表示case语句块的结束。C语言的case只能匹配整型或字符型常量表达式,而Shell脚本的case可以匹配字符串和Wildcard,每个匹配分支可以有若干条命令,末尾必须以;;结束,执行时找到第一个匹配的分支并执行相应的命令,然后直接跳到esac之后,不需要像C语言一样用break跳出。

#! /bin/sh 
echo "Is it morning? Please answer yes or no."
read YES_OR_NO
case "$YES_OR_NO" in
yes|y|Yes|YES)
  echo "Good Morning!";;
[nN]*)
  echo "Good Afternoon!";;
*)
  echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
  exit 1;;
esac
exit 0

使用case语句的例子可以在系统服务的脚本目录/etc/init.d中找到。这个目录下的脚本大多具有这种形式(以/etc/apache2为例):

case $1 in
    start)
        ...
    ;;
    stop)
        ...
    ;;
    reload | force-reload)
        ...
    ;;
    restart)
    ...
    *)
        log_success_msg "Usage: /etc/init.d/apache2 {start|stop|restart|reload|force-reload|start-htcacheclean|stop-htcacheclean}"
        exit 1
    ;;
esac

启动apache2服务的命令是

$ sudo /etc/init.d/apache2 start

$1是一个特殊变量,在执行脚本时自动取值为第一个命令行参数,也就是start,所以进入start)分支执行相关的命令。同理,命令行参数指定为stop、reload或restart可以进入其它分支执行停止服务、重新加载配置文件或重新启动服务的相关命令。

for/do/done

Shell脚本的for循环结构和C语言很不一样,它类似于某些编程语言的foreach循环。例如:

#! /bin/sh 
for FRUIT in apple banana pear; do
  echo "I like $FRUIT"
done

FRUIT是一个循环变量,第一次循环$FRUIT的取值是apple,第二次取值是banana,第三次取值是pear。再比如,要将当前目录下的chap0、chap1、chap2等文件名改为chap0~、chap1~、chap2~等(按惯例,末尾有~字符的文件名表示临时文件),这个命令可以这样写:

$ for FILENAME in chap?; do mv $FILENAME $FILENAME~; done

也可以这样写:

$ for FILENAME in `ls chap?`; do mv $FILENAME $FILENAME~; done

while/do/done

while的用法和C语言类似。比如一个验证密码的脚本:

#! /bin/sh
echo "Enter password:"
read TRY
while [ "$TRY" != "secret" ]; do
  echo "Sorry, try again"
  read TRY
done

下面的例子通过算术运算控制循环的次数:

#! /bin/sh
COUNTER=1
while [ "$COUNTER" -lt 10 ]; do
  echo "Here we go again"
  COUNTER=$(($COUNTER+1))
done

Shell还有until循环,类似C语言的do…while循环

Shell脚本的调试方法

shell提供了一些调试脚本的选项

  • -n读一遍脚本中的命令但不执行,用于检查脚本中的语法错误
  • -v一边执行脚本,一边将执行过的脚本命令打印到标准错误输出
  • -x提供跟踪执行信息,将执行的每一条命令和结果依次打印出来

使用这些选项有种方法一个是在命令行提供参数

$ sh -x ./script.sh

第二种是在脚本开头提供参数

#! /bin/sh -x

第三种方法是在脚本中用set命令启用或禁用参数

#! /bin/sh
if [ -z "$1" ]; then
  set -x
  echo "ERROR: Insufficient Args."
  exit 1
  set +x
fi

Shell函数

和C语言类似,Shell中也有函数的概念,但是函数定义中没有返回值也没有参数列表。例如:

#! /bin/sh 
foo(){ echo "Function foo is called";}
echo "-=start=-"
foo
echo "-=end=-"

注意函数体的左花括号{和后面的命令之间必须有空格或换行,如果将最后一条命令和右花括号}写在同一行,命令末尾必须有;号。

在定义foo()函数时并不执行函数体中的命令,就像定义变量一样,只是给foo这个名字一个定义,到后面调用foo函数的时候(注意Shell中的函数调用不写括号)才执行函数体中的命令。Shell脚本中的函数必须先定义后调用,一般把函数定义都写在脚本的前面,把函数调用和其它命令写在脚本的最后(类似C语言中的main函数,这才是整个脚本实际开始执行命令的地方)。
Shell函数没有参数列表并不表示不能传参数,事实上,函数就像是迷你脚本,调用函数时可以传任意个参数,在函数内同样是用$0$1$2等变量来提取参数,函数中的位置参数相当于函数的局部变量,改变这些变量并不会影响函数外面的$0$1$2等变量。函数中可以用return命令返回,如果return后面跟一个数字则表示函数的Exit Status。

下面这个脚本可以一次创建多个目录,各目录名通过命令行参数传入,脚本逐个测试各目录是否存在,如果目录不存在,首先打印信息然后试着创建该目录。

#! /bin/sh 
is_directory()
{
  DIR_NAME=$1
  if [ ! -d $DIR_NAME ]; then
    return 1
  else
    return 0
  fi
}

for DIR in "$@"; do
  if is_directory "$DIR"
  then :
  else
    echo "$DIR doesn't exist. Creating it now..."
    mkdir $DIR > /dev/null 2>&1
    if [ $? -ne 0 ]; then
      echo "Cannot create directory $DIR"
      exit 1
    fi
  fi
done

注意is_directory()返回0表示真返回1表示假。

一些操作符的区别

  • ' ' " ":双引号允许引用 \转义,单引号不允许引用 \转义
$ Test="123"
$ echo "$Test"
123
$ echo '$Test'
$Test
  • [ ][[ ]]是test 操作命令,二者区别
    命令test或[可以测试一个条件是否成立,如果测试结果为真,则该命令的Exit Status为0,如果测试结果为假,则命令的Exit Status为1(注意与C语言的逻辑表示正好相反)。例如测试两个数的大小关系:
$ VAR=2
$ test $VAR -gt 1
$ echo $?
0
$ test $VAR -gt 3
$ echo $?
1
$ [ $VAR -gt 3 ]
$ echo $?
1

虽然看起来很奇怪,但左方括号[确实是一个命令的名字,传给命令的各参数之间应该用空格隔开,比如,$VAR-gt3][命令的四个参数,它们之间必须用空格隔开。命令test或[的参数形式是相同的,只不过test命令不需要]参数。以[命令为例,常见的测试命令如下表所示:

- -
[ -d DIR ] 如果DIR存在并且是一个目录则为真
[ -f FILE ] 如果FILE存在且是一个普通文件则为真
[ -z STRING ] 如果STRING的长度为零则为真
[ -n STRING ] 如果STRING的长度非零则为真
[ STRING1 = STRING2 ] 如果两个字符串相同则为真
[ STRING1 != STRING2 ] 如果字符串不相同则为真
[ ARG1 OP ARG2 ] ARG1和ARG2应该是整数或者取值为整数的变量,OP是-eq(等于)-ne(不等于)-lt(小于)-le(小于等于)-gt(大于)-ge(大于等于)之中的一个

- $()``命令代换:Shell先执行该命令,然后将输出结果立刻代换到当前命令行中。

$ DATE=`date`
$ echo $DATE

或者

$ DATE=$(date)
  • $(())算数代换:用于算术计算,$(())中的Shell变量取值将转换成整数
$ VAR=45
$ echo $(($VAR+3))
  • * ? []文件名代换:这些用于匹配的字符称为通配符,具体如下
* 匹配0个或多个任意字符
? 匹配一个任意字符
[若干字符] 匹配方括号中任意一个字符的一次出现
$ ls /dev/ttyS*
$ ls ch0?.doc
$ ls ch0[0-2].doc
$ ls ch[012][0-9].doc

推荐资料

钩子

特定条件触发的脚本

webhook

GitHub allows you to register Webhooks for your repositories. Each time an event occurs on your repository, whether it be pushing code, filling issues or creating pull requests, the webhook address you register can be configured to be pinged with details.

You’re only limited by your imagination.

github coding 等git仓库服务提供商提供的功能
Reference:

githook

客户端的钩子

本地钩子只影响它们所在的仓库。当你在读这一节的时候,记住开发者可以修改他们本地的钩子,所以不要用它们来推行强制的提交规范。

  • pre-commit
    pre-commit脚本在每次你运行git commit命令时,Git向你询问提交信息或者生产提交对象时被执行。你可以用这个钩子来检查即将被提交的代码快照。
  • prepare-commit-msg
    prepare-commit-msg钩子在pre-commit钩子在文本编辑器中生成提交信息之后被调用。这被用来方便地修改自动生成的squash或merge提交。
  • commit-msg
    commit-msg钩子和prepare-commit-msg钩子很像,但它会在用户输入提交信息之后被调用。这适合用来提醒开发者他们的提交信息不符合你团队的规范。
  • post-commit
    post-commit钩子在commit-msg钩子之后立即被运行 。它无法更改git commit的结果,所以这主要用于通知用途。
  • post-checkout
    post-checkout钩子和post-commit钩子很像,但它在你用git checkout查看引用的时候被调用。这是用来清理你的工作目录中可能会令人困惑的生成文件。
  • pre-rebase
    pre-rebase钩子在git rebase发生更改之前运行,确保不会有什么糟糕的事情发生。

服务端的钩子

  • pre-receive
    pre-receive钩子在有人用git push向仓库推送代码时被执行。它只存在于远端仓库中,而不是原来的仓库中。
  • update
    update钩子在pre-receive之后被调用,用法也差不多。它也是在实际更新前被调用的,但它可以分别被每个推送上来的引用分别调用。
  • post-receive
    post-receive钩子在成功推送后被调用,适合用于发送通知。

Reference: