控制语句
1 条件语句
跟其它程序设计语⾔⼀样,Bash 中的条件语句让我们可以决定⼀个操作是否被执⾏。结果取决于⼀个包在 [[ ]]
⾥的表达式。
由 [[ ]] ( sh 中是 [ ] )包起来的表达式被称作 检测命令 或 基元。这些表达式帮助我们检测⼀个条件的结果。
这⾥可以找到有关bash 中单双中括号区别的答案。
在 Bash Shell 中,⽅括号 ("[]") 和双⽅括号 ("[[]]") ⽤于条件测试和模式匹配。⽅括号和双⽅括号在⼤多数情况下是
等效的,但是双⽅括号提供了更多的功能和可读性。
具体来说,下⾯是它们的区别:
1. 双⽅括号⽀持⾼级字符串操作,⽐如 =~ 正则匹配和 == 不区分⼤⼩写的字符串⽐较,⽽⽅括号不⽀持这些
操作。
2. 双⽅括号中的变量不需要使⽤双引号引起来,即使变量值中包含空格或其他特殊字符也可以正确⽐较。但⽅括
号中的变量需要使⽤双引号引起来,否则可能会导致错误的⽐较结果。
3. 双⽅括号⽀持多个条件的逻辑与和逻辑或,⽐如 [[ $name == "John" && $age -eq 20 ]] ,⽽⽅括号则
需要使⽤ -a 和 -o 选项实现这个功能。
4. 双⽅括号中的参数扩展(Parameter expansion)和命令替换(Command substitution)需要使⽤转义字符
" \ ",⽽⽅括号则可以直接使⽤。
5. 双⽅括号中的模式匹配,默认情况下不进⾏⽂件名扩展,⽽⽅括号默认进⾏⽂件名扩展。
6. 双⽅括号的错误处理更友好,可以使⽤ set -o errexit 命令打开错误检查功能(如果任何⼀个⼦命令返回
⾮零退出码,则整个条件测试就会失败),⽽⽅括号不⽀持这个功能。双⽅括号提供了更多的功能和更直观的语法,所以⼀般建议在 Bash Shell 中使⽤双⽅括号进⾏条件测试。
if
if 在使⽤上跟其它语⾔相同。如果中括号⾥的表达式为真,那么 then 和 fi 之间的代码会被执⾏。 fi 标志着条
件代码块的结束。# 写成⼀⾏
if [[ 1 -eq 1 ]]; thenecho "1 -eq 1 result is: true";
fi
# Output: 1 -eq 1 result is: true# 写成多⾏
if [[ "abc" -eq "abc" ]]
thenecho ""abc" -eq "abc" result is: true"
fi
# Output: abc -eq abc result is: true(2) if else 语句
if [[ 2 -ne 1 ]]; thenecho "true"
elseecho "false"
fi
# Output: true(3) if elif else 语句
有些时候, if..else 不能满⾜我们的要求。别忘了 if..elif..else ,使⽤起来也很⽅便。
x=10
y=20
if [[ ${x} > ${y} ]]; thenecho "${x} > ${y}"
elif [[ ${x} < ${y} ]]; thenecho "${x} < ${y}"
elseecho "${x} = ${y}"
fi
# Output: 10 < 20
case
如果你需要⾯对很多情况,分别要采取不同的措施,那么使⽤ case 会⽐嵌套的 if 更有⽤。使⽤ case 来解决复杂
的条件判断,看起来像下⾯这样:
x=10
y=20
oper=$1
case ${oper} in
"+")
val=`expr ${x} + ${y}`
echo "${x} + ${y} = ${val}";;
"-")
val=`expr ${x} - ${y}`
echo "${x} - ${y} = ${val}";;
"*")
val=`expr ${x} \* ${y}`
echo "${x} * ${y} = ${val}";;
"/")
val=`expr ${x} / ${y}`
echo "${x} / ${y} = ${val}";;*)
echo "Unknown oper!";;
esac每种情况都是匹配了某个模式的表达式。 | ⽤来分割多个模式, ) ⽤来结束⼀个模式序列。第⼀个匹配上的模式对
应的命令将会被执⾏。 * 代表任何不匹配以上给定模式的模式。命令块⼉之间要⽤ ;; 分隔。# sh cal.sh '+'
10 + 20 = 30
# sh cal.sh '-'
10 - 20 = -10
# sh cal.sh '/'
10 / 20 = 0
# sh cal.sh '%'
Unknown oper!
for 循环
循环其实不⾜为奇。跟其它程序设计语⾔⼀样,bash 中的循环也是只要控制条件为真就⼀直迭代执⾏的代码块。
Bash 中有四种循环: for , while , until 和 select 。for 与它在 C 语⾔中循环⾮常像。看起来是这样:
for arg in elem1 elem2 ... elemN
do### 语句
done
在每次循环的过程中, arg 依次被赋值为从 elem1 到 elemN 。这些值还可以是通配符或者⼤括号扩展。
当然,我们还可以把 for 循环写在⼀⾏,但这要求 do 之前要有⼀个分号,就像下⾯这样:for i in {1..5}; do echo $i; done还有,如果你觉得 for..in..do 对你来说有点奇怪,那么你也可以像 C 语⾔那样使⽤ for ,⽐如:
for (( i = 0; i < 10; i++ )); doecho $i
done当我们想对⼀个⽬录下的所有⽂件做同样的操作时, for 就很⽅便了。举个例⼦,如果我们想把所有的 .sh ⽂件移
动到 script ⽂件夹中,并给它们可执⾏权限,我们的脚本可以这样写:
DIR=/home/shell
for FILE in ${DIR}/*.sh; do/bcp "$FILE" "${DIR}/scripts"
done
# 将 /home/shell ⽬录下所有 sh ⽂件拷⻉到 /home/wing/scripts
#cp 加绝对路径 或者 前⾯ \cp 去覆盖
while 循环
while 循环检测⼀个条件,只要这个条件为 真,就执⾏⼀段命令。被检测的条件跟 if..then 中使⽤的基元并⽆⼆异。因此⼀个 while 循环看起来会是这样:
while [[ condition ]]
do### 语句
done跟 for 循环⼀样,如果我们把 do 和被检测的条件写到⼀⾏,那么必须要在 do 之前加⼀个分号。### 0到9之间每个数的平⽅
x=0
while [[ ${x} -lt 10 ]]; doecho $((x * x))x=$((x + 1))
done
# Output:
# 0
# 1
# 4
# 9
# 16
# 25
# 36
# 49
# 64
# 81⽆限循环
while [ 1 ]; do#监控某个服务ps aux |egrep sshd >/dev/null#服务挂掉了if [[ $? -ne 0 ]];thensystem start sshdfiecho "ssh service up"sleep 30
done
until 循环
until 循环跟 while 循环正好相反。它跟 while ⼀样也需要检测⼀个测试条件,但不同的是,只要该条件为 假 就
⼀直执⾏循环:
x=0
until [[ ${x} -ge 5 ]]; doecho ${x}x=`expr ${x} + 1`
done
# Output:
# 0
# 1
# 2
# 3
# 4
select 循环
select 循环帮助我们组织⼀个⽤户菜单。它的语法⼏乎跟 for 循环⼀致:
select answer in elem1 elem2 ... elemN
do### 语句
done
select 会打印 elem1..elemN 以及它们的序列号到屏幕上,之后会提示⽤户输⼊。通常看到的是 $? ( PS3 变
量)。⽤户的选择结果会被保存到 answer 中。如果 answer 是⼀个在 1..N 之间的数字,那么 语句 会被执⾏,紧
接着会进⾏下⼀次迭代 —— 如果不想这样的话我们可以使⽤ break 语句。#!/usr/bin/env bash
PS3="Choose the package manager: "
select ITEM in bower npm gem pip
do
echo -n "Enter the package name: " && read PACKAGE
case ${ITEM} inbower)echo "bower install ${PACKAGE}" ;;npm)echo "npm install ${PACKAGE}" ;;gem)echo "gem install ${PACKAGE}" ;;pip)echo " pip install ${PACKAGE}" ;;
esac
break # 避免⽆限循环
done这个例⼦,先询问⽤户他想使⽤什么包管理器。接着,⼜询问了想安装什么包,最后执⾏安装操作。
运⾏这个脚本,会得到如下输出:
$ ./select.sh
1) bower
2) npm
3) gem
4) pip
Choose the package manager: 2
Enter the package name: gitbook-cli
break 和 continue
如果想提前结束⼀个循环或跳过某次循环执⾏,可以使⽤ shell 的 break 和 continue 语句来实现。它们可以在任
何循环中使⽤。
break 语句⽤来提前结束当前循环。
continue 语句⽤来跳过某次迭代。# 查找 10 以内第⼀个能整除 2 和 3 的正整数
i=1
while [[ ${i} -lt 10 ]]; doif [[ $((i % 3)) -eq 0 ]] && [[ $((i % 2)) -eq 0 ]]; thenecho ${i}break;fii=`expr ${i} + 1`
done
# Output: 6# 打印10以内的奇数
for (( i = 0; i < 10; i ++ )); doif [[ $((i % 2)) -eq 0 ]]; thencontinue;fiecho ${i}
done
# Output:
# 1
# 3
# 5
# 7
# 9
函数
bash 函数定义语法如下:
[ function ] funname [()] {action;[return int;]
}1. 函数定义时, function 关键字可有可⽆。
2. 函数返回值 - return 返回函数返回值,返回值类型只能为整数(0-255)。如果不加 return 语句,shell默认将以最后⼀条命令的运⾏结果,作为函数返回值。
3. 函数返回值在调⽤该函数后通过 $? 来获得。
4. 所有函数在使⽤前必须定义。这意味着必须将函数放在脚本开始部分,直⾄ shell 解释器⾸次发现它时,才可以使⽤。调⽤函数仅使⽤其函数名即可。#!/usr/bin/env bashcalc(){PS3="choose the oper: "select oper in + - \* / # ⽣成操作符选择菜单doecho -n "enter first num: " && read x # 读取输⼊参数echo -n "enter second num: " && read y # 读取输⼊参数execcase ${oper} in"+")return $((${x} + ${y}));;"-")return $((${x} - ${y}));;"*")return $((${x} * ${y}));;"/")return $((${x} / ${y}));;*)echo "${oper} is not support!"return 0;;esacbreakdone
}
calc
echo "the result is: $?" # $? 获取 calc 函数返回值函数中如何使⽤位置参数
位置参数是在调⽤⼀个函数并传给它参数时创建的变量。
位置参数变量表:
$0 脚本名称
$1 … $9 第 1 个到第 9 个参数列表
${10} … ${N} 第 10 个到 N 个参数列表
$* or $@ 除了 $0 外的所有位置参数
$# 不包括 $0 在内的位置参数的个数
$FUNCNAME 函数名称(仅在函数内部有值)#!/usr/bin/env bashx=0
if [[ -n $1 ]]; thenecho "第⼀个参数为:$1"x=$1
elseecho "第⼀个参数为空"
fiy=0
if [[ -n $2 ]]; thenecho "第⼆个参数为:$2"y=$2
elseecho "第⼆个参数为空"
fiparamsFunction(){echo "函数第⼀个⼊参:$1"echo "函数第⼆个⼊参:$2"
}
paramsFunction ${x} ${y}执⾏结果:
$ ./function-demo2.sh
第⼀个参数为空
第⼆个参数为空
函数第⼀个⼊参:0
函数第⼆个⼊参:0
$ ./function-demo2.sh 10 20
第⼀个参数为:10
第⼆个参数为:20
函数第⼀个⼊参:10
函数第⼆个⼊参:20# 函数处理参数:
$# 返回参数个数
$* 返回所有参数
$$ 脚本运⾏的当前进程 ID 号
$! 后台运⾏的最后⼀个进程的 ID 号
$@ 返回所有参数
$- 返回 Shell 使⽤的当前选项,与 set 命令功能相同。
$? 函数返回值runner() {return 0
}name=xu
paramsFunction(){echo "函数第⼀个⼊参:$1"echo "函数第⼆个⼊参:$2"echo "传递到脚本的参数个数:$#"echo "所有参数:"printf "+ %s\n" "$*"echo "脚本运⾏的当前进程 ID 号:$$"echo "后台运⾏的最后⼀个进程的 ID 号:$!"echo "所有参数:"printf "+ %s\n" "$@"echo "Shell 使⽤的当前选项:$-"runnerecho "runner 函数的返回值:$?"
}
paramsFunction 1 "abc" "hello, \"xu\""
# Output:
# 函数第⼀个⼊参:1
# 函数第⼆个⼊参:abc
# 传递到脚本的参数个数:3
# 所有参数:
# + 1 abc hello, "xu"
# 脚本运⾏的当前进程 ID 号:26400
# 后台运⾏的最后⼀个进程的 ID 号:
# 所有参数:
# + 1
# + abc
# + hello, "xu"
# Shell 使⽤的当前选项:hB
# runner 函数的返回值:0
Shell 扩展
扩展 发⽣在⼀⾏命令被分成⼀个个的 记号(tokens) 之后。换⾔之,扩展是⼀种执⾏数学运算的机制,还可以⽤
来保存命令的执⾏结果,等等。⼤括号扩展
⼤括号扩展让⽣成任意的字符串成为可能。它跟 ⽂件名扩展 很类似,举个例⼦:
echo beg{i,a,u}n ### begin began begun⼤括号扩展还可以⽤来创建⼀个可被循环迭代的区间。
echo {0..5} ### 0 1 2 3 4 5
echo {00..8..2} ### 00 02 04 06 08命令置换
命令置换允许我们对⼀个命令求值,并将其值置换到另⼀个命令或者变量赋值表达式中。当⼀个命令被 `或 $()`包
围时,命令置换将会执⾏。举个例⼦:
now=`date +%T`
### or
now=$(date +%T)echo $now ### 19:08:26算数扩展
在 bash 中,执⾏算数运算是⾮常⽅便的。算数表达式必须包在 $(( )) 中。算数扩展的格式为:
result=$(( ((10 + 5*3) - 7) / 2 ))
echo $result ### 9在算数表达式中,使⽤变量⽆需带上 $ 前缀:
x=4
y=7
echo $(( x + y )) ### 11
echo $(( ++x + y++ )) ### 12
echo $(( x + y )) ### 13
流和重定向
Bash 有很强⼤的⼯具来处理程序之间的协同⼯作。使⽤流,我们能将⼀个程序的输出发送到另⼀个程序或⽂件,
因此,我们能⽅便地记录⽇志或做⼀些其它我们想做的事。
管道给了我们创建传送带的机会,控制程序的执⾏成为可能。1 输⼊、输出流
Bash 接收输⼊,并以字符序列或 字符流 的形式产⽣输出。这些流能被重定向到⽂件或另⼀个流中。
有三个⽂件描述符:
0 stdin 标准输⼊
1 stdout 标准输出
2 stderr 标准错误输出2 重定向
重定向让我们可以控制⼀个命令的输⼊来⾃哪⾥,输出结果到什么地⽅。这些运算符在控制流的重定向时会被⽤到:
> 重定向输出
&> 重定向输出和错误输出
&>> 以附加的形式重定向输出和错误输出
< 重定向输⼊
<< Here ⽂档 语法
<<< Here 字符串以下是⼀些使⽤重定向的例⼦:
### ls的结果将会被写到list.txt中
ls -l > list.txt### 将输出附加到list.txt中
ls -a >> list.txt### 所有的错误信息会被写到errors.txt中
grep da * 2> errors.txt### 从errors.txt中读取输⼊
less < errors.txt3 /dev/null ⽂件
如果希望执⾏某个命令,但⼜不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null:
$ command > /dev/null/dev/null 是⼀个特殊的⽂件,写⼊到它的内容都会被丢弃;如果尝试从该⽂件读取内容,那么什么也读不到。但
是 /dev/null ⽂件⾮常有⽤,将命令的输出重定向到它,会起到”禁⽌输出”的效果。如果希望屏蔽 stdout 和 stderr,可以这样写:
$ command > /dev/null 2>&1
Debug
shell 提供了⽤于 debug 脚本的⼯具。
如果想采⽤ debug 模式运⾏某脚本,可以在其中使⽤⼀个特殊的选项:
#!/bin/bash optionsoptions 是⼀些可以改变 shell ⾏为的选项。下表是⼀些可能对你有⽤的选项:
-f noglob 禁⽌⽂件名展开(globbing)
-i interactive 让脚本以 交互 模式运⾏
-n noexec 读取命令,但不执⾏(语法检查)
-t — 执⾏完第⼀条命令后退出
-v verbose 在执⾏每条命令前,向 stderr 输出该命令
-x xtrace 在执⾏每条命令前,向 stderr 输出该命令以及该命令的扩展参数举个例⼦,如果我们在脚本中指定了 -x 例如:
#!/bin/bash -xfor (( i = 0; i < 3; i++ )); doecho $i
done这会向 stdout 打印出变量的值和⼀些其它有⽤的信息:
$ ./debug.sh
+ (( i = 0 ))
+ (( i < 3 ))
+ echo 0
0
+ (( i++ ))
+ (( i < 3 ))
+ echo 1
1
+ (( i++ ))
+ (( i < 3 ))
+ echo 2
2
+ (( i++ ))
+ (( i < 3 ))有时我们值需要 debug 脚本的⼀部分。这种情况下,使⽤ set 命令会很⽅便。这个命令可以启⽤或禁⽤选项。使
⽤ - 启⽤选项, + 禁⽤选项:
# 开启 debug
set -x
for (( i = 0; i < 3; i++ )); doprintf ${i}
done
# 关闭 debug
set +x
# Output:
# + (( i = 0 ))
# + (( i < 3 ))
# + printf 0
# 0+ (( i++ ))
# + (( i < 3 ))
# + printf 1
# 1+ (( i++ ))
# + (( i < 3 ))
# + printf 2
# 2+ (( i++ ))
# + (( i < 3 ))
# + set +x
for i in {1..5}; do printf ${i}; done
printf "\n"
# Output: 12345
Shell脚本常⽤⽅法总结
#!/bin/bash#设置下载地址
URL='https://cdn.mysql.com//Downloads/MySQL-5.6/mysql-5.6.38.tar.gz'#设置安装⽬录
PREFIX=/usr/local/$(basename $(echo " ${URL%.tar.gz}"))#设置数据⽬录
DATADIR=/usr/local/$(basename ${URL%.tar.gz})/data#提示⻛格
启动:Starting sshd:
停⽌:Stopping sshd:
成功:SUCESS
失败:FAILED#主程序
PROG=/bin/cp#进程号
PID="./$PROG.pid"#家⽬录
HOME=/home/mysql#取得当前⽬录的绝对路径
$(dirname `readlink -f $0`)经过试验,这种⽅法最稳妥,因为它能追踪软连接到真实的⽬录,就算⾃身被软连接也不影响使⽤。#检测root权限
[ $(id -u) != "0" ] && echo "Error: You must be root to run this script" && exit#检测是否已经安装过
[ -f $PREFIX/bin/mysqld_safe ] && echo "Error:MY-SQL Has been installed" && exit#安装EPEL源
if hostnamectl &>/dev/null; then[[ -f /etc/yum.repos.d/epel.repo ]] || curl -o /etc/yum.repos.d/epel.repo
http://mirrors.aliyun.com/repo/epel-7.repo
else[[ -f /etc/yum.repos.d/epel.repo ]] || curl -o /etc/yum.repos.d/epel.repo
http://mirrors.aliyun.com/repo/epel-6.repo
fi#下载函数
#调⽤:download <tar.gz的⽂件链接>,调⽤结果:1:下载并解压⽂件到当前⽬录, 2:得到⽂件名称
#功能:检测URL是否正确、本地⽂件存在不再重复下载,本地⽬录存在不再解压function download() {if [[ -f ${1##*/} ]] ;thenFILE=${1##*/} ; [[ -d ${FILE%.tar.gz} ]] || tar vxf ${1##*/}else[[ $1 != *.tar.gz ]] && echo 'URL Check Failed' && exitcurl -o ${1##*/} $1 && tar vxf ${1##*/}fi
}#编译函数
function Construct() {...
}#下载源码并进⼊⽬录
download $URL && cd ${FILE%.tar.gz} || exit 1#编译安装
Construct && make && make install || exit 1#显示成功的样式
function SUCESS() {echo -e "$*\033[59G[\033[32;1m OK \033[0m]"
}#显示失败的样式
function FAILED() {echo -e "$*\033[59G[\033[1;31m FAILED \033[0m]"
}#在当前⽤户添加⼀个定时任务#作⽤:添加⼀个定时任务#优点:免交互,⽆需root权限,防重复添加
NOTE='#logs clean'
TASK='00 02 * * * /bin/bash /app/yunwei/logs_clean.sh >/dev/null 2>/dev/null'
crontab -l | fgrep -q "${NOTE}" || echo -e "`crontab -l`\n${NOTE}" | crontab -
crontab -l | fgrep -q "${TASK}" || echo -e "`crontab -l`\n${TASK}" | crontab -#启动脚本菜单
case "$1" instart)starting ;;stop)stopping ;;restart)stopping && sleep 5 ; starting ;;status)statusing ;;*)echo "Usage: $0 {start|stop|restart|status}" ;;
esac
⽇志封装函数
#⽇志函数封装function wirelog() {level=$1 # ⽇志级别message=$2 # ⽇志信息timestamp=$(date +"%Y-%m-%d %T") # 时间戳# 根据⽇志级别选择输出颜⾊
function wirelog() {level=$1 # ⽇志级别message=$2 # ⽇志信息timestamp=$(date +"%Y-%m-%d %T") # 时间戳# 根据⽇志级别选择输出颜⾊case $level in"error")color='\033[0;31m';;"warning")color='\033[0;33m';;"info")color='\033[0;36m';;*)color='\033[0m' # 默认颜⾊;;esac# 格式化⽇志输出echo -e "$timestamp | $color$level$color | $message\033[0m"
}# 使⽤示例
wirelog 'info' 'This is an info message!'
wirelog 'warning' 'This is a warning message!'
wirelog 'error' 'This is an error'
获取随机字符串或数字
⽅法 1:
# echo $RANDOM |md5sum |cut -c 1-8 #471b94f2
⽅法 2:
# openssl rand -base64 4 #vg3BEg==
⽅法 3:
# cat /proc/sys/kernel/random/uuid |cut -c 1-8 ed9e032c获取随机 8 位数字
⽅法 1:
# echo $RANDOM |cksum |cut -c 1-8 #23648321
⽅法 2:
# openssl rand -base64 4 |cksum |cut -c 1-8 38571131
⽅法 3:
# date +%N |cut -c 1-8 69024815