!/bin/bash 语句干嘛用的?
shell 脚本中的第一行 !/bin/bash 一般用来表示什么意思呢?
#!/bin/bash
是指定脚本使用 Bash 解释器来执行我们的脚本。Bash 解释器相比其他 Shell 解释器,例如sh、dash 等,提供了更多的功能和语法扩展。
当然有很多时候不规范的写法可以忽略掉这一句,执行起来好像也是ok?
这是因为在我们常用 的linux系统上默认都是执行/bin/bash
来执行我们的shell脚本。
shell 脚本执行方式
-
bash shellscript.sh
和./shellscript.sh
都是在使用一个新的bash环境(子进程)来执行脚本内的内容。 -
source shellscript.sh
是在原父进程执行脚本内容。
shell 脚本默认变量
变量 | 意义 |
---|---|
$# | 指传给脚本的参数个数(不包括$0) |
$0 | 指脚本文件本身名字 |
$@ | 传给脚本的所有参数(不包括$0) |
$$ | 是脚本运行的当前进程ID号 |
$? | 显示最后一个命令的退出状态,0表示没有错误,其他表示有错误 |
命令执行判断
-
cmd1; cmd2
顺序执行sync; sync; shutdown -f
-
cmd1 && cmd2
- 若cmd1执行正确则执行cmd2
- 若cmd1执行错误则不执行cmd2
# 查看txt文件是否存在,存在就新建txt2 ls txt && touch txt2
-
cmd1 || cmd2
- 若cmd1执行正确则不执行cmd2
- 若cmd1执行错误则执行cmd2
# 查看txt文件是否存在,不存在就新建txt。 ls txt || touch txt
-
cmd && cmd1 || cmd2
cmd 执行成功则执行cmd1,执行失败则执行cmd2,实际上是(cmd && cmd1) || cmd2
# 查看txt是否存在,存在输出exit,不存在输出no exit test -f txt && echo 'exit' || echo 'no exit'
数据流重定向
在输出重定向中,>
代表的是覆盖,>>
代表的是追加。
- 标准输入,使用
<
- 标准输出,使用
>
- 标准错误输出,使用
2>
- 标准输出和错误输出重定向
2>&1
,比如:./xx.sh > log 2>&1
shell 脚本中的重定向写法:
类型 | 符号 | 作用 |
---|---|---|
标准输出重定向 | command >file | 以覆盖的方式,把 command 的正确输出结果输出到 file 文件中。 |
标准输出重定向 | command >>file | 以追加的方式,把 command 的正确输出结果输出到 file 文件中。 |
标准错误输出重定向 | command 2>file | 以覆盖的方式,把 command 的错误信息输出到 file 文件中。 |
标准错误输出重定向 | command 2>>file | 以追加的方式,把 command 的错误信息输出到 file 文件中。 |
正确输出和错误信息同时保存 | command >file 2>&1 | 以覆盖的方式,把正确输出和错误信息同时保存到同一个文件(file)中。 |
正确输出和错误信息同时保存 | command >>file 2>&1 | 以追加的方式,把正确输出和错误信息同时保存到同一个文件(file)中。 |
正确输出和错误信息同时保存 | command >file1 2>file2 | 以覆盖的方式,把正确的输出结果输出到 file1 文件中,把错误信息输出到 file2 文件中。 |
正确输出和错误信息同时保存 | command >>file1 2>>file2 | 以追加的方式,把正确的输出结果输出到 file1 文件中,把错误信息输出到 file2 文件中。 |
""
和 ''
区别
- 以单引号' '包围变量的值时,单引号里面是什么就输出什么,即使内容中有变量和命令(命令需要反引起来)也会把它们原样输出
- 以双引号" "包围变量的值时,输出时会先解析里面的变量和命令,而不是把双引号中的变量名和命令原样输出
根据变量状态为其赋值
TODO
字符串操作
-
获取长度
str="hello world" echo ${#str} # 输出 11
-
获得子串位置
str="hello world hello world" expr index "${str}" "hello" # 输出1,字符串下标从1开始 expr index "${str}" "stu" # 没有输出0
-
截取
- 按照索引截取
str="hello world" echo ${str:6} # 输出 world echo ${str:0:5} # 输出 hello,截取[0, 0+5] 范围的 echo ${str:(-5)} # 输出world, 从左往右数, 截取长度5
- 去掉最后一个字符
str="hello world" echo "${str%?}" # 输出 hello worl echo "${str%??}" # 输出 hello wor echo "${str%???}" # 输出 hello wo
- 按照索引截取
-
匹配替换
str="hello world hello world" echo ${str/hello/stu} # 输出 stu world hello world / 替换一次 echo ${str//hello/stu} # 输出 stu world stu world // 替换所有匹配 echo ${str/#he/xx} # 输出 xxllo world hello world # 以什么开头来匹配 echo ${str/%ld/yy} # 输出 hello world hello woryy % 以什么结尾来匹配
数组
shell中的数据分为2类:一类是普通数组,另一类是关联数组。
-
数组定义方式
array_test=(a b c d e f) declare -a array_test # 先声明一个空的数组,后面可以根据索引来动态添加
-
将命令结果直接赋值给数组
array=(`ls`)
-
数组特殊表达式
命令 解释 结果 ${A[@]} 返回数组全部元素 a b c d e f ${A[0]} 返回数组第一个元素 a ${#A[@]} 返回数组元素总个数 4 ${#A[3]} 返回第四个元素的长度,即def的长度 3 A[3]=xzy 则是将第四个组数重新定义为 xyz -
数组的遍历
#!/bin/bash array=(a b c d e f) for elem in ${array[@]}; do echo "${elem}" done 或者: for i in "${!arr[@]}"; do echo "$i : ${arr[$i]}" done
数值运算
-
加法
#!/bin/bash service_port=10000 port1=`expr ${service_port} + 1` # port = 10001 port2=`expr ${service_port} - 1` # port = 9999 port3=`expr ${service_port} \* 1` # port = 10000 port4=`expr ${service_port} / 3` # port = 3333 port5=`expr ${service_port} % 3` # port = 1
-
浮点运算
#! /bin/bash i=90.0 j=20.5 c=`echo "scale=2;${i}" / "${j}" | bc` # scale=2表示保留2位小数 echo $c
当数值小于0时的浮点数bc并不会输出前面的0,比如0.12,只会输出.12,解决办法,结合awk
# echo "scale=2; 2/3" | bc .66 # echo "scale=2; 2/3" | bc | awk '{printf "%.2f\n", $0}' 0.66
if else
if [ 表达式1 ]; then
...
elif [ 表达式2 ]; then
...
else
...
fi
条件与
if [ 表达式1 ] && [ 表达式2 ]
条件或
if [ 表达式1 ] || [ 表达式2 ]
-
if 中整数比较
if [ ${num} -eq 3 ]; then
判断式 意义 -eq 相等 -ne 不等 -gt 大于 -ge 大于等于 -lt 小于 -le 小于等于 -
if 中浮点数比较
利用bc# echo "3.2 > 3" | bc 1 # echo "3.2 > 5" | bc 0 # echo "3.2 > 3.2" | bc 0 # echo "3.2 >= 3.2" | bc 1
-
if 中字符串比较
if [ "$test"x = "test"x ]; then
这里的关键有几点:
- 使用单个等号
- 注意到等号两边各有一个空格:这是unix shell的要求
- 注意到
"\$test"x
最后的x
,这是特意安排的,因为当$test
为空的时候,上面的表达式就变成了x = testx
, 显然是不相等的,而如果没有这个x,表达式执行就会报错:[: =: unary operator expected
-
其他判断式
判断式 意义 -e filename 判断文件名是否存在 -f filename 判断文件名是否存在且为文件 -d filename 判断文件名是否存在且为目录 -b -c -S -p -L 判断文件名是否存在且为块设备,字符设备,Socket文件,管道文件,连接文件 -r filename 检测该文件名是否存在且具有读权限 -w filename 检测该文件名是否存在且具有写权限 -x filename 检测该文件名是否存在且具有执行权限 local l_file=$1 if [ -f "${l_file}" ]; then echo "found" else echo "not found" fi
for循环
# 固定循环
for var in con1 con2 con3
do
...
done
# 普通的循环
for ((i=0; i<5; i++)) # 也可以使用: for i in `seq 1 1 10`
do
...
done
# 如果写在一行
for ((i=0; i<5; i++)); do echo "hello world"; done
while 循环
while [条件判断式]
do
...
done
- 无限循环
while : do ... done
#!/bin/bash kiwi_deploy_host_ip="10.1.59.21" max_cnt=300 cnt=0 while : do ping ${kiwi_deploy_host_ip} -c 1 > /dev/null if [ $? -eq 0 ]; then echo "success" break fi cnt=$(expr ${cnt} + 1) if [ ${cnt} -eq ${max_cnt} ]; then echo "timeout" exit 1 fi done
case esac
case $变量名称 in
"第一个变量内容")
......
;; # 有2个分号
"第二个变量内容")
......
;; # 有2个分号
......
esac
命令行:
#!/bin/bash
case "$1" in
setup)
echo "setup"
;;
unsetup)
echo "unsetup"
;;
*)
echo "usage: $0 {setup|unsetup}"
exit 1
esac
函数
-
函数传参注意
#!/bin/bash function func_param() { local l_var=$1; echo "${l_var}" } mysql_user_info="-usheng -psheng0" func_param ${mysql_user_info} # 输出 -usheng func_param "${mysql_user_info}" # 输出 -usheng -psheng0
-
函数内定义local 变量,防止名称污染
var="hello" function test() { local var="world"; echo "${var}" } test # 输出 world
-
函数返回字符串
shell函数只能返回数字,不允许返回字符串,但是可以用点小技巧返回字符串。#! /bin/bash function return_str() { local l_str="hello world" echo "${l_str}" } str=$(return_str) echo ${str} # hello world
-
向函数传递数组
-
从函数返回数组
#!/bin/bash function file_list() { array=(`ls`) echo "${array[@]}" } list=($(file_list)) echo "len: ${#list[@]}" echo "${list[@]}"
注意在 <
>
左边的变量不会展开
#!/bin/bash
ip=10.1.59.43
cmd="ping -c 2 ${ip} &> /dev/null"
${cmd} # 这里会报错,需要用 eval ${cmd}
eval命令将首先会先扫描命令行进行所有的置换,然后再执行该命令。
ssh 执行命令配合awk时
# ssh xxx "cat /etc/passwd | grep root | awk '{print $1, $2}'" # 这里会报错
# ssh xxx "cat /etc/passwd | grep root | awk '{print \$1, \$2}'" # 正确写法
脚本中创建文件
脚本开发中,经常需要将内容写入文件中,采用如下:
cat > file << EOF
hello world
EOF
如果需要追加则:
cat >> file << EOF
hello world
EOF
getopts 处理选项参数
getopts是bash内嵌的一个命令。通过该命令可以获得函数的选项和参数值或者是脚本的命令行选项和参数值。语法为:getopts optstring [args]
#!/bin/bash
while getopts "n:l:t:" arg
do
case "$arg" in
n)
echo "-n;count $OPTARG"
;;
l)
echo "-l length: $OPTARG"
;;
t)
echo "-t:thread num: $OPTARG"
;;
?)
echo "not found param"
exit 1
;;
esac
done
生成随机数和随机字符串
-
生成随机数
- 生成 [0, 32767] 范围随机数
# echo ${RANDOM}
- 生成 [0, 32767] 范围随机数
-
生成随机字符串
- UUID
# cat /proc/sys/kernel/random/uuid
- UUID
循环读取文件每一行
#!/bin/bash
while read line
do
echo $line
done < txt
多任务
-
使用后台运行
&
实现多任务并行#!/bin/bash for i in {1..254} do ip="192.168.80.$i" ping -c 2 $ip &> /dev/null && echo $ip is up & done
- 后台运行
&
与nohup
Shell的后台运行(&)与nohup
- 后台运行
-
使用
wait
实现任务同步
假设我们有数据库初始化脚本1.sh,辅助程序安装脚本2.sh,最后是启动主程序脚本3.sh,我们希望1.sh 和 2.sh可以并行,但3.sh需要1.sh 和 2.sh执行完毕。#!/bin/bash ./1.sh & ./2.sh & wait ./3.sh
判断一个变量是否为空
#!/bin/bash
function check_var_is_null()
{
local l_var=$1
if [ ! -n "${l_var}" ]; then
echo "var is null"
else
echo "var is not null"
fi
}
para1=""
para2=
check_var_is_null ${para1} # 输出 var is null
check_var_is_null ${para2} # 输出 var is null
check_var_is_null ${para3} # 输出 var is null
不可逆操作引用环境变量时用${var:?"undefined 'var'"}
#!/bin/bash
rm -rf ${dir}/ # 如果dir未定义, 则删除根目录.
rm -rf ${dir:?"undefined 'dir'"} # 如果dir未定义, 报错.
文本替换
#!/bin/bash
function global_replace()
{
local l_file=$1
local l_old_word=$2
local l_new_word=$3
eval sed -i 's/${l_old_word}/${l_new_word}/g' ${l_file}
if [ $? -ne 0 ]; then
echo "error: replace word faild, exit"
exit 1
fi
}
定义一个彩色输出函数
#!/bin/bash
function color_echo()
{
if [ $1 == "green" ]; then
echo -e "\033[32;40m$2\033[0m"
elif [ $1 == "red" ]; then
echo -e "\033[31;40m$2\033[0m"
fi
}
color_echo red "test"
进制转换
-
$((N#xx))
可以将N进制表示的xx转为十进制表示echo $((2#110)) 6
- 将十进制转为其他进制
利用 bc 来完成这一操作echo 'obase=16;15' | bc F
编写可靠shell脚本技巧
set -
表示启用某些选项,set +
表示关闭某些选项。
-x
在执行每一个命令之前把经过的变量展开之后的命令打印出来。-
-e
遇到一个命令失败(返回值非0)时,立即退出。#!/bin/bash set -e mysql -uroot -e 'drop database xx' echo "hello world"
假设我们数据库中并不存在xx,那么输出如下:
#./test.sh ERROR 1008 (HY000) at line 1: Can't drop database 'xx'; database doesn't exist
但是如果有时确实需要忽略某个错误,那么可以使用
set +e
,如:#!/bin/bash set +e mysql -uroot -e 'drop database xx' echo "hello world"
输出:
# ./test.sh ERROR 1008 (HY000) at line 1: Can't drop database 'xx'; database doesn't exist hello world
-
-u
如果shell脚本中试图使用未定义的变量,则立即退出。#!/bin/bash set -u echo ${var} echo "here" # 输出:./te.sh: line 4: var: unbound variable
-
-o pipefail
只要管道中的一个子命令失败,整个命令就失败。 -
timeout 限制运行时间
有时候,需要对命令设置一个超时时间,这时可以使用timeout
命令。timeout 600s [cmd] arg1 arg2
命令在超时时间内运行结束时,返回码为 0,否则会返回一个非零返回码。
-
使用 shellcheck 工具
shellcheck 是一款实用的 shell脚本静态检查工具,项目地址:https://github.com/koalaman/shellcheckshellcheck xx.sh
sshpass
经常需要脚本里面执行ssh命令,需要显示写入密码而不需要键盘输入,那么可以使用sshpass 这个工具。
安装 yum -y install sshpass
- 远程连接
sshpass -p xxx ssh root@192.168.11.11
- 远程执行命令
sshpass -p xxx ssh root@192.168.11.11 "ethtool eth0"
shell 代码开发规范
推荐一个shell代码片段学习
项目地址:https://github.com/dylanaraps/pure-bash-bible
参考资料
1、https://zhuanlan.zhihu.com/p/123989641
2、https://zhuanlan.zhihu.com/p/46100771
3、https://www.cnblogs.com/gaochsh/p/6901809.html