1. shell交互式编程
通过交互方式接收用户参数,可以逐步引导用户使用脚本。
使用read接收用户输入参数
假设有个脚本test.sh需要用户输入参数param:
- snippet.bash
[root@node29 ~]# vim ./test.sh #! /bin/bash # 下文示例省略该行 param="" read param echo "The param you input is: ${param}"
以上是最基础用法,实际使用时,会发现很不好用。想要做得更友好,还需要关注细节。
下面先来了解一下read的常用参数。
read常用参数
- -p:显示提示符,例如:
- snippet.bash
[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:在获取用户输入的时候,对功能键做了处理。例如:
- snippet.bash
[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来实现。
2. history
history命令
在命令行手动输入过程中,一个很常用的功能就是,快速翻查之前已经输入过的命令,以加快命令输入过程。
bash有完善的历史命令,满足该需求,使用简单方便。
显示命令history:
- snippet.bash
[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 中。
定制脚本自己的history
现在有这样一个需求:脚本test.sh接收用户交互式输入参数,且用户可以翻查之前输入过的参数历史记录。
代码如下:
- snippet.bash
[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快捷键,就可以翻查到之前的输入记录了。
3. 显示进度
对于一个执行时间比较长的操作,如果可以动态显示处理进度,将有效提高交互的友好性。
以下是动态显示处理进度的最基本代码:
- snippet.python
[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进度显示:
- snippet.python
[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要简洁):
- snippet.python
[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)
4. shell脚本执行Python脚本
shell脚本中,可以直接执行Python命令,例如:
- snippet.bash
[root@node29 ~]# vim ./test.sh python -c "print 'abc'"
也可以直接执行Python脚本,例如:
- snippet.bash
[root@node29 ~]# vim ./test.sh python ./test.py
现在有这么一个需求:已有shell脚本test.sh,已经开发了很多功能,但是有个新功能需要处理一个大文件(文件大小几十GB以上),直接使用shell脚本处理效率太低,想用Python来实现,但是,这个Python的处理脚本内容较长,不适合使用“直接执行Python命令”方式。为了便于后期维护脚本维护,不想因为这个功能再增加一个Python脚本文件。
无论哪种脚本文件,最后都是文本文件,因此,我们可以“使用一个脚本生成另一个脚本”,以达到多个脚本内容合成在一个文件的效果。此方法同样适用于其它不同脚本语言之间的交叉使用。
范例如下:
- snippet.bash
[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
5. 友好的报错提示
现在有一个示例脚本,其中big_files文件不存在,
- snippet.bash
[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;
- 仅从报错信息,无法知道是哪一行报的错。
也就是说,这样的报错信息并不友好,会误导使用者,也不利于定位问题。
下面稍作优化:
- snippet.bash
[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,以定位问题。