Shell脚本编程

提示

本文中由于编译的问题,将所有英文括号换成了中文括号,需要注意!

Shell脚本编程

什么是Shell

Shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。Shell 既是一种命令语言,又是一种程序设计语言。

Shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。

Ken Thompson 的 sh 是第一种 Unix Shell,Windows Explorer 是一个典型的图形界面 Shell。

Shell脚本

当命令不在命令行中执行,而是从一个文件中执行时,该文件就称为 Shell 脚本。

  • Shell 脚本是纯文本文件。
  • Shell 脚本通常以 .sh 作为后缀名,但不是必须。
  • Shell 脚本是以行为单位的,在执行脚本的时候会分解成一行一行依次执行。
  • Shell 是一种功能强大的解释型编程语言,通常用于完成特定的、较复杂的系统管理任务。
  • Shell 脚本语言非常擅长处理文本类型的数据。

脚本其实就是短小的、用来让计算机自动化完成一系列工作的程序,这类程序可以用文本编辑器修改,不需要编译,通常是解释运行的。

Shell 环境

Shell 编程跟 JavaScript、php 编程一样,只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。

Linux 的 Shell 种类众多,常见的有:

  • Bourne Shell(/usr/bin/sh或/bin/sh)
  • Bourne Again Shell(/bin/bash)
  • C Shell(/usr/bin/csh)
  • K Shell(/usr/bin/ksh)
  • Shell for Root(/sbin/sh)
  • ……

我们最常使用的通常是Bash。 同时,Bash 也是大多数Linux 系统默认的 Shell。 一般而言,bash和sh不严格区分。

Shell编程的步骤

一般来说,编写、执行一个Shell脚本需要如下的步骤:

  1. 创建一个文本文件,通常以.sh结尾,但不影响脚本的执行(Linux的特性)

    touch test.sh

  2. 使用文本编辑器打开该文件,一般可以使用gedit、vim等

    vim test.sh

  3. 输入脚本代码,通常我们还需要在开头声明执行脚本的程序的解释器,即哪一种Shell解释器

    #!/bin/bash
    This is the first Bash shell program 
    # Scriptname: greetings.sh
    echo
    echo -e "Hello $LOGNAME, \c"
    echo    "it's nice talking to you."
    echo -n "Your present working directory is: "
    pwd # Show the name of present directory
    echo
    echo -e "The time is `date +%T`!. \nBye"
    echo
  4. 保存脚本,并为其添加可执行权限

    chmod +x test.sh

  5. 执行脚本,这里必须使用./告诉系统在当前目录下寻找脚本。因为不加./,系统会默认从环境变量$PATH中的路径寻找可执行的脚本,而一般我们的路径都不在$PATH中。

    ./test.sh

  6. 也可以直接使用解释器执行,将文件名作为解释器的参数:

    bash test.sh

Shell变量

Shell变量的类型

Shell变量主要可以分为4类:

  • 用户自定义变量
    • 由用户自己定义、修改和使用
  • Shell 环境变量
    • 由系统维护,用于设置用户的Shell工作环境
    • 只有少数的变量用户可以修改其值
  • 位置参数变量(Positional Parameters)
    • 通过命令行给程序传递执行参数
    • 可用 shift 命令实现位置参数的迁移
  • 专用参数变量(Special Parameters)
    • Bash 预定义的特殊变量
    • 用户不能修改其值

Shell变量的定义和赋值

定义变量时,变量名不加美元符号($,PHP语言中变量需要),如:

a=0

注意,变量名和等号之间不能有空格,这可能和你熟悉的所有编程语言都不一样。同时,变量名的命名须遵循如下规则:

  • 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头。
  • 中间不能有空格,可以使用下划线(_)。
  • 不能使用标点符号。
  • 不能使用bash里的关键字(可用help命令查看保留关键字)。

除了显式的为变量赋值,我们还可以使用命令赋值,如:

for file in $(ls /etc)

此外,还可以用read语句通过读取输入端口的输入为变量赋值,如:

read -p “please input:” name

Shell变量的使用

使用一个定义过的变量,只要在变量名前面加美元符号即可,如:

your_name="qinjx"
echo $your_name
echo ${your_name}

变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界,比如下面这种情况:

for skill in Ada Coffe Action Java; do
    echo "I am good at ${skill}Script"
done

如果不给skill变量加花括号,写成echo “I am good at $skillScript",解释器就会把$skillScript当成一个变量(其值为空),代码执行结果就不是我们期望的样子了。此外,变量还可以参与计算,此时必须加上花括号。

推荐给所有变量加上花括号,这是个好的编程习惯。

已定义的变量,可以被重新定义,如:

your_name="tom"
echo $your_name
your_name="alibaba"
echo $your_name

需要注意的是:定义变量时一律不加$,使用变量时一律加$

只读变量

使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。

#!/bin/bash
a=0
readonly a
a=1

如果尝试修改只读变量的值,程序将报错。类似于Java中的final。

删除变量

使用 unset 命令可以删除变量。语法:

unset variable_name

变量被删除后不能再次使用。unset 命令不能删除只读变量。

被删除变量的所有资源将被系统回收。

变量的间接引用

变量的间接引用在高级语言中十分常见,特别是可变类型的变量。Shell中以下两种方式来进行间接引用:

  • 使用感叹号:
# bash2.0以上才支持
newstr=${!str2}
echo $newstr
Hello World
或
echo ${!str2}
Hello World 
  • 使用$
eval newstr=`$$str2
echo $newstr
Hello World
或
eval echo `$$str2
Hello World 

内置命令eval

eval arg1 [arg2] … [argN]

Shell内置命令eval通常用于解释并执行字符串类型的命令,我们可以将命令以字符串的形式进行拼接修改,最后用eval命令进行执行。

它对参数进行两次扫描和替换:

  • 将所有的参数连接成一个表达式,并计算或执行该表达式

  • 参数中的任何变量都将被展开

listpage="ls -l | more"
eval $listpage

位置参数变量

位置参数变量是一组特殊的内置变量,一般用于跟在函数调用之后或者执行脚本时脚本名的后面。

$1代表的是第一个位置参数变量,${11}代表第十一个位置参数变量。

用处主要是:

  • 从 shell 命令/脚本 的命令行接受参数
  • 在调用 shell 函数时为其传递参数

shift [n]

  • 将位置参量列表依次左移n次,缺省为左移一次
  • 一旦位置参量列表被移动,最左端的那个参数就会从列表中删除
  • 经常与循环结构语句一起使用,以便遍历每一个位置参数
#!/bin/sh
# ScriptName: pp_shift.sh
# To test Positional Parameters & Shift.
echo "The script name is :  $0"
echo '$1'=$1,'$2'=$2,'$3'=$3,'$4'=$4   --   '$#'="$#" 
echo '$@': "$@" 
shift              # 向左移动所有的位置参数1次
echo '$1'=$1,'$2'=$2,'$3'=$3,'$4'=$4   --   '$#'="$#"
echo '$@': "$@"
shift 2            # 向左移动所有的位置参数2次
echo '$1'=$1,'$2'=$2,'$3'=$3,'$4'=$4   --   '$#'="$#"
echo '$@': "$@"

专用参数变量

用户无法修改这些值。

位置参数相关

  • $* 将所有位置参量看成一个字符串(以空格间隔) 。
  • $@ 将每个位置参量看成单独的字符串(以空格间隔)。
  • $*” 将所有位置参量看成一个字符串(以$IFS间隔)。
  • $@” 将每个位置参量看成单独的字符串(以$IFS间隔) 。
  • $0 命令行上输入的Shell程序名。
  • $# 表示命令行上参数的个数。

进程状态相关

  • $? 表示上一条命令执行后的返回值
    • 0:成功
    • 1-255:不成功
      • 1:通用错误
      • 126:命令或脚本没有执行权限
      • 127:命令没找到
    • 通常与exit命令配合使用,用于退出脚本或当前Shell
  • $$ 当前进程的进程号
  • $! 显示运行在后台的最后一个作业的 PID
  • $_ 在此之前执行的命令或脚本的最后一个参数

Shell字符串

字符串是shell编程中最常用最有用的数据类型(除了数字和字符串,也没啥其它类型好用了),字符串可以用单引号,也可以用双引号,也可以不用引号。单双引号的区别跟PHP类似。

单引号

str='this is a string'

单引号字符串的限制:

  • 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的;
  • 单引号字串中不能出现单独一个的单引号(对单引号使用转义符后也不行),但可成对出现,作为字符串拼接使用。

双引号

your_name='runoob'
str="Hello, I know you are \"$your_name\"! \n"
echo -e $str

输出结果为:

Hello, I know you are "runoob"! 

双引号的优点:

  • 双引号里可以有变量
  • 双引号里可以出现转义字符(但必须在使用echo语句时,加上-e选项,解释转义字符)

单双引号最显著的区别在于:单引号不解释变量,双引号解释变量且支持转义。

反撇号

反撇号 ` `:展开变量并执行表达式,将命令执行的结果输出给变量。

string="runoob is a great site"
count=`expr index "$string" io`  # 输出 4

类似的,$(expression)也有类似的作用,可以用来获取执行命令的结果:

#!/bin/bash

echo $(expr 3 + 4)

拼接字符串

拼接时可以使用双引号,也可以使用单引号。嵌套的单引号将解释变量,但不嵌套的单引号不解释内层的变量。

your_name="runoob"
# 使用双引号拼接
greeting="hello, "$your_name" !"
greeting_1="hello, ${your_name} !"
echo $greeting  $greeting_1
# 使用单引号拼接
greeting_2='hello, '$your_name' !'
greeting_3='hello, ${your_name} !'
echo $greeting_2  $greeting_3

输出结果为:

hello, runoob ! hello, runoob !
hello, runoob ! hello, ${your_name} !

字符串计数

使用#符号,加在字符串变量之前进行计数。

string="abcd"
echo ${#string} #输出 4

字符串截取

${var:m}将截取变量${var}从第m个字符到最后一个字符的子串,`${var:\m:len}截取从第m个字符开始,长度为len的部分。默认情况下,变量的字符编号从0开始。

string="runoob is a great site"
echo ${string:1:4} # 输出 unoo

字符串删除

命令 含义
${var#pattern} 删除${var}中开头部分与pattern匹配的最小部分
${var##pattern} 删除${var}中开头部分与pattern匹配的最大部分(贪心)
${var%pattern} 删除${var}中结尾部分与pattern匹配的最小部分
${var%%pattern} 删除${var}中结尾部分与pattern匹配的最大部分(贪心)

需要注意的是,这里的模式串pattern可以是正则表达式,

str='I love linux. I love UNIX too.’
echo ${str#I love}
linux. I love UNIX too.
echo ${str#I*.}
I love UNIX too.
echo ${str##I*}

字符串替换

命令 含义
${var//old/new} 用new替换${var}中所有的old(全局替换)
${var/#old/new} 用new替换${var}中开头部分与old匹配的部分
${var/old/new} 用new替换${var}中第一次出现的old
${var/%old/new} 用new替换${var}中结尾部分与old匹配的部分
  • old 中可以使用 通配符。
  • var 可以是 @ 或 *,表示对每个位置参数进行替换
str='I love linux. I love UNIX too.’
echo ${str/love/like}
I like linux. I love UNIX too.
echo ${str//love/like}
I like linux. I like UNIX too.
echo ${str/I*linux/I like FreeBSD}
I like FreeBSD. I love UNIX too.
echo ${str/#I love/"J'aime"}
J'aime linux. I love UNIX too.
echo ${str//I love/"J'aime"}
J'aime linux. J'aime UNIX too. 
echo ${str/%too./also.}
I love linux. I love UNIX also.

Shell输入输出

read命令

read [-p “提示信息”] [var1 var2 …]

read命令从键盘输入内容为变量赋值。若省略变量名,则将输入的内容存入系统变量$REPLY变量。

#!/bin/bash
# This script is to test the usage of read
# Scriptname: ex4read.sh
echo "=== examples for testing read ==="
echo -e "What is your name? \c"
read name
echo "Hello $name"
echo
echo -n "Where do you work? "
read
echo "I guess $REPLY keeps you busy!"
echo
read -p "Enter your job title: "
echo "I thought you might be an $REPLY."
echo
echo "=== End of the script ==="

echo命令

echo 字符串

Shell 的 echo 指令与 PHP 的 echo 指令类似,都是用于字符串的输出。

字符串的类型依然分为三类,分别是单引号、双引号、反撇号,具体功能见上一章。

这里需要注意的是:echo命令输出时,双引号可以省略,但如果字符串内有空格,必须加上双引号

如果在字符串内使用了转义字符,我们在使用echo进行输出时还需要开启转义,具体做法是采用-e选项。

echo -e "OK! \n" # -e 开启转义
echo "It is a test"

echo命令可以使用重定向符号进行标准输出端口的重定向(默认是终端的屏幕)。

printf 命令

printf format 输出参数列表

printf命令与C++等高级语言中类似,都是用来输出格式化字符串的。

格式字符串format的规格如下:

![printf](printf.png)

常用的格式替代符有:

  • %s:字符串string
  • %c:字符char
  • %d:整型数
  • %f :浮点数
#!/bin/bash

printf "%-10s %-8s %-4s\n" 姓名 性别 体重kg  
printf "%-10s %-8s %-4.2f\n" 郭靖 男 66.1234 
printf "%-10s %-8s %-4.2f\n" 杨过 男 48.6543 
printf "%-10s %-8s %-4.2f\n" 郭芙 女 47.9876 

printf命令支持如下的转义序列:

序列 说明
\a 警告字符,通常为ASCII的BEL字符
\b 后退
\c 抑制(不显示)输出结果中任何结尾的换行字符(只在%b格式指示符控制下的参数字符串中有效),而且,任何留在参数里的字符、任何接下来的参数以及任何留在格式字符串中的字符,都被忽略
\f 换页(formfeed)
\n 换行
\r 回车(Carriage return)
\t 水平制表符
\v 垂直制表符
\ 一个字面上的反斜杠字符
\ddd 表示1到3位数八进制值的字符。仅在格式字符串中有效
\0ddd 表示1到3位的八进制值字符
printf "%6d\t%6o\"%6x\"\n" 20 20 20

Shell整数运算

Bash 变量没有严格的类型定义

  • 本质上 Bash 变量都是字符串
  • 若一个字面常量或变量的值是纯数字的,不包含字母或其他字符, Bash可以将其视为长整型值,并可做算数运算和比较运算。

Bash 也允许显式地声明整型变量

  • declare -i 变量名

declare命令

declare [选项] variable[=value]

Shell内置命令 declare 可用来显式地声明变量,因为Shell默认是弱类型语言,使用declare命令可以强制规定变量的类型,无需等到执行到具体的命令时才知道变量的类型。

选项 含义
-r 将变量设为只读 ( readonly )
-x 将变量输出到子 shell 中(export 为全局变量)
-i 将变量设为整型 ( integer )
-a 将变量设置为一个数组 ( array )
-f 列出函数的名字和定义 ( function )
-F 只列出函数名

算术运算符

算术运算符
+、 -、 *、 / (四则运算)
**、 % (幂运算 和 模运算,取余数)
<<、 >> (按位左移 和 按位右移)
&、 ^、 | (按位与 、按位异或 和 按位 或)
=、 +=、 -= 、 *=、 /= 、 %= 、<<= 、 >>= 、 &=、 ^=、 |= (赋值运算)
<、 >、 <=、 >=、 ==、 != (比较操作符)
&&、 || (逻辑与 和 逻辑 或)

Shell自带的算术运算符基本与Java类似,含义也类似。

expr命令

原生bash不支持简单的数学运算,但是可以通过其他命令来实现,例如 awk 和 expr,expr 最常用。

expr 是一款表达式计算工具,使用它能完成表达式的求值操作。

#!/bin/bash

val=`expr 2 + 2`
echo "两数之和为 : $val"

两点注意:

  • 表达式和运算符之间要有空格,例如 2+2 是不对的,必须写成 2 + 2,这与我们熟悉的大多数编程语言不一样。
  • 完整的表达式要被``包含,注意这个字符不是常用的单引号,在 Esc 键下边。
  • 表达式中的运算可以是算术运算,比较运算,字符串运算和逻辑运算。

$[expression]$((expression))

同时,随着Bash的升级,我们也可以使用符号来声明和进行整数运算。

num1=$[4+1]; echo $num1
num1=$(($num1*2-3)); echo $num1

需要注意的是,用 $[···]$((···)) 进行整数运算时,括号内变量前的美元符号 $ 可以省略。

这样运算的话,不需要在表达式和运算符之间打空格(不过推荐都打上,养成良好习惯,看的也清楚,赋值时不能打)

let命令

Shell还内置了let命令用于算术运算。

num2=1; echo $num2
let num2=4+1; echo $num2
let num2=$num2+1; echo $num2
let "num2=4 + 1" # 用引号忽略空格的特殊含义

需要注意以下几点:

  • 赋值符号和运算符两边不能留空格!除非用引号将算式括起来。

  • 如果将字符串赋值给一个整型变量时,则变量的值为 0。

  • 如果变量的值是字符串,则进行算术运算时设为 0。

Shell浮点数运算

bash 只支持整数运算。

但我们可以使用一些工具,例如:可以通过使用 bc 或 awk 工具来处理浮点数运算。

n=$(echo "scale=3; 13/2" | bc )
echo $n

m=`awk 'BEGIN{x=2.45;y=3.123; \ 
   printf "%.3f\n", x*y}'`
echo $m

Shell数组

数组的定义

数组中可以存放多个值。Bash Shell 只支持一维数组(不支持多维数组),初始化时不需要定义数组大小(与 PHP 类似)。

与大部分编程语言类似,数组元素的下标由0开始。

Shell 数组用括号来表示,元素用”空格”符号分割开,语法格式如下:

array_name=(value1 … valuen)

与python等弱类型脚本语言类似,Shell数组可以存放任意类型的值,这一点和C++、Java等编译型语言不同:

my_array=(A B "C" D)

我们也可以使用declare命令声明数组,直接使用下标定义元素,并使用`${array_name[index]}去访问元素,这里的下标可以为变量:

declare -a array_name

array_name[0]=value0
array_name[1]=value1
array_name[2]=value2

echo "第一个元素为: ${my_array[0]}"
echo "第二个元素为: ${my_array[1]}"
echo "第三个元素为: ${my_array[2]}"
echo "第四个元素为: ${my_array[3]}"

使用@ 或 * 可以获取数组中的所有元素,获取所有元素后我们可以使用#符号获取数组的长度,和字符串类似,例如:

my_array[0]=A
my_array[1]=B
my_array[2]=C
my_array[3]=D

echo "数组的元素为: ${my_array[*]}"
echo "数组的元素为: ${my_array[@]}"
echo "数组元素个数为: ${#my_array[*]}"

数组与数组元素的删除

使用unset命令:

unset stu[1]   # 删除stu的第二个元素
unset stu      # 删除整个数组

Shell条件测试

条件测试可以判断某个特定条件是否满足,通常是命令是否成功或者是表达式的真假。

条件测试的值:

  • Bash中没有布尔类型变量
    • 退出状态为 0 表示命令成功或表达式为真
    • 非0 则表示命令失败或表达式为假
  • 状态变量 $? 中保存了退出状态的值Bash中没有布尔类型变量

条件测试的格式

Shell支持三种格式的条件测试语句,分别为:

  • 格式1: test <测试表达式>
  • 格式2: [ <测试表达式> ]
  • 格式3: [[ <测试表达式> ]] (bash 2.x 版本以上)
  • 格式4: ((<测试表达式>)) (整数关系运算)

说明:

  • 格式1 和 格式2 是等价的,格式3是扩展的 test 命令
  • 在 [[ ]] 中可以使用通配符进行模式匹配
  • &&, ||, <, 和>能够正常存在于[[ ]]中,但不能在 [] 中出现
  • [和[[之后的字符必须为空格,]和]]之前的字符必须为空格
  • 要对整数进行关系运算也可以使用 (()) 进行测试

文件测试

文件测试用于检测文件是否存在,文件属性,访问权限等。

命令 含义
[ -f fname ] fname 存在且是普通文件时,返回真 ( 即返回 0 )
[ -L fname ] fname 存在且是链接文件时,返回真
[ -d fname ] fname 存在且是一个目录时,返回真
[ -e fname ] fname(文件或目录)存在时,返回真
[ -s fname ] fname 存在且大小大于 0 时,返回真
[ -r fname ] fname(文件或目录)存在且可读时,返回真
[ -w fname ] fname(文件或目录)存在且可写时,返回真
[ -x fname ] fname(文件或目录)存在且可执行时,返回真

字符串测试

命令 含义
[ -z string ] 如果字符串string长度为0,返回真
[ -n string ] 如果字符串string长度不为0,返回真
[ str1 = str2 ] 两字符串相等(也可使用 == )返回真
[ str1 != str2 ] 两字符串不等返回真
[[ str1 == str2 ]] 两字符串相同返回真
[[ str1 != str2 ]] 两字符串不相同返回真
[[ str1 =~ str2 ]] str2是str1的子串返回真
[[ str1 > str2 ]] str1大于str2返回真(按照Ascii码进行比较)
[[ str1 < str2 ]] str1小于str2返回真

整数测试

命令 含义
[ int1 -eq int2 ] int1 等于 int2 返回真
[ int1 -ne int2 ] int1 不等于 int2 返回真
[ int1 -gt int2 ] int1 大于 int2 返回真
[ int1 -ge int2 ] int1 大于或等于 int2 返回真
[ int1 -lt int2 ] int1 小于 int2 返回真
[ int1 -le int2 ] int1 小于或等于 int2 返回真
命令 含义
[[ int1 == int2 ]] int1 等于 int2 返回真
[[ int1 != int2 ]] int1 不等于 int2 返回真
[[ int1 > int2 ]] int1 大于 int2 返回真
[[ int1 >= int2 ]] int1 大于或等于 int2 返回真
[[ int1 <int2 ]] int1 小于 int2 返回真
[[ int1 <= int2 ]] int1 小于或等于 int2 返回真
命令 含义
((int1 == int2)) int1 等于 int2 返回真
((int1 != int2)) int1 不等于 int2 返回真
((int1 > int2)) int1 大于 int2 返回真
((int1 >= int2)) int1 大于或等于 int2 返回真
((int1 < int2)) int1 小于 int2 返回真
((int1 <= int2)) int1 小于或等于 int2 返回真

观察上述表格,我们可以发现:

  • []和[[]]两侧必须加空格

  • [[]]可以加入诸如>、<等符号的逻辑运算符,而[]只能使用字母来表示

  • (())两侧不一定需要加入空格,可省略

  • []不能加入通识匹配符,如>、<

逻辑测试

命令 含义
[ expr1 -a expr2 ] 逻辑与,都为真时,结果为真
[ expr1 -o expr2 ] 逻辑或,有一个为真时,结果为真
[ ! expr ] 逻辑非
命令 含义
[[ pattern1 && pattern2 ]] 逻辑与
[[ pattern1 || pattern2 ]] 逻辑或
[[ ! pattern ]] 逻辑非
命令 含义
(( expr1 && expr2 )\) 逻辑与
(\( expr1 || expr2 )) 逻辑或
(( ! expr )) 逻辑非

需要注意的点有:

  • 不能随便添加括号
  • 不能在 (()) 中做字符串比较,只能进行整数运算

Shell流程控制

分支语句if

分支语句if通常和条件测试语句配合使用。

if expr1      # 如果 expr1 为真(返回值为0)
then          # 那么
   commands1  # 执行语句块 commands1
elif expr2    # 若 expr1 不真,而 expr2 为真
then          # 那么
   commands2  # 执行语句块 commands2
 ... ...      # 可以有多个 elif 语句 
else          # else 最多只能有一个
   commands4  # 执行语句块 commands4
fi            # if 语句必须以单词 fi 终止

注意点:

  • elif 可以有任意多个(0 个或多个)
  • else 最多只能有一个(0 个或 1 个)
  • if 语句必须以 fi 表示结束
  • exprX 通常为条件测试表达式;也可以是多个命令,以最后一个命令的退出状态为条件值。
  • commands 为可执行语句块,如果为空,需使用 shell 提供的空命令 “ : ”,即冒号。该命令不做任何事情,只返回一个退出状态 0
  • if 语句可以嵌套使用
#!/bin/bash
## filename: ask-age.sh
read  -p "How old are you?  "  age 
# 使用 Shell算术运算符(())进行条件测试
if ((age<0||age>120)); then
    echo "Out of range !"
    exit 1
fi 
# 使用多分支if语句
if   ((age>=0&&age<13)) ; then   echo "Child !"
elif ((age>=13&&age<20)); then   echo "Callan !"
elif ((age>=20&&age<30)); then   echo "P III !"
elif ((age>=30&&age<40)); then   echo "P IV !"
else   echo "Sorry I asked."
fi

分支语句Case

与高级语言类似,Shell中也存在着Case语句,用来进行多分支的判断。

case expr in # expr 为表达式,关键词 in 不要忘!
  pattern1)  # 若 expr 与 pattern1 匹配,注意括号
   commands1 # 执行语句块 commands1
   ;;        # 跳出 case 结构
  pattern2)  # 若 expr 与 pattern2 匹配
   commands2 # 执行语句块 commands2
   ;;        # 跳出 case 结构  ... ...    # 可以有任意多个模式匹配
  *)         # 若 expr 与上面的模式都不匹配
   commands  # 执行语句块 commands
   ;;        # 跳出 case 结构
esac         # case 语句必须以 esac 终止

需要注意的点有:

  • 表达式 expr 按顺序匹配每个模式,一旦有一个模式匹配成功,则执行该模式后面的所有命令,然后退出 case。
  • 如果 expr 没有找到匹配的模式,则执行缺省值 “ *) ” 后面的命令块( 类似于 if 中的 else );“ *) ” 可以不出现。
  • 所给的匹配模式 pattern 中可以含有通配符和“ | ”。
  • 每个命令块的最后必须有一个双分号;;,可以独占一行,或放在最后一个命令的后面。
  • case语句必须以esac终止。
#!/bin/bash
## filename: all_in_one_backup.sh
# A shell script to backup mysql, webserver and files.
# opt=$1
case $1 in
   sql) echo "Running mysql backup using mysqldump tool..." ;;
  sync) echo "Running backup using rsync tool..."           ;;
   git) echo "Running backup using gistore tool..."         ;;
   tar) echo "Running tape backup using tar tool..."        ;;
     *) 
        echo "Backup shell script utility"
        echo "Usage: $0 {sql|sync|git|tar}"
        echo "    sql  : Run mySQL backup utility."
        echo "    sync : Run web server backup utility."    
        echo "    git  : Run gistore backup utility."    
        echo "    tar  : Run tape backup utility." 
        ;;
esac

循环语句For

与高级语言类似,Shell支持两种类型的循环,分别是foreach型和c语言型

for循环(foreach型)

for variable in list 
# 每一次循环,依次把列表 list 中的一个值赋给循环变量
do          # 循环体开始的标志
  commands  # 循环变量每取一次值,循环体就执行一遍
done        # 循环结束的标志,返回循环顶部

说明:

  • 列表 list 可以是命令替换、变量名替换、字符串和文件名列表 ( 可包含通配符 ),每个列表项以空格间隔
  • for 循环执行的次数取决于列表 list 中单词的个数
  • 可以省略 in list , 如果不用它,for循环使用命令行的位置参数,相当于 in “$@”。
#!/bin/bash
## filename: for1--constant_as_list.sh
# 使用字面字符串列表作为 WordList
for x in centos ubuntu gentoo opensuse
do   echo "$x" ; done
# 若列表项中包含空格必需使用引号括起来
for x in Linux "Gnu Hurd" FreeBSD "Mac OS X"
do  echo "$x" ; done
for x in ls "df -h" "du -sh"
do
    echo "==$x==" ; eval $x
done

需要注意,如果同一行出现了两条命令,需要在他们之间加分号;

下面的情况特别需要注意,用于分割的空格不能被包含进双引号内:

  • for x in “centos” “ubuntu” “gentoo” “opensuse”,输出的是四个单词
  • for x in “centos ubuntu gentoo opensuse”,输出的是一整个句子

更多实例:

#!/bin/bash
## filename: for3--pp_as_list.sh
# 使用位置参数变量 $@ 作为 WordList, in $@ 可以省略
i=1
for day ; do
  echo -n "Positional parameter $((i++)): $day "
  case $day in
    [Mm]on|[Tt]ue|[Ww]ed|[Tt]hu|[Ff]ri)
       echo " (weekday)" ;;
    [Ss]at|[Ss]un)
       echo " (WEEKEND)" ;;
    *) echo " (Invalid weekday)" ;;
  esac
done
#!/bin/bash
## filename: for4--filenames_as_list.sh
# 使用文件名或目录名列表作为 WordList

# 将当前目录下的所有的大写文件名改为小写文件名
for filename in * ; do
  # 使用命令替换生成小写的文件名,赋予新的变量 fn
  fn=$(echo $fname | tr A-Z a-z)
  # 若新生成的小写文件名与原文件名不同,改为小写的文件名
  if [[ $fname != $fn ]] ; then mv $fname $fn ; fi
  # 上面的 if 语句与下面的命令聚合均等效
  # [[ $fname != $fn ]] && mv $fname $fn
  # [[ $fname == $fn ]] || mv $fname $fn
done

for fn in /etc/[abcd]*.conf ; do echo $fn ; done
for fn in /etc/cron.{*ly,d}/* ; do echo $fn ; done
for i in *.zip; do 
  j="${i%.zip}"; mkdir "$j" && unzip -d "$j" "$i"
done
#!/bin/bash
## filename: for5--command_output_as_list.sh
# 使用命令的执行结果作为 WordList
i=1
for username in `awk -F: '{print $1}' /etc/passwd` 
do
    echo "Username $((i++)) : $username"
done

for line in $(cat files.txt|egrep -v "^$|^#") ; do
    echo "$line"; done 

for suffix in $(seq 254)
do echo "192.168.0.${suffix}"; done

for f in $( ls /var/ ); do echo $f; done

for循环(C语言型)

C 语言风格的 for 语句通常用于实现计数型循环。该循环方式和C语言、Java等非常类似:

for ((expr1;expr2;expr3)) # 执行 expr1
do # 若 expr2的值为真时进入循环,否则退出 for循环
  commands  # 执行循环体,之后执行 expr3
done        # 循环结束的标志,返回循环顶部
  • expr1:初始赋值
  • expr2:结束条件
  • expr3:每轮变化
#!/bin/bash
## filename: addusers_for_C-style.sh
# 成批添加50个用户

for (( n=1; n<=50; n++ ))
do
    if ((num<10))
    then  st="st0${n}" 
    else  st="st${n}"
    fi
    useradd $st
    echo "centos"|passwd --stdin $st
    chage -d 0 $st
done

break和continue

break [n]

用于强行退出当前循环。

如果是嵌套循环,则 break 命令后面可以跟一数字 n,表示退出第 n 重循环(最里面的为第一重循环)。

continue [n]

用于忽略本次循环的剩余部分,回到循环的顶部,继续下一次循环。

如果是嵌套循环,continue 命令后面也可跟一数字 n,表示回到第 n 重循环的顶部。

While循环语句

格式:

while expr  # 执行 expr
do # 若expr的退出状态为0,进入循环,否则退出while
  commands  # 循环体
done        # 循环结束标志,返回循环顶部

While语句当expr为真时(状态不为0),循环执行。

实例:

  • 使用重定向符号为while的条件表达式中的read进行输入重定向。
#!/bin/bash
## filename: while--read_file.sh
file=/etc/resolv.conf
while IFS= read -r line
do
    # echo line is stored in $line
    echo $line
done < "$file"

while IFS=: read -r user enpass uid gid desc home shell
do
    # only display if UID >= 500 
    [ $uid -ge 500 ] && echo "User $user ($uid) assigned \"$home\" home directory with $shell shell."
done < /etc/passwd

  • 使用管道符为while传入输入
#!/bin/bash
## filename: while-rename_filename.sh
# 找出当前目录下包含空格的文件名,将空格替换为下划线

DIR="."
find $DIR -type f | while read file; do
  # using POSIX class [:space:] to find space in the filename
  if [[ "$file" = *[[:space:]]* ]]; then
     # substitute space with "_" character (rename the filename)
     mv "$file" $(echo $file | tr ' ' '_')
  fi
done

until循环语句

格式:

until expr  # 执行 expr
do # 若expr的退出状态非0,进入循环,否则退出until
  commands  # 循环体
done        # 循环结束标志,返回循环顶部

While语句当expr为假时(状态不为0),循环执行。

实例:

#!/bin/bash
## filename: until-user_online_to_write.sh
username=$1
if [ $# -lt 1 ] ; then
  echo "Usage: `basename $0`    []"
  exit 1
fi
if grep "^$username:" /etc/passwd > /dev/null ; then   :
else
  echo "$username is not a user on this system."
  exit 2
fi
until who|grep "$username" > /dev/null ; do
    echo "$username is not logged on."
    sleep 600
done
shift ; msg=$*
[[ X"$msg" == "X" ]] && msg="Hello, $username"
echo "$msg" | write $username

#!/bin/bash
## filename: while-until-for_sum.sh

# 使用当型循环求 sum(1..100)
((i=0,s=0))        # i=0 ; s=0
while ((i<100)) ; do ((i++,s+=i)) ; done
echo sum(1..100)=$s

# 使用直到型循环求 sum(1..100)
((i=0,s=0))
until ((i==100)) ; do ((i++,s+=i)) ; done
echo sum(1..100)=$s

# 使用C风格的 for 循环求 sum(1..100)
for ((s=0,i=1;i<=100;s+=i,i++)) ; do : ; done
echo sum(1..100)=$s

循环语句的使用技巧

循环语句可以在末尾done之后加入管道符和重定向符,用来重定向当前循环体内的输入、输出端口,以及将循环体内的命令执行结果传入其他命令处理。

  • 管道:
#!/bin/bash
## filename: loop--to_pipe.sh

for i in 7 8 9 2 3 4 5 11; do
    echo $i
done | sort -n


awk -F':' '$3 >= 500 {print $1}' /etc/passwd | 
while IFS= read -r person 
do
    echo $person 
done | sort 

也可以在done后加入&,来将循环体放在后台运行:

#!/bin/bash
## filename: loop--in_background.sh

for person in Brown Jiff John Stone
do
     mail -s "Test" $person < "Hello $person ."
done &

awk -F':' '$3 >= 500 {print $1}' /etc/passwd | 
while IFS= read -r person
do
    mail -s "Test" $person <<-END
    Hello $person,
        This message is from $(hostname -f).
                     $USER   $(date +%F)
    END
done &

Select语句实现菜单

select 循环主要用于创建菜单,通常与case语句配合使用,根据用户输入的不同进入不同的菜单分支。
select 是个无限循环,退出方式为:

  • 按 ctrl+c 退出循环
  • 在循环体内用 break 命令退出循环
  • 或用 exit 命令终止脚本

格式:

select variable in list 
do          # 循环开始的标志
  commands  # 循环变量每取一次值,循环体就执行一遍
done        # 循环结束的标志

需要注意的点有:

  • 按数值顺序排列的菜单项(list item)会显示到标准错误
  • 菜单项的间隔符由环境变量 IFS 决定
  • 用于引导用户输入的提示信息存放在环境变量 PS3 中
  • 用户输入的值会被存储在内置变量 RELAY 中
  • 用户直接输入回车将重新显示菜单
  • 与 for 循环类似,省略 in list 时等价于 in “$*

实例:

#!/bin/bash
## filename: what-os-do-you-like_select.sh
clear
PS3="What is your preferred OS? "
IFS='|'
os="Linux|Gnu Hurd|FreeBSD|Mac OS X"
select s in $os
do
  case $REPLY in
    1|2|3|4) echo "You selected $s"  ;;
          *) exit ;;
  esac
done

位置参数处理

在脚本中经常使用流程控制处理位置参数:

  • 循环结构:while、for
  • 多分支结构:case

在脚本中经常使用如下命令配合位置参数处理:

  • shift
  • getopts

shift命令

shift命令没有参数,它的作用是将所有位置向左循环移动一位。

以下是一个while循环+shift命令,循环遍历命令行位置参数的例子:

#!/bin/bash
## filename: pp_traverse_shift_while.sh
# Usage: pp_traverse_shift_while.sh [arguments]
#
echo "using while loop to traverse positional parameter"

#while [[ "$1" ]] ; do
#    echo "$1"
#    shift
#done

num=1
while [[ "$1" ]] ; do
    echo "The ${num}th argument is: $1"
    let num=num+1
    shift
done

getops命令

格式

getopts OPTSTRING VARNAME [ARGS…]

OPTSTRING :

  • 是由若干有效的选项标识符组成的选项字符串
  • 若某选项标识符后有冒号,则表示此选项有附加参数
  • 若整个字符串前有冒号,将使用“安静”的错误模式

VARNAME :

  • 每次匹配成功的选项保存在变量中

ARGS ::

  • 参数列表,省略时为 ”$@”

执行过程

  • 通常需要以循环的方式执行多次 getopts 来解析位置参数中的选项以及可能存在的选项附加参数
  • 每次调用 getopts,将会处理参数列表中的“下一个”选项
  • 将选项存储在VARNAME变量中
  • 将此选项对应的附加参数存储在环境变量OPTARG中
  • 对环境变量OPTIND进行自增操作,使 OPTIND 总是指向原始参数列表中“下一个”要处理的元素位置
  • 若VARNAME与OPTSTRING的所有选项均不匹配,则做“invalid option”的错误设置
  • 若某选项的参数不存在,则做“required argument not found”的错误设置

注意事项

  • getopts 不能解析 GNU-style 长参数 (–myoption)
  • getopts从不改变原始位置参数
  • 若希望移动位置参数,需手工执行 shift
  • getopts会自动对变量 OPTIND 做自增处理
  • OPTIDX的初始值为 1
  • 若要重新解析命令行参数,需将OPTIDX的值置为 1
  • getopts 遭遇到第一个非选项参数时终止解析
  • 终止解析后执行命令
    shift ((OPTIND-1))
  • 可以使 ”$@” 只包含“操作 对象/数”(operands)

实例

#!/bin/bash
## filename : mybackup_getopts2.sh
while getopts :zc:x:rv opt
do
  case $opt in
    c) if [[ $OPTARG = -* ]]; then  ((OPTIND--)) ;  continue ;  fi
       ConfFile=$OPTARG       ;;
    x) ExcludeFile=$OPTARG    ;;
    z) Compress=true          ;;
    r) Recursive=true         ;;
    v) Verbose=true           ;;
    :)
      echo "$0: Must supply an argument to -$OPTARG." >&2
      exit 1
      ;;
    \?) echo "Invalid option -$OPTARG ignored." >&2   ;;
  esac
done

shift ((OPTIND-1)) ; echo $0 ; echo "$@"

环境变量说明:

  • OPTIND:存放当前遍历的选项的下标
  • OPTARG:存放当前选项的附加参数
  • VARNAME:存放当前选项

Shell函数

使用函数式编程有如下的优点:

  • 简化程序代码,实现代码重用
    • 实现一次定义多次调用。如:is_root_user()函数可以由不同的shell脚本重复使用。
  • 实现结构化编程
    • 使脚本内容更加简洁,增强程序的易读性
  • 提高执行效率
    • 将常用的功能定义为多个函数并将其保存在一个文件中
    • 类似其他语言的“模块”文件
      • 在 ~/bashrc 或命令行上使用 source 命令调用这个文件
    • 此文件中定义的多个函数一次性地调入内存,从而加快运行速度

函数定义

Shell函数的定义可以带function fun() 定义,也可以直接fun() 定义,不带任何参数。 如:

function 函数名 () {
   commands 
}

函数名 () {
   commands
}

实例:

demoFun(){
    echo "这是我的第一个 shell 函数!"
}
echo "-----函数开始执行-----"
demoFun
echo "-----函数执行完毕-----"

函数调用和传参

Shell函数的调用和执行命令是一样的,只需输入函数名即可调用函数,函数必须在调用之前定义。

函数的参数利用位置参数进行获取与使用:

  • 调用函数时,使用位置参数的形式为函数传递参数
  • 函数内的$1-${n}$*$@ 表示其接收的参数
  • 函数调用结束后位置参数 $1-${n}$*$@ 将被重置为调用函数之前的值
  • 在主程序和函数中,$0始终代表脚本名
funWithParam(){
    echo "第一个参数为 $1 !"
    echo "第二个参数为 $2 !"
    echo "第十个参数为 $10 !"
    echo "第十个参数为 ${10} !"
    echo "第十一个参数为 ${11} !"
    echo "参数总数有 $# 个!"
    echo "作为一个字符串输出所有参数 $* !"
}
funWithParam 1 2 3 4 5 6 7 8 9 34 73

函数内变量的作用域

  • 函数内使用 local 声明的变量是局部(Local)变量,局部变量的作用域是当前函数以及其调用的所有函数。
  • 函数内未使用 local 声明的变量是全局(Global)变量,即主程序和函数中的同名变量是一个变量(地址一致)。
  • 这一点与python不同,python在函数内默认的变量作用域是函数作用域,也就是说默认是局部变量,需要用globa在函数内将其声明为全局变量。

我觉得Shell的变量作用域比较容易混乱。

#!/bin/bash
## filename: function_max.sh
# User define Function (UDF)
usage () {
  echo "List the MAX of the positive integers in command line. "
  echo "Usage: `basename $0`   [  ... ]"
  exit
}
max () {
  [[ -z $1 || -z $2 ]] && usage    #发现挺多这样写的,因为Shell的与运算默认是短路与,相当于if的效果
  largest=0
  for i ; do  ((i>largest)) && largest=$i ; done
}
### Main script starts here ###
max "$@"
echo "The largest of the numbers is $largest."

#由于largest变量在函数max内没有使用local声明,所以它是全局的

source命令

  • 如果函数和调用它的主程序保存在同一个文件中,那么函数的定义必须出现在调用之前,这是所有脚本语言共同的特点,python也是这样的。
  • 如果函数和调用它的主程序保存在不同的文件中,保存函数的文件必须先使用 source 命令执行,之后才能调用其中的函数。
    • source命令类似于python中的import,引入对应的代码文件内容。

文件1:

#!/bin/bash
## filename: /root/bin/my_backup_functions.sh
### User define Function (UDF) ###
sql_bak  () { echo "Running mysqldump tool..."; }
sync_bak () { echo "Running rsync tool..."; }
git_bak  () { echo "Running gistore tool..."; }
tar_bak  () { echo "Running tar tool..."; }

文件2:

#!/bin/bash
## filename: all_in_one_backup_select.sourcefunc.sh
source /root/bin/my_backup_functions.sh    #在这里进行了引入
### Main script starts here ###
PS3="Please choose a backup tools : "
select s in  mysqldump rsync gistore tar quit ; do
  case $REPLY in
       1|[mM]ysqldump) sql_bak  ;;
       2|[rR]sync)     sync_bak ;;
       3|[gG]istore)   git_bak  ;;
       4|[tT]ar)       tar_bak  ;;
       5) exit     ;;
  esac
done

函数的结束和返回值

隐式结束:

  • 当函数的最后一条命令执行完毕,将自动结束
  • 函数的返回值就是最后一条命令的退出码
  • 其返回值被保存在系统变量$?

显式结束:

  • return [N]

    • return 将结束函数的执行
    • 可以使用 N 指定函数返回值
  • exit [N]

    • exit 将中断当前函数及当前Shell的执行
    • 可以使用 N 指定返回值
  • return只结束函数,而exit直接退出脚本

  • 使用 return 或 exit 只能返回整数值

    • 我们也可以使用echo命令输出我们想要返回的结果至标准输出,然后在调用函数时,利用$(func)或者`func`执行函数,并获取结果。这样既可以返回整数,也可以返回字符串。
    RES=$(functionName)
    echo $RES

   转载规则


《Shell脚本编程》 HillZhang 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
正则表达式笔记 正则表达式笔记
正则表达式笔记什么是正则表达式正则表达式,又称规则表达式。(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。
2020-05-05
下一篇 
Linux系统启动及常用命令 Linux系统启动及常用命令
Linux系统启动及常用命令Linux系统启动过程RHEL/CentOS启动过程总览 BIOS初始化 检测所有外围设备 按配置顺序寻找启动介质 从MBR寻找引导程序 启动加载器 GRUB(stage1) GRUB(stage1.5) G
2020-05-02
  目录