在Linux系统使用过程中,不可避免的要编写脚本,如进行大量重复操作,或需要根据条件自动执行某操作。本文介绍了bash脚本编写的相关内容,文末有一些示例。
严格来讲,这里的bash是工作在用户空间的一个程序而已,与其他程序不同的是,该程序负责与使用者交互,可以说是计算机与用户的沟通的媒介。
我们通过输入设备输入指令或数据,有它负责进行相应操作,或启动另一个进程处理。从这个意义来讲,他就像是一个“外壳”,普通用户与计算机的交互都是通过该程序。这样的程序我们将其称为Shell,Windows平台如桌面,cmd,powershell,Linux平台有bash,zsh,csh,KDE,Gnome等。
作为一款shell程序,bash的强大之处在于,其内部支持其特有的命令输入方式,如,可以将多个命令写在一行,中间使用分号分隔即可;再如,它可以进行条件判断等1。
这里要说明的其另一个重要特性,他可以将以文本文件的内容,按照其规则进行解释后执行,效果如用户键入的相同。该规则可以称为bash的语法,该文件可称为bash脚本。
由于其这种特性,它几乎可以被当做一门编程语言。
为了从头讲明,也便于同其他语言对比,这里提一下这个话题。关于软件编程,这里有基础介绍:https://blog.csdn.net/xiyangyang410/article/details/85043737#2__46
这里我们只讨论高级语言,我们知道,开发人员写的代码最初为文本文件,而其需要被“翻译”为计算机能识别的指令才能执行。此处可从代码的“翻译”方式对于编程语言做以简要说明:
一般的,我们将解释型语言的源代码文件叫做脚本,有时也将半解释型语言代码文件这么称呼,但是一般编译型语言的源代码文件不这么叫。
比如你可能听过bat脚本、vb脚本,甚至Python脚本,但是你听过C脚本吗?
几乎所有的编程语言,在代码编写过程中都会有一些约定俗成的规则,并且也有很多团队有自己内部的开发规范,这里对基本的规则予以介绍,以避免养成一些陋习。
bash可以说是最好学的编程语言了,因为他在工作时是直接调用的系统命令。而其他编程语言,我们将其本身的语法学完之后,很难直接使用它快速完成实际工作——我们还需要学一大堆的库。
bash编程,只要了解相关命令的用法,以及bash的语法即可。
变量(Variable) 相信大家并不陌生,其实质上就是一段命名的内存空间。而在bash中,同样支持变量的概念。
变量类型 | 说明 |
---|---|
环境变量 | 作用域为当前shell进程及其子进程 |
本地变量 | 作用域为整个bash进程 |
局部变量 | 作用域为当前代码段(通常指函数) |
位置变量 | $1,$2,…用于让脚本在脚本代码中调用通过命令行传递给它的参数 |
特殊变量 | 保存某些特殊数据 |
$0
: 脚本名称本身
$?
: 上一条命令的执行状态
$$
:脚本运行的当前进程ID
$!
:Shell最后运行的后台进程的PID
$#
: 参数数量
$*
: 所有参数的一个字符串
$@
:所有参数单独作为每个字符串
$1
、
$2
…:位置变量,对应第一、第二个参数
$1
$2
$3
)则"
$*
" 等价于 “
$1 $2 $3
"(传递了一个参数);而“
$@
" 等价于 “
$1
” “
$2
” “
$3
”(传递了三个参数)
bash中变量的命名规则同其他语言类似,变量名只能包含数字、字母和下划线,而且不能以数字开头,关键字不能用作变量名,另外,变量赋值时不能使用$。
bash为弱类型语言,故在声明变量时不必指定变量类型(但这不代表这些变量没有类型),可直接使用如下形式声明变量:
[set] VARNAME=VALUE
set可省略
使用不带参数的set
命令可查看系统已定义的所有变量
readonly VARNAME
或
declare -r VARNAME
在声明时指定readonly
,或使用declare -r
,可声明只读变量(常量),由于其在声明后不可更改内容,需要在声明时进行初始化。
export VARNAME=VALUE
或
VARNAME=VALUE; export VARNAME
或
VARNAME=VALUE; declare -x VARNAME
或
declare -x VARNAME=VALUE
环境变量对当前shell及其子shell都有效
local VARNAME=VALUE
仅对局部代码生效
所谓数组,是有序的元素序列。若将有限个类型相同的变量的集合命名,那么这个名称为数组名。它使用连续的内存空间,可以使用索引来获取相关元素。
在bash-4及以后的版本中,支持关联数组(Associative Array) 的概念,即可自定义索引,而不是仅仅以0,1,2……为其索引的索引数组(Indexed Array),类似于Python中字典(dict) 的概念。bash中声明数组的方式为:
declare -a ARRAY_NAME
# 声明一个索引数组
declare -A ARRAY_NAME
# 声明一个关联数组
在声明一个数组后,可对其进行初始化,使用ARRAY_NAME=("VAR1" "VAR2" "VAR3")
的形式,各元素之间使用空白分隔,bash将按给出的次序给予每一个元素索引(0,1,2……),若数组声明为关联数组,可使用ArrayName=([INDEX]='VAR' [INDEX]='VAR')
的形式,此处的INDEX
并非必须是数字。
可以使用如下方式对数组的某个元素进行赋值:
ARRAY_NAME[INDEX]=VAR
注意,此处没有$
,关于bash中数组的其他用法,下文将做介绍
关于数组的引用,可使用如下格式:
${ARRAY_NAME[INDEX]}
注意,此处的{}
(花括号)不能省略,而数组名将引用数组首元素
变量的赋值可使用VAR_NAME=VALUE
的形式,此处VALUE
可以为字面值,也可以引用变量,此处介绍以下特殊引用方式
${var:-VALUE}
var
变量为空,或未设置,则返回
VALUE
,否则返回
var
变量的值
${var:=VALUE}
var
变量为空,或未设置,则返回
VALUE
,并将
VALUE
赋值给var变量,否则返回
var
变量的值
${var:+VALUE}
var
变量不空,或未设置,则返回
VALUE
,否则返回空,与
${var:-VALUE}
相反
${var:?ERROR_INFO}
var
为空,或未设置,那么返回
ERROR_INFO
为错误提示,否则返回
var
的值
变量将在当前shell的声明周期结束时被自动撤销,而欲手动撤销变量,可使用unset VAR_NAME
,此时,若VAR_NAME
为变量或数组,则可直接撤销,若为数组的某元素,则可仅撤销该元素
“与”表示“并且”之意,即两种事件都发生,使用 && 表示,其运算结果为
即二者都为真(True),结果才为真(False),否则为假
“或”表示二者任一即可,使用 || 表示,其运算结果为
即二者都为假,结果才为假,否则为真
“非”表示“不”,取相反结果之意,这是一个单目运算符,使用 ! 其运算结果为
由于“与”和“或”运算时自左而右的,加之其运算特性,我们可以得出如下结论
如此则不必再去计算其二个运算数(或表达式)的结果,可直接得出最终结果,这种运算方式成为短路运算,也叫作逻辑短路。
通过这种特性,在bash中可是实现类似if条件判断的处理。
我们知道,程序在执行完后会返回一个执行状态,用于标识程序执行成功与否,使用0标识执行成功(即“真”),非0则标识执行失败(即“假”)
可通过判断该状态返回值,对不同的结果(成功或失败)处以不同的操作,如
查看user1是否存在,若存在,则输出其信息,否则创建之:
id user1 2> /dev/null || useradd user1
可以组合多个这样的命令以实现更复杂的控制
user1
存在,就系显示用户已存在,否则就添加此用户id user1 && echo "user1 exists." || useradd user1
user1
不存在,就添加此用户,否则就显示用户已存在! id user1 && useradd user1 || echo "user1 exists."
# 或
id user1 || useradd user1 && echo "user1 exists."
user1
不存在,就添加并且给密码,否则显示其已存在! id user1 && useradd user1 && echo "user1" | passwd --stdin user1 || echo "user1 exists."
该定律应用很广泛,此处也将其列出
A ⋂ B ‾ ≡ A ‾ ⋃ B ‾ \overline{A \bigcap B} \equiv \overline{A} \bigcup \overline{B} A⋂B≡A⋃B
A ⋃ B ‾ ≡ A ‾ ⋂ B ‾ \overline{A \bigcup B} \equiv \overline{A} \bigcap \overline{B} A⋃B≡A⋂B
对于以上的根据不同条件进行不同操作的方式,可使用一个更易读的语法:if语句
表示,如果某条件满足,则执行特性操作,否则不行该操作,其使用格式为
if BOOL_EXP; then
STATEMENT
fi
BOOL_EXP
为一个布尔表达式,可以是一个命令,此时将取命令的执行状态返回值作为布尔值参与表达式计算
若BOOL_EXP
值为真,则执行代码块中内容(STATEMENT
),否则不执行
then
关键字也可以另起一行,另外对于bash而言,缩进不是必要的,但为了代码易读,建议对代码块中的语句使用缩进。而有的编程语言这一要求是必须的,如Python
if BOOL_EXP; then
STATEMENT1
else
STATEMENT2
fi
表示若BOOL_EXP
为真,执行STATEMENT1
,否则执行STATEMENT2
if BOOL_EXP1; then
STATEMENT1
elif BOOL_EXP2; then
STATEMENT2
elif BOOL_EXP3; then
STATEMENT3
...
else
STATEMENT4
fi
表示若BOOL_EXP1
为真,执行STATEMENT1
,若BOOL_EXP2
位真,执行STATEMENT2…
elif
段可以出现多次,最后的else
也是可选的,若有,表示若所有条件都不满足,执行STATEMENT4
故以上的代码使用if语句的实现为
if ! id user1 2> /dev/null; then
useradd user1
fi
有时在处理有多种条件分支的情况时,若使用if语句,我们不得不编写冗长的elif段,此时,可以使用case语句来处理该情形,case语句的基本使用格式为
case EXPRESSION in
PATTERN1)
STATEMENTS
;;
PATTERN2)
STATEMENTS
;;
...
esac
PATTERN为过滤的模式,其支持的方式为
如
#!/bin/bash
case $1 in
'start')
echo "start server...";;
'stop')
echo "stop server...";;
'restart')
echo "restarting server...";;
'status')
echo "running...";;
*)
echo "`basename $0` {start|stop|restart|ststus}";;
esac
由于bash是弱类型的语言,其声明的变量默认为字符,运算法则默认也是按照字符运算进行的,若要使其进程算术运算,可使用如下几种方式:
bash中常用的运算符如下
运算符 | 描述 |
---|---|
+ | 加 |
- | 减 |
* | 乘 |
/ | 除 |
** | 乘方 |
% | 取模 |
若欲计算变量A与变量B的和,以上表示的实现为:
#let VAR = ARITH_EXPR
let C = $A + $B
#VAR = $[ARITH_EXPR]
C = $[$A + $B]
#VAR = $((ARITH_EXPR))
C = $(($A + $B))
#VAR = $(ARITH_EXPR)
C = $(expr $A + $B)
C = `expr $A + $B`
如,计算user1,user2,user3用户的UID之和
#!/bin/bash
uid1=`id -u user1`
uid2=`id -u user2`
uid3=`id -u user3`
uid_sum=$[$uid1 + $uid2 + $uid3]
echo "The sum of uid if $uid_sum."
bash中的增强型赋值类似于C语言,可较高效的实现引用并赋值的操作,以变量A
与B
为例:
增强赋值 | 等效操作 |
---|---|
A+=B |
A=$A+$B |
A*=B |
A=$A*$B |
A-=B |
A=$A-$B |
A/+B |
A=$A/$B |
A%=B |
A=$A%$B |
同其他编程不同,所谓的bash编程,调用的是系统上已有的程序命令,通过bash内置的变量、流程控制等机制实现的。
代码的源文件问文本格式,而内核只能执行二进制格式文件,故直接执行该文件内核是无法理解的,而bash是解释执行的,需要为其程序文件执行一个解释器。
在程序文件内容的最开始处使用以 #! 开头,后跟解释器程序路径的字符串来通知内核,使用该程序对文件进行解释执行,而不是直接执行,这个字符串被称为 Shebang 。
#!/PATH/TO/SHELL_INTERPRETER
事实上在Linux系统中,解释型语言普遍都是采用以上方式,如Python、Java等代码,此处以bash脚本为例,可指定为
#!/bin/bash
上文程序的第一行就是Shebang。
若不指定,则脚本无法直接执行,但可以明确指定解释器,将该脚本文件当做参数让解释器执行,如上面的文件为:
uid1=`id -u user1`
uid2=`id -u user2`
uid3=`id -u user3`
uid_sum=$[$uid1 + $uid2 + $uid3]
echo "The sum of uid is $uid_sum."
可使用bash SCRIPT_NAME
执行之
另外,默认新建的文件是没有执行权限的,若要直接执行,则需要为其赋予执行权限2:
chmod +x /PATH/TO/SCRPTE
测试脚本中是否有语法错误:
bash -n SCRIPT
调试脚本(单步执行)
bash -x SCRIPT
如,脚本文件test.sh
内容为
#!/bin/bash
declare stra=root
[[ $stra ~= oot ]] && echo Yes || echo No
进行语法测试:
[root@localhost ~]# bash -n scripts/test.sh
scripts/test.sh: line 4: conditional binary operator expected
scripts/test.sh: line 4: syntax error near `~='
scripts/test.sh: line 4: `[[ $stra ~= oot ]] && echo Yes || echo No'
将文件中的~=
修改为=~
再次测试则没有报错,表示无语法错误。
单步执行1-10中的偶数和,test.sh
文件内容为:
#!/bin/bash
declare -i sum=0
for((i=1;i<=10;i++));do
if [ $[$i % 2] -eq 0 ];then
let sum+=$i
fi
done
echo $sum
[root@localhost ~]# bash -x scripts/test.sh
+ declare -i sum=0
+ (( i=1 ))
+ (( i<=10 ))
+ '[' 1 -eq 0 ']'
+ (( i++ ))
+ (( i<=10 ))
+ '[' 0 -eq 0 ']'
+ let sum+=2
+ (( i++ ))
+ (( i<=10 ))
+ '[' 1 -eq 0 ']'
+ (( i++ ))
+ (( i<=10 ))
+ '[' 0 -eq 0 ']'
+ let sum+=4
+ (( i++ ))
+ (( i<=10 ))
+ '[' 1 -eq 0 ']'
+ (( i++ ))
+ (( i<=10 ))
+ '[' 0 -eq 0 ']'
+ let sum+=6
+ (( i++ ))
+ (( i<=10 ))
+ '[' 1 -eq 0 ']'
+ (( i++ ))
+ (( i<=10 ))
+ '[' 0 -eq 0 ']'
+ let sum+=8
+ (( i++ ))
+ (( i<=10 ))
+ '[' 1 -eq 0 ']'
+ (( i++ ))
+ (( i<=10 ))
+ '[' 0 -eq 0 ']'
+ let sum+=10
+ (( i++ ))
+ (( i<=10 ))
+ echo 30
30
有时需要重复执行一些特定的操作,比如,对某文件的每一行内容做特定操作,循环执行某计算知道某特定条件满足。此时可使用循环控制,bash中提供三种基本的循环控制语句。
while的基本使用格式为:
while CONDITION; do
STATEMENTS
done
CONDITION为进入循环的条件,是布尔表达式,其结果为真时进入循环。
如:计算100以内整数的和
#!/bin/bash
declare -i I=1
declare -i SUM=0
while [ $I -le 100 ]; do
let SUM+=$I
let I++
done
echo $SUM
until的作用于while相似,不同的是当条件为假时进入循环,其使用格式为
until CONDITION; do
STATEMENS
done
如使用until计算100以内整数的和
#!/bin/bash
declare -i I=1
declare -i SUM=0
until [ $I -gt 100 ]; do
let SUM+=$I
let I++
done
echo $SUM
for的基本使用格式为:
for VAR in LIST; do
COMMANDS;
done
LIST是提供了一系列用于迭代的值的列表,在for语句执行时,LIST中的每一个元素将在每次循环时赋给变量VAR,在循环体内部可饮用VAR进行相应操作。
列表由以下几种常见的生成方式
for i in one two three; do
echo $i
done
执行结果:
one
two
three
for i in {1..10}; do
# 将生成1 2 3 4 5 6 7 8 9 10
某些命令的直接结果就是以列表形式给出的,可以使用命令替换使用该结果,如
for i in `ls /`; do
echo $i
done
Tips:使用seq命令生成
seq命令可生成一个数字列表,其使用格式为
seq [START [STEP]] END
如
[root@localhost scripts]# seq 3
1
2
3
[root@localhost scripts]# seq 3 6
3
4
5
6
[root@localhost scripts]# seq 1 2 10
1
3
5
7
9
bash的Glob特性所匹配到的各个结果也是以列表的形式存在,如
for file in /etc/*.conf
...
done
代码将遍历/etc目录中以.conf
结尾的文件
此处需要指出的是两个特殊变量$@
与$*
,他们都存储了传递给脚本的参数,其区别只有在双引号中体现出来。假设在脚本运行时写了三个参数(分别存储在$1 $2 $3
)则$*
等价于 “$1 $2 $3
”(传递了一个参数);而$@
等价于 “$1
” “$2
” “$3
”(传递了三个参数)
语法格式
for((INIT_EXPR;EXIT_COND;ITER_EXPR)); do
COMMANDS;
done
这种for语句与C语言的语法类似,INIT_EXPR为变量的初始化表达式,EXIT_COND为循环退出的条件测试,结果为假时退出循环,ITER_EXPR为每次循环的迭代操作,常用作修正循环变量,如,计算[1,100]的整数和的实现为:
declare -i sum=0
for((i=1;i<=100;i++)); do
let sum+=$i
done
echo "$sum"
需要说明的是,C风格的写法中,变量的引用方式、布尔运算符等都都与bash风格有所不同:
break用于跳出当前循环语句,其后加一个数字可跳出多层循环
continue用于结束本轮循环
如,计算1-100的偶数和:
#!/bin/bash
declare -i sum=0
declare -i idx=1
while true; do
let idx++
if [ $[$idx%2] -ne 0 ]; then
continue
fi
if [ $idx -gt 100 ]; then
break
fi
let sum+=$idx
done
echo $sum
在使用条件判断时,经常需要通过一系列比较与测试,根据其结果做出相应的操作。此处再次强调,bash中0表示真,非0则为假
可使用一下三种方式
[ EXPRESSION ]
# 中括号与EXPRESSION之间必须有空格,该中括号是命令
[[ EXPRESSION ]]
# 内侧的中括号与EXPRESSION之间必须有空格,这两个中括号是关键字
test EXPRESSION
EXPRESSION
为测试表达式,常用的有以下三类
[ ]
是bash的内部命令,[
和test
是等同的。如果我们不用绝对路径指明,通常我们用的都是bash自带的命令。if/test
结构中的左中括号是调用test
的命令标识,右中括号]
是关闭条件判断的。这个命令把它的参数作为比较表达式或者作为文件测试,并且根据比较的结果来返回一个退出状态码。if/test
结构中并不是必须右中括号,但是新版的Bash中要求必须这样
test
和[ ]
中可用的比较运算符只有==
和!=
,两者都是用于字符串比较的,不可用于整数比较(下文将做介绍),整数比较只能使用-eq
,-gt
这种形式。无论是字符串比较还是整数比较都不支持大于号小于号。如果实在想用,对于字符串比较可以使用转义形式,如果比较"ab"和"bc":[ ab \< bc ]
,结果为真,也就是返回状态为0。[ ]
中的逻辑与和逻辑或使用-a
和-o
表示
[[
是bash程序语言的关键字。并不是一个命令,[[ ]]
结构比[ ]
结构更加通用。在[[
和]]
之间所有的字符都不会发生文件名扩展或者单词分割,但是会发生参数扩展和命令替换
支持字符串的模式匹配,使用=~
操作符时甚至支持shell的正则表达式。字符串比较时可以把右边的作为一个模式,而不仅仅是一个字符串,比如[[ hello =~ hell? ]]
,结果为真。[[ ]]
中匹配字符串或通配符,不需要引号
使用[[ ... ]]
条件判断结构,而不是[ ... ]
,能够防止脚本中的许多逻辑错误。比如,&&
、||
、<
和>
操作符能够正常存在于[[ ]]
条件判断结构中,但是如果出现在[ ]
结构中的话,会报错
bash把双中括号中的表达式看作一个单独的元素,并返回一个退出状态码
数值比较使用如下形式
NUM1 OPRAND NUM2
OPRAND
为一个双目比较的操作操作符,有如下为常用的操作符
OPRAND | 说明 |
---|---|
-eq |
即equal,测试两个数是否相等,若是则返回真 |
-ne |
即not equal,测试两个数是否不等,若是则返回真 |
-gt |
即greater than,测试第一个操作数是否大于第二个操作数,若是返回真 |
-ge |
即greater equal,测试第一个操作数是否大于或等于第二个操作数,若是返回真 |
-lt |
即less than,测试第一个操作数是否小于第二个操作数,若是返回真 |
-le |
即less equal,测试第一个操作数是否小于或等于第二个操作数,若是返回真 |
bash中字符测试域数值测试类似,可测试字符或字符串,只是OPRAND
的形式不同
OPRAND | 说明 |
---|---|
== |
测试两个字符串是否相等,若是则返回真 |
!= |
测试两个字符串是否不等,若是则返回真 |
> |
测试第一个操作数是否大于第二个操作数,若是返回真 |
>= |
测试第一个操作数是否大于或等于第二个操作数,若是返回真 |
< |
测试第一个操作数是否小于第二个操作数,若是返回真 |
<= |
测试第一个操作数是否小于或等于第二个操作数,若是返回真 |
=~ |
模式匹配,若左侧的字符串可以被右侧的模式匹配,则返回真 |
字符串大小比较 将逐位比较两个操作数中每一个字符的大小,若相等则比较下一位,直至比较结果不等,以该结果作为整个表达式的结果。
关于=~
模式匹配测试,注意一下几点
[[ ]]
中使用如,测试stringA中是否包含oot:
#!/bin/bash
declare stringA=root
echo $stringA
[[ $stringA =~ oot ]] && echo "Matched." || echo "Not Match."
stringA=Linux
echo $stringA
[[ $stringA =~ oot ]] && echo "Matched." || echo "Not Match."
输出结果:
root
Matched.
Linux
Not Match.
此外,字符串测试还有单目测试符:
-n "STRING" 测试指定字符串是否不空,不空为真
-z "STRING" 测试指定字符串是否为空,空则为真
Tips
[[ ]]
文件测试使用测试符对文件内容或某属性进行测试
操作符 | 描述 |
---|---|
-e FILE | 测试文件是否存在 |
-a FILE | 测试文件是否存在 |
操作符 | 描述 |
---|---|
-s FILE | 测试文件是否不空 |
操作符 | 描述 |
---|---|
-f FILE | 测试文件是否为普通文件 |
-d FILE | 测试文件是否为路径(即目录) |
-b FILE | 测试文件是否为块设备文件 |
-c FILE | 测试文件是否为字符设备文件 |
-h FILE | 测试文件是否为符号链接文件,同-L |
-L FILE | 测试文件是否为符号链接文件,同-h |
-p FILE | 测试文件是否为命名管道文件 |
-S FILE | 测试文件是否为套接字文件 |
若文件不存在,则测试结果直接为假
操作符 | 描述 |
---|---|
-r FILE | 测试当前用户对指定文件是否有读权限 |
-w FILE | 测试当前用户对指定文件是否有写权限 |
-x FILE | 测试当前用户对指定文件是否有执行权限 |
若文件不存在,则测试结果直接为假
操作符 | 描述 |
---|---|
-g FILE | 测试文件是否设置了SGID |
-u FILE | 测试文件是否设置了SUID |
-k FILE | 测试文件是否设置了sticky |
操作符 | 描述 |
---|---|
-t FD | FD表示文件描述符,是否已经打开且与某终端相关 |
操作符 | 描述 |
---|---|
-N FILE | 文件自上一次被读取之后是否被修改过 |
操作符 | 描述 |
---|---|
-O FILE | 当前有效用户是否为文件属主 |
-G FILE | 当前有效用户是否为文件属组 |
操作符 | 描述 |
---|---|
FILE1-nt FILE2 | 若FILE1比FILE2更新,则为真(若FILE1存在,FILE2不存在,也为真) |
FILE1 -ot FILE2 | 若FILE1比FILE2更老,则为真 |
FILE1 -ef FILE2 | 若FILE1与FILE2引用了相同的设备以及inode,则为真 |
[ -e /etc/inittab ]
[ -x /etc/rc.d/rc.sysinit ]
#!/bin/bash
FILE=/etc/rc.d/rc.sysinit
if [ ! -e $FILE ]; then
echo "No such file."
exit6
fi
if [ -f $FILE ]; then
echo "Common file."
elif [ -d $FILE ];then
echo "Directory."
else
echo "Unknown."
fi
#!/bin/bash
targetDir='/tmp/logs'
[ -e $targetDir ] || mkdir $targetDir
for fileName in /var/log/*; do
if [ -d $fileName ]; then
copyCommand='cp -r'
elif [ -f $fileName ]; then
copyCommand='cp'
elif [ -h $fileName ]; then
copyCommand='cp -d'
else
copyCommand='cp -a'
fi
$copyCommand $fileName $targetDir
done
我们在执行命令时可以在命令后附加一个或多个参数,在脚本中也支持传递参数,向脚本传递参数的方式与使用命令相同,即SCRIPT ARG1 ARG2 ...
而传递的参数在脚本中可以向变量一样使用,这些参数存储在特定变量中。
向脚本传递的参数将被bash依次传递给$1
,$2
,$3
……,如./test.sh root /etc/fstab linux
中
$1
为root,$2
为/etc/fstab,$3
为linux
此处再次列出相关的特殊变量3
特殊变量 | 描述 |
---|---|
$? |
上一条命令的退出状态码 |
$# |
参数的个数 |
$* |
参数列表,会合并各个参数 |
$@ |
参数列表,不会合并参数 |
$0 |
执行的命令或脚本名 |
有时,我们事先并不知道用户会传递多少个参数给脚本,而又需要处理各个参数,如计算给出的所有数字的和。此时可以使用shift命令来切换各参数,即若有多个参数,shift后,当前参数剔除,先一个参数变为当前参数,以此类推,使用格式为:
shift [N]
#N为数字,可省略,默认是1
如,脚本代码内容为
#!/bin/bash
echo $1
shift
echo $1
shift
echo $1
若给出了3个参数,则脚本将依次显示之
默认情况下,当脚本的最后一条命令执行完成后,脚本将退出,我们也可以使用exit
来显式控制脚本退出,使用方式为
exit RETURN_CODE
exit
后指定一个退出状态码,即$?
所查看的值,再次说明,0表示执行成功,而执行失败的值没有具体规定,但是一般将使用如下习惯:
代码 | 描述 |
---|---|
0 | 命令成功完成 |
1 | 通常的未知错误 |
2 | 误用shell命令 |
126 | 命令无法执行 |
127 | 没有找到命令 |
128 | 无效的退出命令 |
128+x | 使用Linux信号x的致命错误 |
130 | 使用Ctrl+C终止命令 |
255 | 规范以外的退出状态 |
#!/bin/bash
USERNAME=$1
if ! grep "^$USERNAME\>" /etc/passwd &> /dev/null; then
echo "No such user: $USERNAME."
exit 1
fi
USERID=`grep "^$USERNAME\>" /etc/passwd | cut -d: -f3`
GROUPID=`grep "^$USERNAME\>" /etc/passwd | cut -d: -f4`
if [ $USERID -eq $GROUPID ]; then
echo "Good guy."
else
echo "Bad guy."
fi
关于数组的基础用法(声明,引用,赋值),前文已有说明, 此处从其他角度进行进一步介绍。
使用数组名将引用数组的第一个元素,要引用其所有元素,可使用${ARRAY_NAME[*]}
的形式
${#ARRAY_NAME[INDEX]}
,同理,${#ARRAY_NAME}
将获取数组中第一个元素的长度
${#ARRAY_NAME[*]}
与${#ARRAY_NAME[@]}
可获取数组中所有有效元素的个数
因此,可以使用如下方式向数组中追加元素:
ARRAY_NAME[$#{ARRAY_NAME[*]}]=VAR
数组切片的使用语法为
${ARRAY_NAME[@]:OFFSET:NUMBER}
# OFFSET:偏移量
# NUMBER:获取的元素个数
如
[root@localhost ~]# files=(/etc/[Pp]*)
[root@localhost ~]# echo $files
/etc/pam.d
[root@localhost ~]# echo ${files[*]}
/etc/pam.d /etc/passwd /etc/passwd- /etc/pbm2ppa.conf /etc/php.d /etc/php.ini /etc/pinforc /etc/pkcs11 /etc/pki /etc/plymouth /etc/pm /etc/pnm2ppa.conf /etc/polkit-1 /etc/popt.d /etc/postfix /etc/ppp /etc/prelink.conf.d /etc/printcap /etc/profile /etc/profile.d /etc/protocols /etc/pulse /etc/python
[root@localhost ~]# echo ${files[*]:2:4}
/etc/passwd- /etc/pbm2ppa.conf /etc/php.d /etc/php.ini
字符串切片的使用格式为${VAR:OFFSET:NUMBER}
,OFFSET
默认为0,NUMBER
默认为0,如
[root@localhost ~]# name=Jerry
[root@localhost ~]# echo ${name:2:2}
rr
[root@localhost ~]# echo ${name::1}
J
可以使用负数,实现从右向左截取:
[root@localhost ~]# name=Jerry
[root@localhost ~]# echo ${name: -1}
y
[root@localhost ~]# echo ${name: -4}
erry
注意,冒号后有空格
${VAR#*word}
word
为指定的分隔符VAR
字符串开头至第一次出现word
之间的所有字符,如[root@localhost scripts]# mypath="/etc/init.d/functions"
[root@localhost scripts]# echo ${mypath#*e}
tc/init.d/functions
${VAR##*word}
word
为指定的分隔符VAR
字符串开头至最后一次出现word
之间的所有字符,如[root@localhost scripts]# mypath="/etc/init.d/functions"
[root@localhost scripts]# echo ${mypath##*/}
functions
${VAR%word*}
word
为指定的分隔符VAR
字符串末尾至第一次出现word
(包括word
)之间的所有字符,如[root@localhost scripts]# mypath="/etc/init.d/functions"
[root@localhost ~]# echo ${mypath%/*}
/etc/init.d
${VAR%%word*}
word
为指定的分隔符VAR
字符串末尾至最后一次出现word
(包括word
)之间的所有字符,如[root@localhost scripts]# mypath="/etc/init.d/functions"
[root@localhost ~]# echo ${mypath%%/*}
[root@localhost scripts]# mypath="etc/init.d/functions"
[root@localhost ~]# echo ${mypath%%/*}
etc
${var/PATTERN/SUBSTI}
PATTERN
所匹配到的字符串,替换为
SUBSTI
所表示的字符串
${var/PATTERN}
:以
PATTERN
为模式查找
var
中第一次匹配到的内容,将其删除
${var//PATTERN/SUBSTI}
PATTERN
所匹配到的字符串,全部替换为SUBSTI所表示的字符串
${var//PATTERN}
:以PATTERN为模式查找var中所有匹配到的内容,将其删除
${var/#PATTERN/SUBSTI}
var
所表示的字符串中,行首被
PATTERN
所匹配到的字符串,替换为SUBSTI所表示的字符串
${var/#PATTERN}
:以
PATTERN
为模式查找var中行首被匹配到的内容,将其删除
${var/%PATTERN/SUBSTI}
PATTERN
所匹配到的字符串,替换为
SUBSTI
所表示的字符串
${car/%PATTERN}
:以
PATTERN
为模式查找
var
中行尾被匹配到的内容,将其删除
${var^^}
:将var
中的所有小写字符转换为大写${var,,}
:将var
中的所有大写字符转换为小写如
[root@localhost ~]# str=abcABC
[root@localhost ~]# echo ${str^^}
ABCABC
[root@localhost ~]# echo ${str,,}
abcabc
[root@localhost ~]# echo $str
abcABC
在脚本中若有大量重复且复杂的操作,开发者若也一遍一遍地写大量重复的代码,这显示时及其低效的,此时可以使用函数(Function) 来 处理,其直接作用之一就是代码重用
函数的定义使用关键字function
,可使用如下两种方式使用
function FUNC_NAME {
COMMAND
}
# 此处的小括号应紧跟在函数名之后
FUNC_NAME() {
COMMAND
}
函数的调用直接给出函数名即可,若函数支持参数,直接在函数名后给出各参数,使用空格分隔即可。
如,写一个脚本,完成如下功能
#!/bin/bash
ShowMenu() {
cat << EOF
disk) show disk info
mem) show memory info
cpu) show cpu info
EOF
}
main() {
ShowMenu
read -p "Please choose an option: " option
case $option in
disk)
df -h
;;
mem)
free -m
;;
cpu)
cat /proc/cpuinfo
;;
*)
echo "Wrong option"
esac
}
main
事实上,函数也可看做是更小型的shell脚本,故其返回值与脚本类似,此处的说明相信很好理解
同脚本相同,函数中最后一条指令的执行状态返回值即为函数的执行状态返回值,不同的是,在脚本中可使用exit
退出脚本并执行执行转台码,而函数中使用return
函数的运行结果的引用依然类似于脚本(或命令),函数中的输出语句(如echo
、printf
等)、函数中的命令执行结果都可作为函数的运行结果
执行状态返回值保存在变量$?
中,而执行结果的引用要使用命令引用4
若在函数中使用了在主程序中声明的变量,重新赋值会修改主程序中的变量。如果不期望函数与主程序中的变量冲突,函数中使用变量都用local
修饰,即使用局部变量
在函数中使用了在主程序中没有声明的变量,在函数执行结束后即被撤销,无论是否使用了local
修饰符
但若在函数中没有使用declare
,直接声明,如A=10
,则变量为全局的
如上所述,可以向给脚本传递参数一样给函数传参,需要注意的是函数的$1
与脚本的$1
不同,如
调用脚本时使用SCRIPT_FILE ARG1 ARG2 ARG3
,在该脚本中有一行内容FUNC1 root $1 $2
,则,此时,对于脚本而言,$1
、$2
、$3
分别为ARG1
、ARG2
、ARG3
,而对于函数而言,$1
、$2
、$3
分别为root
、$ARG1
、$ARG2
可以把脚本的全部位置参数,统统传递给脚本中某函数使用:$*
#!/bin/bash
CnetPing(){
for i in {0..255}; do
ping -c 1 -w 1 $1.$i
done
}
BnetPing(){
for j in {0..255}; do
CnetPing $1.$j
done
}
AnetPing(){
for m in {0.255}; do
BnetPing $1.$m
done
}
netType=`echo $1 | cut -d'.' -f1`
if [[ $netType -gt 0 -a $netType -le 126 ]]; then
AnetPing $1
elif [[ $netType -ge 128 -a $netType -le 191 ]]; then
BnetPing $1
elif [[ $netType -ge 192 -a $netType -le 223 ]]; then
CentPing $1
else
echo "Wrong"
exit 3
fi
#!/bin/bash
showMenu() {
while true; do
if [[ $# -lt 5 ]]; then
echo "mkscript.sh [-D|--description script description] [-A|--author script author] /path/to/somefile"
exit 6
else
return 0
fi
done
}
option() {
case $1 in
-D|--description)
authName=$4
desInfo=$2
;;
-A|--author)
authName=$2
desInfo=$4
;;
*)
showMenu
;;
esac
}
creatFile() {
if [[ -f $1 ]]; then
if [ `head -1 $1` == "#!/bin/bash" ]; then
vim + $1
else
echo "Wrong"
exit 5
fi
else
touch $1 && echo -e "#!/bin/bash\n# Description: $desInfo\n# Author: $authName\n#\n" > $1
vim + $1
fi
}
reedit() {
read -p "Press y to edit,n to exit: " choose
if [ "$choose" == "y" ]; then
vim $1
fi
if [ "$choose" == "n" ]; then
exit 0
fi
}
syntax() {
bash -n $1 &> /dev/null && chmod +x $1 || reedit $1
}
showMenu $*
option $*
creatFile $5
syntax $5
在脚本中可以直接捕获信号,以避免内其中的命令捕获而影响执行5
首先,此处将常用信号再次列出
信号 | 值 | 描述 |
---|---|---|
1 | SIGHUP | 挂起进程,让一个进程不必重启即可重读其配置文件 |
2 | SIGINT | 中断进程,Ctrl+C |
9 | SIGKILL | 杀死进程 |
15 | SIGTERM | 终止一个进程 |
18 | SIGCONT | 调回后台进程 |
19 | SIGSTOP | 停止一个进程,即送往后台,Ctrl+Z |
在脚本中可以捕获信号,但是一般9与15信号不能被捕捉,使用trap
可实现信号捕捉,其使用格式为
trap 'COMMAND' SIGNALS
捕捉到信号SIGNALS
后,执行COMMAND
同kill -l
,也可以使用trap -l
查看信号列表,一般,脚本中经常需要被捕获的信号为SIGHUP与SIGINT
#!/bin/bash
#
trap 'echo "quit"; exit 5' INT
for i in {1..254}; do
if ping -w 1 -c 1 172.16.254.$i &> /dev/null; then
echo "172.16.254.$i is up."
else
echo "172.16.254.$i is down."
fi
done
#!/bin/bash
declare -a hosttmpfiles
trap 'mytrap' INT
mytrap() {
echo "Quit"
rm -f ${hosttmpfiles[@]}
exit 1
}
for i in {1..50}; do
tempfile=`mktemp /tmp/ping.XXXX`
if ping -W 1 -c 1 192.168.18.$i &> /dev/null; then
echo "192.168.18.$i is up." | tee $tempfile
else
echo "192.168.18.$i is down." | tee $tempfile
fi
hosttmpfiles[${#hosttmpfiles[*]}]=$tempfile
done
rm -f ${hosttmpfiles[@]}
#!/bin/bash
if ! id $1 &>/dev/null; then
echo "No such user."
ecit 10
fi
if [ $1 == `id -n -g $1` ]; then
echo "Yiyang"
else
echo "Bu Yiyang"
fi
#!/bin/bash
if [ $1 = 'q' ];then
echo "Quiting..."
exit 1
elif [ $1 = 'Q' ];then
echo "Quiting..."
exit 2
elif [ $1 = 'quit' ];then
echo "Quiting..."
exit 3
elif [ $1 = 'Quit' ];then
echo "Quiting..."
exit 4
else
echo $1
fi
#!/bin/bash
for userName in `cut -d: -f1 /etc/passwd`; do
if [[ `grep "^$userName\>" /etc/passwd | cut -d: -f7` =~ sh$ ]]; then
echo "login user: $userName."
else
echo "nologin user: $userName."
fi
done
#!/bin/bash
if [ $# -lt 1 ]; then
echo "Usage:adminusers ARG"
exit 7
fi
if [ $1 == '--add' ]; then
for I in {1..10}; do
if id user$I &> /dev/null; then
echo "user$I exists."
else
useradd user$I
echo user$I | passwd --stdin user$I &> /dev/null
echo "Add user$I finished."
fi
done
elif [ $1 == '--del' ]; then
for I in {1..10}; do
if id user$I &> /dev/null; then
userdel -r user$I
echo "Delete user$I finished."
else
echo "No user$I."
fi
done
else
echo "Unknown ARG"
exit 8
#!/bin/bash
if [ $1 == '--add' ]; then
for I in `echo $2 | sed 's/,/ /g'`; do
if id $I &> /dev/null; then
echo "$I exists."
else
useradd $I
echo $I | passwd --stdin $I &> /dev/null
echo "Add $I finished."
fi
done
elif [ $1 == '--del' ]; then
for I in `echo $2 | sed 's/,/ /g'`; do
if id $I &> /dev/null; then
userdel -r $I
echo "Delete $I finished."
else
echo "$I NOT exist."
fi
done
elif [ $1 == '--help' ]; then
echo "Usage:adminuser2.sh --add USER1,USER2,... | --del USER1,USER2,... | --help"
else
echo "Unknown options."
fi
#!/bin/bash
[ $# -lt 2 ] && echo "Too less argements,quit" && exit 3
if [[ "$1" == "-u" ]]; then
userName="$2"
shift 2
fi
if [ $# -ge 2 ] && [ "$1" == "-v" ]; then
verFlag=$2
fi
verFlag=${verFlag:-0}
if [ -n $verFlag ]; then
if ! [[ $verFlag =~ [012] ]]; then
echo "Wrong parameter."
echo "Usage: `basename $0` -u UserName -v {1|2}"
exit 4
fi
fi
if [ $verFlag -eq 1 ]; then
grep "^$userName" /etc/passwd | cut -d: -f1,3,4,6
elif [ $verFlag -eq 2 ]; then
grep "^$userName" /etc/passwd | cut -d: -f1,3,4,6,7
else
grep "^$userName" /etc/passwd | cut -d: -f1,3,4
fi
#!/bin/bash
[ -d /backup ] || mkdir /backup
cat << EOF
Plz choose a compress tool:
xz) xz compress
gzip) gzip compress
bzip2) bzip2 compress
EOF
while true; do
read -p "Your option: " option
option=${option:-xz}
case $option in
xz)
compressTool='J'
suffix='xz'
break;;
gzip)
compressTool='z'
suffix='gz'
break;;
bzip2)
compressTool='j'
suffix='bz2'
break;;
*)
echo "Wrong option." ;;
esac
done
tar ${compressTool}cf /backup/etc-`date +%F-%H-%M-%S`.tar.$suffix /etc/*
关于bash的特性,详见这篇文章 https://blog.csdn.net/xiyangyang410/article/details/85090293#bash_385 ↩︎
关于权限的相关介绍 https://blog.csdn.net/xiyangyang410/article/details/85090293#_1324 ↩︎
关于bash变量,详见 https://blog.csdn.net/xiyangyang410/article/details/85454040#bash_156 ↩︎
命令引用相关内容详见 https://blog.csdn.net/xiyangyang410/article/details/85090293#_555 ↩︎
关于信号相关内容,后续将详细介绍 ↩︎