写了一周的shell脚本人都写吐了,看会learning bash总结下一些shell的特性和技巧.
关于 ∗和@ 的区别
首先shell里有个变量叫做IFS(Internal Field Seprator) 内部域分格符.
这东西的大致用处是这样的:
>> a="alice,in,wonderland"
>> IFS=,
>> for i in "$a"; do echo $i; done
alice in wonderland
如果我们需要打印输入的参数但是不是用空格分割的,而是通过逗号来区别的,那我们的脚本可能要这么写:
IFS=,
echo "$*"
>> ./shell.sh alice dormouse hatter
alice,dormouse,hatter
还有就是如果你有这样的一个函数
function countargs
{
echo "$# args."
}
当你调用countargs “∗"和countargs"@”会有不同的结果
countargs “∗"会打引出1args,countargs"@”会打印3 args.
也就是说”$@”通常会以字符串中的双引号作为分割的标志.
更换IFS的值是很有风险的,但也可能并不会发生什么事如果我们并不滥用他,当然使用完直接重置就行了.
字符串变量操作
${varname:-world} :初始化变量
${varname:=world} :同上,但是不能对坐标变量使用.比如$1
${varname:=message} :如果$varname未定义则打印出messge并退出脚本运行
${varname:+word} :如果$varname已定义就返回word.
${varname:offset:length} :字符串切片
${variable#pattern} :匹配字符串前端最短部分并删除
${variable##pattern} :匹配字符串前端最长部分并删除
${variable%pattern} :匹配字符串前端最短部分并删除
${variable%%pattern} :匹配字符串前端最短部分并删除
${variable/pattern/string}:匹配并替换字符串
# 注意:只有*string这样的pattern才会有最短最长的匹配区别.
getopt:
ARGS=`getopt -a -o P:I: -l prefix:,type: -- "$@"`
[ $? -ne 0 ] &&usage
eval set -- "${ARGS}"
while true
do
case "$1" in
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
esac
shift
done
重定向输入
{
TERM=vt100
# assume this as a default
line=$(tty)
while read dev termtype; do
if [ $dev = $line ]; then
TERM=$termtype
echo "TERM set to $TERM."
break;
fi
done
} < /etc/terms
bash 命令调用流程
- 分割command为一个个的tokens 分割符有SPACE,TAB,NEWLINE.;,(,),<,>,|和&.
token的类型有words,keywords,I/O 重定向和分号. - 检查第一个token是否为引用或者反斜杠(“\”),,如果这个这个token是个关键字,比如if之类的控制结构(control-structure)关键字,function,{,或者(,那么这个指令其实是一个组合指令.读取下一个指令并重新开始进程.
如果这个token不是组合指令,而是一个控制结构(control-structure)的”middle”,比如说是then,else,do或者fi,done这样的end标志,或者是逻辑操作符,shell会报出一个语法错误(syntax error) - 检查指令的第一个word是否是个是个引用(alias),如果找到相应的引用,那就返回setp1重新开始.否则就进入到step4.这个方案允许递归别名.同样允许用于关键字的别名,比如alias aslongas=while
or alias procedure=function. - 执行大括号扩展,例如,a{b,c}会变成ab ac.
- 将~替换为用户的home目录($HOME),如果他是第一个单词的开头.将~user替换为改用户的home目录.
- 替换参数中,或者说变量中所有开头为$的表达式(expression
- 替换指令中所有$(string)格式的表达式
- 计算所有$((string))格式的算术表达式.
- 再次将替换所有参数,指令和算术表达式的指令分割成单词(words,这次使用的分隔符为$IFS而不是step1中的那些元字符.
- 对所有有*,?或者[/]的地方执行路径名扩展,a.k.a.通配符扩张.
11.使用第一个单词作为指令,在下列这些地方寻找其来源(source):先是函数指令(function command),然后是内建(built-in)指令,之后会去寻找在$PATH路径中的文件. - 在设置I/O重定向和其他一些操作之后执行指令.
虽然这里的操作很多,但这还不是完整的流程.
example:
#我们先
alias ll="ls -l"
# 然后执行
ll $(type -path cc) ~alice/.*$(($$%1000))
shell会怎么调用这个命令呢.
# step1:先进行一次分割
ll $(type -path cc) ~alice/ .*(($$%1000))
# step2:ll不是一个关键字,step2跳过
# step3:将ll 替换为 ls -l,因为这是一个alias,然后重复step1将其分割为"ls" "-l"
ls -l $(type -path cc) ~alice/.*$(($$%1000))
# step4:没有大括号,所以不会做任何事.
# step5:将~alice替换为/home/alice
ls -l $(type -path cc) /home/alice/.*$(($$%1000))
# step6:将$$之类的变量替换掉,这里假设是2537
ls -l $(type -path cc) /home/alice/.*$((2537%1000))
# step7: 执行指令type -path cc然后替换掉$(type -path cc)
ls -l /usr/bin/cc /home/alice/.*$((2537%1000))
# step8: 计算算术式2537%1000并替换
ls -l /usr/bin/cc /home/alice/.*537
# step9:再次使用IFS 分割,这里没有做任何事.
# step10:替换所有包含通配符的目录文件名,这里是.*537
# step11: 寻找ls指令,最终在/usr/bin目录下找到了.
/usr/bin/ls -l /usr/bin/cc /home/alice/.*537
# step12:执行/usr/bin/ls 带着选项-l和两个参数.
除了上述之外,还有五个步骤会修改执行的指令:quoting;使用command,buildtin或者
enable指令或者使用高级指令eval.
个人感觉比较重要的是指令的调用顺序和IFS分割符的调用时机,还有就是有个大致替换的顺序,还有就是变量和words之间的区别.
插一下讲一下昨天写脚本时候遇到的问题,脚本大概是这样的
#!/bin/bash
IFS="
"
all=`cat deps`
cd require
for i in $all
do
rpms=$i
# 去除注释
if [ "${rpms%% *}" == "###" ];then
echo $rpms
else
yum -y install $rpms
[ $? -ne 0 ] && echo $rpms >> ./error
fi
done
就是安装一些依赖而已,但是执行的时候却会报错说有些包找不到,但是手动安装又是找的到的.
于是开debug
$ bash -x -e deps.sh
.......
+ yum -y install "net-tools "
原因就是没搞清楚字符串和我们平时输入的参数是有区别的.yum找不到
"net-tools "
的这个包.解决也很简单,改成
yum -y install ${rpm// /}
把字符串中的空格去掉就行了.
sed 的一些用法
$ cat phone.txt
5555551212
5555551213
5555551214
6665551215
6665551216
7775551217
# 使用&来记录批评的内容
$ sed -e 's/^[[:digit:]][[:digit:]][[:digit:]]/(&)/g' phone.txt
(555)5551212
(555)5551213
(555)5551214
(666)5551215
(666)5551216
(777)5551217
# 多次使用脚本
$ sed -e 's/^[[:digit:]]\{3\}/(&)/g' \
-e 's/)[[:digit:]]\{3\}/&-/g' phone.txt
(555)555-1212
(555)555-1213
(555)555-1214
(666)555-1215
(666)555-1216
(777)555-1217
# 使用()和\num来标记使用匹配的块.
$ cat phone.txt | sed 's/\(.*)\)\(.*-\)\(.*$\)/Area \
code: \1 Second: \2 Third: \3/'
Area code: (555) Second: 555- Third: 1212
Area code: (555) Second: 555- Third: 1213
Area code: (555) Second: 555- Third: 1214
Area code: (666) Second: 555- Third: 1215
Area code: (666) Second: 555- Third: 1216
Area code: (777) Second: 555- Third: 1217
build-in declare
设置变量属性
example
$ declare -i val3=12 val4=5
$ declare -i result2
$ result2=val3*val4
带点错误处理的脚本包装
在写脚本的时候觉得直接执行出了错误不能追踪,而且想写一个带点动画的安装脚本,于是就对执行的脚本命令进行了包装.
waiting() {
local pid="$1"
Loading &
local anime_pid="$!"
# 对用户主动crtl-Z做处理.
trap "Cance $anime_pid $pid; echo -e \"\e[0m\e[?25h\"; exit 1; " 2
# 等待脚本执行结果
wait $pid
status="$?"
#恢复光标到最后保存的位置
tput rc
# 发送信号量给动画进程,如果出错座退出处理
[ $status -eq 0 ] && kill -6 $anime_pid >/dev/null 1>&2
[ $status -ne 0 ] && kill -7 $anime_pid >/dev/null 1>&2 && exit 1
sleep 1
# exit $status
}
spin=('[ \e[31m●\e[32m●\e[33m●\e[34m● \e[39m]' '[ \e[32m●\e[33m●\e[34m●\e[31m● \e[39m]' '[ \e[33m●\e[34m●\e[31m●\e[32m● \e[39m]' '[ \e[34m●\e[31m●\e[32m●\e[33m● \e[39m]')
# 输出动画
Loading() {
# 捕获信号量,根据信号量判断执行结果
trap 'echo -ne "[\e[92m Done \e[39m]\n";exit 0;' 6
trap 'echo -ne "[\e[91mfailed\e[39m]\n";exit 1;' 7
cnt=0
while [ 1 ]
do
tput sc
echo -ne "${spin[$((cnt % 4))]}"
sleep 0.5
tput rc
cnt=$((cnt+1))
done
}
print_line(){
echo -en "\e[0m"
string1="Begin $1"
printf '%*.*s' 0 $((LEFT_SIDE)) "$blank"
echo -en " \e[1m"
printf '%s' "$string1"
echo -en " \e[21m"
printf '%*.*s' 0 $((padlength - ${#string1} - 8 )) "$pad"
}
wrap_cmd() {
Description=$1;shift
[ ! -z "$Description" ]&& print_line "$Description"
for script in "$@"
do
# 执行脚本的步骤 -x显示具体的执行步骤 -e 表示在其中有命令出错的时候退出执行
# 输出log
eval " bash -x -e $script 1>> $LOGFILE 2>$ERR_LOG | tee -a $ALLLOG"
st="$?"
sleep 1
[ $st -ne 0 ] && exit 1
done
exit 0
}
使用的方法
#!/bin/bash
source base.sh
wrap_cmd "What ever I Done"\
"/tmp/main.sh"\
&
echo "$pid"
waiting "$pid"
实现的效果是这样的