Linux Shell Scripts

Stone大约 11 分钟

Linux Shell Scripts

概述

类似于 Windows 下的 Bat 程序,在 Linux 下可以编写和使用 Shell Scripts 来管理系统。

简单来说,Shell Scripts 就是包含 Linux 命令以及流程控制语句的可执行文件。Shell 读取这个文件,依次执行里面的命令。

开始

创建第一个 Shell Script:

[root@stone ~]# vi hello.sh
#!/bin/bash
# author: stone
echo "Hello Linux"

其中:

  • hello.sh:脚本文件名,一般使用 .sh 后缀名
  • #!:称为 Shebang,指定执行脚本的解释器,Bash 脚本的解释器一般是 /bin/sh/bin/bash
  • #:表示注释

为脚本加上可执行权限:

[root@stone ~]# chmod a+x hello.sh 

[root@stone ~]# ll hello.sh 
-rwxr-xr-x. 1 root root 31 Sep  4 14:22 hello.sh

使用相对路径执行脚本:

[root@stone ~]# ./hello.sh 
Hello Linux

使用绝对路径执行脚本:

[root@stone ~]# /root/hello.sh 
Hello Linux

手动将脚本传给解释器来执行:

[root@stone ~]# sh hello.sh 
Hello Linux

也可以将脚本放到 PATH 环境变量指定的路径下,然后直接执行:

[root@stone ~]# env | grep PATH
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
[root@stone ~]# mkdir /root/bin
[root@stone ~]# mv hello.sh /root/bin
[root@stone ~]# hello.sh 
Hello Linux

返回值

使用 $? 获取上一条命令或者脚本执行后的返回值:

[root@stone ~]# hello.sh 
Hello Linux
[root@stone ~]# echo $?
0

如果返回 0,表示执行成功;返回非 0,表示执行失败。

可以使用 exit 命令终止脚本执行,并指定一个返回值:

[root@stone ~]# vi bin/hello.sh 
#!/bin/bash
# author: stone
echo "Hello Linux"
exit 1
[root@stone ~]# hello.sh 
Hello Linux
[root@stone ~]# echo $?
1

参数

可以为脚本指定参数,例如:

[root@stone ~]# hello.sh a b c

指定参数后,脚本内部使用以下特殊变量来引用参数:

  • $0:脚本文件名,这里为 hello.sh

  • $1:第一个参数,这里为 a

  • $2:第二个参数,这里为 b

  • $3:第三个参数,这里为 c

  • $#:参数个数,这里为 3

  • $@:全部参数,参数之间使用空格分隔

[root@stone ~]# vi bin/hello.sh 
#!/bin/bash
# author: stone
echo "Hello Linux"
echo '$0 = ' $0
echo '$1 = ' $1
echo '$2 = ' $2
echo '$3 = ' $3
echo '$# = ' $#
echo '$@ = ' $@

[root@stone ~]# hello.sh a b c
Hello Linux
$0 =  /root/bin/hello.sh
$1 =  a
$2 =  b
$3 =  c
$# =  3
$@ =  a b c

可以使用 shift 命令移除参数,例如:

[root@stone ~]# vi bin/hello.sh 
#!/bin/bash
# author: stone
echo "Hello Linux"
echo '$1 = ' $1
echo '$@ = ' $@

echo -e '\n'
echo "shift 1 arg"
shift
echo '$1 = ' $1
echo '$@ = ' $@

echo -e '\n'
echo "shift 2 args"
shift 2
echo '$1 = ' $1
echo '$@ = ' $@

[root@stone ~]# hello.sh a b c d
Hello Linux
$1 =  a
$@ =  a b c d


shift 1 arg
$1 =  b
$@ =  b c d


shift 2 arg
$1 =  d
$@ =  d

输入

在脚本中使用 read 命令等待用户输入。

语法:

read [options] [name ...]

常用选项有:

  • -p prompt:指定提示信息
  • -t timeout:指定等待用户输入的超时秒数
  • -s:用户的输入不显示在屏幕上,常用于输入密码
[root@stone ~]# vi bin/hello.sh 
#!/bin/bash
# author: stone

read -t 30 -p "Enter Name: " NAME
echo "Hello $NAME"

read -s -t 30 -p "Enter Password: " PASSWORD
echo -e "\nPassword: $PASSWORD"

[root@stone ~]# hello.sh 
Enter Name: stone
Hello stone
Enter Password: 
Password: 123456

条件

test

使用 test 命令判断条件。

语法:

test EXPRESSION

如果 EXPRESSION 表达式为真,则返回 0,否则返回 1。EXPRESSION 可以是以下形式:

  • 字符串判断:
    • -n STRING:如果 STRING 的长度大于零,则为真
    • -z STRING:如果 STRING 的长度等于零,则为真
    • STRING1 = STRING2:如果 STRING1STRING2 相同,则为真
    • STRING1 != STRING2:如果 STRING1STRING2 不同,则为真
  • 整数判断:
    • INTEGER1 -eq INTEGER2:如果 INTEGER1 等于 INTEGER2,则为真
    • INTEGER1 -gt INTEGER2:如果 INTEGER1 大于 INTEGER2,则为真
    • INTEGER1 -lt INTEGER2:如果 INTEGER1 小于 INTEGER2,则为真
    • INTEGER1 -ge INTEGER2:如果 INTEGER1 大于等于 INTEGER2,则为真
    • INTEGER1 -le INTEGER2:如果 INTEGER1 小于等于 INTEGER2,则为真
    • INTEGER1 -ne INTEGER2:如果 INTEGER1 不等于 INTEGER2,则为真
  • 文件判断:
    • -e FILE:如果 FILE 存在,则为真
    • -f FILE:如果 FILE 存在且为文件,则为真
    • -d FILE:如果 FILE 存在且为目录,则为真
    • -L FILE:如果 FILE 存在且为符号链接,则为真
    • -b FILE:如果 FILE 存在且为块设备文件,则为真
    • -c FILE:如果 FILE 存在且为字符设备文件,则为真
    • -S FILE:如果 FILE 存在且为套接字文件,则为真
    • -r FILE:如果 FILE 存在且有读权限,则为真
    • -w FILE:如果 FILE 存在且有写权限,则为真
    • -x FILE:如果 FILE 存在且有执行权限,则为真
    • -s FILE:如果 FILE 存在且大小大于 0,则为真
    • FILE1 -nt FILE2:如果 FILE1FILE2 新,则为真
    • FILE1 -ot FILE2:如果 FILE1FILE2 旧,则为真
  • 逻辑判断:
    • EXPRESSION1 -a EXPRESSION2:与运算
    • EXPRESSION1 -o EXPRESSION2:或运算
    • ! EXPRESSION:非运算
[root@stone ~]# test -e .bash_profile 
[root@stone ~]# echo $?
0

[]

使用 [] 替代 test 来判断表达式,需要注意:

  • 中括号两端及内部各项都需要使用空格分隔
  • 中括号内的变量和常量建议使用双引号包裹
[root@stone ~]# [ -e /etc/hosts ]
[root@stone ~]# echo $?
0

(())

使用 (()) 进行算术运行。

[root@stone ~]# ((3 > 2))
[root@stone ~]# echo $?
0

if

使用 if 语句根据条件进行判断是否执行某些命令。

语法如下:

if commands; then
  commands
[elif commands; then
  commands...]
[else
  commands]
fi
[root@stone ~]# vi bin/getversion.sh
#!/bin/bash
# author: stone

VERSION=`cut -d ' ' -f 4 /etc/redhat-release |  cut -d '.' -f 1`

if [ "$VERSION" == 7 ]; then
  echo "CentOS 7"
elif [ "$VERSION" == 6 ]; then
  echo "CentOS 6"
else
  echo "ERROR"
fi

[root@stone ~]# chmod a+x bin/getversion.sh 
[root@stone ~]# getversion.sh 
CentOS 7

case

使用 case 语句根据表达式的值执行某些命令。

语法:

case expression in
  pattern )
    commands ;;
  pattern )
    commands ;;
  ...
esac

其中 pattern 可以是:

  • a):匹配 a
  • a|b):匹配 ab
  • [[:alpha:]]):匹配单个字母
  • ???):匹配 3 个字符的单词
  • *.txt):匹配 .txt 结尾
  • *):匹配任意输入,通过作为 case 结构的最后一个模式
[root@stone ~]# vi bin/getversion1.sh
#!/bin/bash
# author: stone

VERSION=`cut -d ' ' -f 4 /etc/redhat-release |  cut -d '.' -f 1`

case "$VERSION" in
  7 )
    echo "CentOS 7" ;;
  6 )
    echo "CentOS 6" ;;
  * )
    echo "ERROR" ;;
esac

[root@stone ~]# chmod a+x bin/getversion1.sh 
[root@stone ~]# getversion1.sh 
CentOS 7

循环

常用的循环语句有 forwhile

for

使用 for 语句进行循环。

有 2 种形式 :

  • for name [ [ in [ word ... ] ] ; ] do list ; done
  • for (( expr1 ; expr2 ; expr3 )) ; do list ; done

对于第一种 for...in 循环,还可以写为:

for variable in list
do
  commands
done

其中:

  • list 可以是多个值,或者使用通配符匹配多个文件,或者使用命令产生。 如果省略 in list 部分,则默认为脚本的所有参数,即 $@

判断指定网段的 IP 地址是否被使用:

[root@stone ~]# vi bin/testip.sh
#!/bin/bash
# author: stone

if [ "$1" == '' ]; then
  echo "Usage: testip xxx.xxx.xxx"
  exit 0
fi


for i in $(seq 1 254)
do
  ping -c 1 -w 1 $1.$i > /dev/null
  if [ $? == 0 ]; then
    echo "IP $1.$i is Used"
  else
    echo "IP $1.$i is not Used"
  fi
done

[root@stone ~]# chmod a+x bin/testip.sh
[root@stone ~]# testip.sh 192.168.92
IP 192.168.92.1 is Used
IP 192.168.92.2 is Used
IP 192.168.92.3 is not Used

对于第二种 for (( expr1 ; expr2 ; expr3 )) 循环,还可以写为:

for (( expression1; expression2; expression3 ))
do
  commands
done

其中:

  • expr1:初始化循环条件
  • expr2:循环结束的条件
  • expr3:在每次循环迭代的末尾执行,用于更新值

在圆括号之中使用变量时,不必加上美元符号 $

[root@stone ~]# cp bin/testip.sh bin/testip1.sh
[root@stone ~]# vi bin/testip1.sh 
#!/bin/bash
# author: stone

if [ "$1" == '' ]; then
  echo "Usage: testip xxx.xxx.xxx"
  exit 0
fi


for (( i=1; i<5; i=i+1 ))
do
  ping -c 1 -w 1 $1.$i > /dev/null
  if [ $? == 0 ]; then
    echo "IP $1.$i is Used"
  else
    echo "IP $1.$i is not Used"
  fi
done

[root@stone ~]# testip1.sh 192.168.92
IP 192.168.92.1 is Used
IP 192.168.92.2 is Used
IP 192.168.92.3 is not Used
IP 192.168.92.4 is not Used

while

使用 while 语句进行循环。

语法:

while condition; do commands; done

或者:

while condition
do
  commands
done
[root@stone ~]# cp bin/testip.sh bin/testip2.sh
[root@stone ~]# vi bin/testip2.sh
#!/bin/bash
# author: stone

if [ "$1" == '' ]; then
  echo "Usage: testip xxx.xxx.xxx"
  exit 0
fi

i=1
while [ $i -lt 5 ]
do
  ping -c 1 -w 1 $1.$i > /dev/null
  if [ $? == 0 ]; then
    echo "IP $1.$i is Used"
  else
    echo "IP $1.$i is not Used"
  fi
  i=$((i + 1))
done

[root@stone ~]# testip2.sh 192.168.92
IP 192.168.92.1 is Used
IP 192.168.92.2 is Used
IP 192.168.92.3 is not Used
IP 192.168.92.4 is not Used

break

使用 break 终止循环,不再执行剩下的循环。

[root@stone ~]# vi bin/testip.sh 
#!/bin/bash
# author: stone

if [ "$1" == '' ]; then
  echo "Usage: testip xxx.xxx.xxx"
  exit 0
fi


for i in $(seq 1 255)
do
  ping -c 1 -w 1 $1.$i > /dev/null
  if [ $? == 0 ]; then
    echo "IP $1.$i is Used"
  else
    echo "IP $1.$i is not Used"
  fi
  if [ $i == 5 ]; then
    break
  fi
done

[root@stone ~]# testip.sh 192.168.92
IP 192.168.92.1 is Used
IP 192.168.92.2 is Used
IP 192.168.92.3 is not Used
IP 192.168.92.4 is not Used
IP 192.168.92.5 is not Used

continue

使用 continue 终止本轮循环,开始执行下一轮循环。

[root@stone ~]# vi bin/testip.sh 
#!/bin/bash
# author: stone

if [ "$1" == '' ]; then
  echo "Usage: testip xxx.xxx.xxx"
  exit 0
fi


for i in $(seq 1 255)
do
  if [ $i == 2 ]; then
    continue
  fi

  ping -c 1 -w 1 $1.$i > /dev/null
  if [ $? == 0 ]; then
    echo "IP $1.$i is Used"
  else
    echo "IP $1.$i is not Used"
  fi

  if [ $i == 5 ]; then
    break
  fi
done

[root@stone ~]# testip.sh 192.168.92
IP 192.168.92.1 is Used
IP 192.168.92.3 is not Used
IP 192.168.92.4 is not Used
IP 192.168.92.5 is not Used

select

使用 select 生成简单的菜单,语法与 for...in 循环基本一致。

语法:

select name [ in list ] ; do commands ; done

或者:

select name [in list]
do
  commands
done

处理逻辑如下:

  1. 生成一个菜单,内容是列表 list 的每一项,并且每一项前面还有一个数字编号。
  2. 提示用户选择一项,输入它的编号。
  3. 用户输入以后,会将该项的内容存在变量 name,该项的编号存入环境变量 REPLY。如果用户没有输入,就按回车键,则会重新输出菜单,让用户选择。
  4. 执行命令体 commands
  5. 执行结束后,回到第一步,重复这个过程。
[root@stone ~]# vi bin/selectversion.sh
#!/bin/bash
# author: stone

select VERSION in CentOS5 CentOS6 CentOS7
do
  echo "the OS Version is $VERSION"
done

[root@stone ~]# selectversion.sh 
1) CentOS5
2) CentOS6
3) CentOS7
#? 1
the OS Version is CentOS5
#?

函数

可以将脚本中的代码片段定义为函数,重复使用。

语法:

[ function ] fun_name [()]
{
    action;
    [return int;]
}

其中:

  • 函数体内可以使用参数变量以获取函数参数,函数参数变量与脚本参数变量一致,包括:

    • $1~$N:函数的第 1 个到第 N 个参数

    • $0:函数所在的脚本名

    • $#:函数的参数个数

    • $@:函数的全部参数,参数之间使用空格分隔

  • 使用 return 命令从函数返回一个值。

  • 函数体内直接声明的变量为全局变量,也可在函数体内修改全局变量。

  • 使用 local 命令在函数体内声明局部变量,只在函数体内有效。

[root@stone ~]# vi bin/testfun.sh
#!/bin/bash
# author: stone

hello() {
 echo "Hello $1"
}

hello $1

[root@stone ~]# chmod a+x bin/testfun.sh
[root@stone ~]# /root/bin/testfun.sh World
Hello World

命令

mktemp

使用 mktemp 命令创建一个随机文件名的临时文件。

常用选项有:

  • -d:创建目录
  • -p:指定临时文件所在的目录,默认为 $TMPDIR,否则为 /tmp
[root@stone ~]# mktemp 
/tmp/tmp.lYSxy66tNv

[root@stone ~]# ll /tmp/tmp.lYSxy66tNv
-rw-------. 1 root root 0 Sep  6 16:37 /tmp/tmp.lYSxy66tNv

trap

使用 trap 命令在脚本中响应系统信号。

语法:

trap command signal

可用信号使用 -l 选项查看:

[root@stone ~]# trap -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

常用信号有:

  • 1) SIGHUP:通知进程重新加载配置文件
  • 2) SIGINT:按下 Ctrl + C,让脚本终止运行
  • 9) SIGKILL:强制终止进程
  • 15) SIGTERM:结束进程,默认值。会等待正在进行的工作完成后才结束,如果卡死了,则无法结束
  • EXIT:编号 0, 不是系统信号,而是 Bash 脚本特有的信号,不管什么情况,只要退出脚本就会产生

在脚本中使用 trap 命令,指定退出时执行的命令:

[root@stone ~]# vi bin/testtrap.sh
#!/bin/bash
# author: stone

trap 'rm -f "$TMPFILE"' EXIT

TMPFILE=$(mktemp) || exit 1
ls /etc > $TMPFILE
if grep -qi "kernel" $TMPFILE; then
  echo 'find'
fi

[root@stone ~]# chmod a+x bin/testtrap.sh
[root@stone ~]# /root/bin/testtrap.sh 
find

注意:

trap命令必须放在脚本的开头。否则,它上方的任何命令导致脚本退出,都不会被它捕获。

set

使用 set 命令修改子 Shell 环境的运行参数,提高脚本的安全性和可维护性。

常用选项有:

  • -e:等价于 -o errexit,遇到错误,停止执行脚本,使用 +e 关闭该选项
  • -E:解决使用 -e 选项导致 trap 失效问题
  • -u:等价于 -o nounset,遇到未定义的变量,停止执行脚本并报错,使用 +u 关闭该选项
  • -x:等价于 -o xtrace,输出脚本中执行的命令,使用 +x 关闭该选项
  • -o pipefail:只要管道中某个命令失败,则整个管道命令就失败,脚本终止执行
  • -n:等价于 -o noexec,不运行命令,只检查语法是否正确

建议在脚本头部加入以下选项:

set -Eeuxo pipefail

也可以使用 bash 命令从命令行传入这些选项:

[root@stone ~]# bash -Eeuxo pipefail bin/getversion.sh 
+ bash -Eeuxo pipefail bin/getversion.sh
++ cut -d ' ' -f 4 /etc/redhat-release
++ cut -d . -f 1
+ VERSION=7
+ '[' 7 == 7 ']'
+ echo 'CentOS 7'
CentOS 7
上次编辑于:
贡献者: stonebox,stone