一、Shell交互式编程
通过交互方式接收用户参数,可以逐步引导用户使用脚本。
1.1 使用read接收用户输入参数
假设有个脚本test.sh需要用户输入参数param
[root@node29 ~]# vim ./test.sh
#! /bin/bash # 下文示例省略该行
param=""
read param
echo "The param you input is: ${param}"
以上是最基础用法,实际使用时,会发现很不好用。想要做得更友好,还需要关注细节。
下面先来了解一下read的常用参数
1.2 read常用参数
-p:显示提示符,例如:
[root@node29 ~]# vim ./test.sh
param=""
read -p "Please input param: " param
echo "The param you input is: ${param}"
-d:表示delimiter,即定界符,一般情况下是以IFS为参数的间隔,但是通过-d,我们可以定义一直读到出现执行的字符位置。
-t:用于表示等待输入的时间,单位为秒,等待时间超过,将继续执行后面的脚本。
-e:在获取用户输入的时候,对功能键做了处理。例如:
[root@node29 ~]# vim ./test.sh
param=""
read -e -p "Please input param: " param
echo "The param you input is: ${param}"
带-e参数的区别,需要自己体会,分别执行上面带-e和不带-e的两个脚本,然后在输入参数时使用上下方向键,看看有什么不同。
带-e后,我们会发现,可以像在shell命令行中一样,翻查历史命令(history),history后文再具体说。
-e这个参数对于增强脚本的易用性非常有帮助,例如,有以下两个需求:
- 某个输入参数是文件或者目录的路径,在shell命令行中,我们经常会使用Tab键自动补齐,那么-e参数可以实现参数输入时的自动补全功能。
- 用户输入参数时,可以翻查之前的历史输入记录。
那么,就需要使用-e参数,再配合history来实现。
二、history
2.1 history命令
在命令行手动输入过程中,一个很常用的功能就是,快速翻查之前已经输入过的命令,以加快命令输入过程。
bash有完善的历史命令,满足该需求,使用简单方便。
显示命令history:
[root@node29 ~]# history
1 set -o history
2 set -o history 2> /dev/null
3 read -p "Please input param" param
4 vim ./test.sh
其它参数:
-c:清空历史命令。
-w:把缓存中的历史命令写入历史命令保存文件中。如果不手工指定历史命令保存文件,则放入默认历史命令保存文件~/.bash_history 中。
2.2 定制脚本自己的history
现在有这样一个需求:脚本test.sh接收用户交互式输入参数,且用户可以翻查之前输入过的参数历史记录。
代码如下:
[root@node29 ~]# vim ./test.sh
# 设置history文件,以及size:
HISTFILE=~/.test_history
HISTSIZE=1000
HISTFILESIZE=1000
# 启用history
set -o history
param=""
read -e -p "Please input param: " param
echo "${param}" >> ${HISTFILE}
echo "The param you input is: ${param}"
运行以上test.sh脚本,任意输入参数,再次运行test.sh脚本,然后使用history快捷键,就可以翻查到之前的输入记录了。
三、显示进度
对于一个执行时间比较长的操作,如果可以动态显示处理进度,将有效提高交互的友好性。
以下是动态显示处理进度的最基本代码:
[root@node29 ~]# vim ./test.sh
for ((i=0; $i<=100; i+=1)); do
printf "progress: %d\r" $i
sleep 1
done
echo ""
Python 2.7进度显示:
[root@node29 ~]# vim ./test.py
#! /usr/bin/env python
#coding: utf-8
from __future__ import print_function
import sys
import time
for i in range(0, 100):
print('progress: %d' % i, end='\r')
sys.stdout.flush()
time.sleep(1)
Python 3进度显示(比Python 2.7要简洁):
[root@node29 ~]# vim ./test.py
#! /usr/bin/env python
#coding: utf-8
import time
for i in range(0, 100):
print('progress: %d' % i, end='\r', flush=True)
time.sleep(1)
四、shell脚本执行Python脚本
shell脚本中,可以直接执行Python命令,例如:
[root@node29 ~]# vim ./test.sh
python -c "print 'abc'"
也可以直接执行Python脚本,例如:
[root@node29 ~]# vim ./test.sh
python ./test.py
现在有这么一个需求:已有shell脚本test.sh,已经开发了很多功能,但是有个新功能需要处理一个大文件(文件大小几十GB以上),直接使用shell脚本处理效率太低,想用Python来实现,但是,这个Python的处理脚本内容较长,不适合使用“直接执行Python命令”方式。为了便于后期维护脚本维护,不想因为这个功能再增加一个Python脚本文件。
无论哪种脚本文件,最后都是文本文件,因此,我们可以“使用一个脚本生成另一个脚本”,以达到多个脚本内容合成在一个文件的效果。此方法同样适用于其它不同脚本语言之间的交叉使用。
范例如下:
[root@node29 ~]# vim ./test.sh
# Python脚本内容
PYTHON_TXT="\
#! /usr/bin/env python
#coding: utf-8
import sys
import os
if __name__ == '__main__':
file = sys.argv[1]
with open(file, 'r') as r_f:
for line in r_f:
print(line)
"
# 生成Python脚本文件
echo "${PYTHON_TXT}" > ./test.py
# 执行Python脚本
python ./test.py big_file.txt
五、友好的报错提示
现在有一个示例脚本,其中big_files文件不存在,
[root@node29 ~]# vim ./test.sh
show_file_list()
{
echo "begin"
file_list=`ls ./big_files`
if [ "$?" != 0 ]; then
echo "Invalid file list!"
return 1
fi
echo "file_list is: ${file_list}"
}
show_file_list
执行以上脚本,效果如下:
假设test.sh是一个千行以上脚本文件,以上报错存在两个问题:
- 虽然是正常的报错,但是会让人误以为脚本有BUG;
- 仅从报错信息,无法知道是哪一行报的错。
也就是说,这样的报错信息并不友好,会误导使用者,也不利于定位问题。
下面稍作优化:
[root@node29 ~]# vim ./test.sh
my_tip()
{
echo "${FUNCNAME[1]}(), line ${BASH_LINENO[0]}: $*"
}
show_file_list()
{
echo "begin"
file_list=`ls ./big_files 2> /dev/null`
if [ "$?" != 0 ]; then
my_tip "Invalid file list!"
return 1
fi
echo "file_list is: ${file_list}"
}
show_file_list
执行结果如下:
两方面改善:
- 隐藏了命令原有的报错信息,仅显示脚本期望的报错信息;
- 从报错信息,可以知道是脚本哪里报的错。
利用脚本的调试信息,还可以实现其它功能,例如,打印调用栈……
最后,如果shell脚本比较复杂,在debug时,建议使用bashdb debug,以定位问题。