Shell脚本易用性编程

由 FairyFar 发布

一、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这个参数对于增强脚本的易用性非常有帮助,例如,有以下两个需求:

  1. 某个输入参数是文件或者目录的路径,在shell命令行中,我们经常会使用Tab键自动补齐,那么-e参数可以实现参数输入时的自动补全功能。
  2. 用户输入参数时,可以翻查之前的历史输入记录。

那么,就需要使用-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是一个千行以上脚本文件,以上报错存在两个问题:

  1. 虽然是正常的报错,但是会让人误以为脚本有BUG;
  2. 仅从报错信息,无法知道是哪一行报的错。

也就是说,这样的报错信息并不友好,会误导使用者,也不利于定位问题。
下面稍作优化:

[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

执行结果如下:
执行效果
两方面改善:

  1. 隐藏了命令原有的报错信息,仅显示脚本期望的报错信息;
  2. 从报错信息,可以知道是脚本哪里报的错。

利用脚本的调试信息,还可以实现其它功能,例如,打印调用栈……
最后,如果shell脚本比较复杂,在debug时,建议使用bashdb debug,以定位问题。



打赏作者以资鼓励: