2020年3月

Shell脚本易用性编程

0 条评论 工具 Python 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,以定位问题。


Code::Blocks 20.03 CentOS 7离线安装

0 条评论 工具 IDE FairyFar

一、安装环境

操作系统:CentOS 7 64位
ssh工具:Xmanager Enterprise 5
注:无需登录到Linux桌面,使用Xshell ssh登录到命令终端即可。Code::Blocks启动后界面将在Windows环境中显示,但是运行在Linux里。

二、简介

Code::Blocks自从2017年发布17.12版本,时隔三年终于更新版本了——20.03。

三、安装

本文以6位系统为例,如果安装32位Code::Blocks,请下载相应的32位版本。
以下rpm包,除了Code::Blocks相关包,其它包可以到rpmfind.net搜索。
请按以下顺序安装:

[下载] rpm -i wxBase-2.8.12-20.el7.x86_64.rpm
[下载] rpm -i wxGTK-2.8.12-20.el7.x86_64.rpm
[下载] rpm -i codeblocks-libs-20.03-1.el7.x86_64.rpm
[下载] rpm -i libXaw-1.0.13-4.el7.x86_64.rpm
[下载] rpm -i xterm-295-3.el7.x86_64.rpm
[下载] rpm -i codeblocks-20.03-1.el7.x86_64.rpm
[下载] rpm -i codeblocks-devel-20.03-1.el7.x86_64.rpm
[下载] rpm -i codeblocks-contrib-libs-20.03-1.el7.x86_64.rpm
[下载] rpm -i gamin-0.1.10-16.el7.x86_64.rpm
[下载] rpm -i codeblocks-contrib-20.03-1.el7.x86_64.rpm

四、运行

在Xshell中,直接执行“codeblocks”,IDE节点将在本地Windows中启动。


Shell调试工具bashdb

0 条评论 工具 Shell Linux FairyFar

一、什么是bashdb

bash是Unix/Linux操作系统最常用的shell之一,bash脚本经常用于编写批处理工具。
一般地,编写bash脚本时,也需要进行debug,通常会加一些print或者echo进行脚本调试,调试结束后还需要删除这些调试代码。
实际上,linux环境下也有类似gdb的工具用于调试bash脚本。这其中,bashdb相对来说更易于使用,使用方法也十分类似gdb。
bashdb开源,基于GLPv2协议。

二、编译安装bashdb

2.1 bashdb项目主页

bashdb.sourceforge.net

2.2 下载源码

https://sourceforge.net/projects/bashdb/files/bashdb/
需要注意的是,不同版本的bashdb对bash版本有要求,bashdb 5.0以上版本要求安装环境的bash版本也是5.0以上版本。

[root@node29 ~]# bash --version 
GNU bash, version 4.2.46(2)-release (x86_64-redhat-linux-gnu)
Copyright (C) 2011 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

2.3 编译安装

本文以bashdb-4.4-1.0.1版本为例,其它版本类似。

[root@node29 ~]# tar -xf bashdb-4.4-1.0.1.tar.bz2 
[root@node29 ~]# cd bashdb-4.4-1.0.1 

[root@node29 bashdb-4.4-1.0.1]# ./configure 
[root@node29 bashdb-4.4-1.0.1]# make 
[root@node29 bashdb-4.4-1.0.1]# make install 

[root@node29 bashdb-4.4-1.0.1]# bashdb --version
bashdb, release 4.4-1.0.1

三、bashdb使用示例

以下以一个简单调试为例,演示bashdb使用方法,详细内容请参考bashdb用户手册。

[root@node29 CORED]# bashdb --debug /root/tools.sh
bash debugger, bashdb, release 4.4-1.0.1

Copyright 2002-2004, 2006-2012, 2014, 2016-2018 Rocky Bernstein
This is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.

Reading /root/tools.sh done.
(/root/tools.sh:11):
11:    LOG_DIR="./tools_log"
bashdb<0> b 1095
Breakpoint 1 set in file /root/tools.sh, line 1095.
bashdb<1> c
Breakpoint 1 hit (1 times).
(/root/tools.sh:1095):
1095:        func_sel="$1"
bashdb<2> n
(/root/tools.sh:1098):
1098:        lioc_dmc_prep
bashdb<3> print $func_sel
1
bashdb<4> q
bashdb: That's all, folks...

四、more…

如果想要进一步使用IDE调试bash脚本,可以在图形界面下,安装DDD。
详情参考:http://www.gnu.org/software/ddd/


C语言“模板”编程模型

0 条评论 编码 C/C++ FairyFar

  关于C++的模板技术,从未停止过争论,反对者与拥趸者态度截然不同,如果你是模板技术的反对者,那么对于本文的阅读可以到此return了。
  模板(template)是C++程序设计语言中采用类型作为参数的程序设计,是泛型编程的基础。C语言中没有模板功能,但是有些场景非常适合用模板来编码实现,我们可以使用宏来模拟“模板”。
  假设有这样一种情况,某个业务函数需要处理int、char、long、int64、float、double等多种不同类型参数,业务逻辑高度相似,如果不使用模板,每种类型参数都需要对应一个函数,代码看上去是不是很笨拙。这种情况在数据库软件中十分常见,成熟数据库软件支持的数据类型多达几十种,但业务逻辑十分相似。试想一下每种类型都对应一套代码,代码维护成本剧增。
  业内有一款开源软件对于模板的使用达到了极致——ClickHouse,代码性能极高,有兴趣的可以下载源码研读。

一、待重构代码

  假设有这么一个问题,现在有两种结构体,功能非常相似,每个结构体都有对应的处理函数,例如:

struct osan_iocmd
{
  int  start;
  long  flags;
};

struct osan_segcmd
{
  int  start;
  long tags;
};

/* 以下代码为待重构 */
int get_iocmd_start(osan_iocmd* p_iocmd)
{
  return p_iocmd->start;
}

int get_segcmd_start(osan_segcmd* p_segcmd)
{
  return p_segcmd->start;
}

void demo()
{
  ……
  int st1 = get_iocmd_start(p_iocmd);
  int st2 = get_segcmd_start(p_segcmd);
}

  以上代码片段,get_iocmd_start_time和get_segcmd_start_time业务逻辑一样,唯一区别就是函数参数类型不同。为此,我们维护了两个版本的函数。如果这种情况发生在实际代码场景中,针对这两种数据解构相应的处理函数非常多,每个函数都使用两个版本函数,代码的维护成本就整体翻倍了。
  这种情况非常适合使用模板编程进行代码重构。

二、C++模板

  先看一下如何使用C++模板重构代码。
  创建头文件cmd_inc.h:

/* 使用C++模板重构 */
template <class T>
int get_cmd_start(T* p_cmd)  // 函数模板声明与定义
{
  return p_cmd->start;
}

  函数测试文件demo.c:

#include "cmd_inc.h"
void demo()
{
  ……
  int st1 = get_cmd_start<osan_iocmd>(p_iocmd);   // 使用osan_iocmd实例化
  int st2 = get_cmd_start<osan_segcmd>(p_segcmd); // 使用osan_segcmd实例化
}

  我们用模板将两个版本函数合成了一个,代码变得更优雅了。

三、C语言模拟“模板”

  C语言没有模板,我们使用宏模拟模板,达到与模板相似的功能。注意,无法完全实现模板特性。
  创建头文件cmd_inc.h:

#ifndef _CMD_INC_H_
#define _CMD_INC_H_
……
// 此处放函数“模板”之外的代码
#ifndef // end of _CMD_INC_H_

/* 声明两个版本函数原型 */
int get_iocmd_start(osan_iocmd* p_iocmd);
int get_segcmd_start(osan_segcmd* p_segcmd);

#ifdef ENABLE_C_TEMPL
int get_cmd_start(CMD_TYPE* p_cmd)  // 函数“模板”定义
{
  return p_cmd->start;
}
#endif // end of ENABLE_C_TEMPL

  创建函数“模板”文件cmd_inc.c:

……
// 上面是其它代码

#define ENABLE_C_TEMPL

/* 使用宏展开osan_iocmd版本函数定义 */
#define get_cmd_start  get_iocmd_start
#include "cmd_inc.h"
#undef get_cmd_start

/* 使用宏展开osan_segcmd版本函数定义 */
#define get_cmd_start  get_segcmd_start
#include "cmd_inc.h"
#undef get_cmd_start

#undef ENABLE_C_TEMPL

// 下面是其它代码
……

  函数测试文件demo.c:

#include "cmd_inc.h"
void demo()
{
  ……
  int st1 = get_iocmd_start(p_iocmd);
  int st2 = get_segcmd_start(p_segcmd);
}

  关于上述代码说明:

  1. 使用宏ENABLE_C_TEMPL启用函数“模板”, demo.c文件也include cmd_inc.h文件了,但是无需再定义模板函数,此时因为没有define该宏,所以,不会启用函数“模板”代码。
  2. cmd_inc.c文件中,我们include两次cmd_inc.h,每次define get_cmd_start不同来展开不同函数,也就是说,我们通过宏展开生成了get_iocmd_start和get_segcmd_start两个版本函数定义。
  3. 函数可以重复声明,所以,cmd_inc.h中的函数声明可以被多次include引入。

四、C语言“模板”的优缺点

  优点:

  • 功能重复,参数类型不同的函数仅保留一个版本,便于维护。
  • 不影响debug时源码与指令对照。
  • 编译期类型检查仍然有效。

  缺点:

  • 编码时,编辑器无法对“模板”参数进行智能提示,这也是C++模板的缺点。
  • 使用模板需要把函数模板放在头文件里,这样会暴露函数实现,不适合用作接口,这同样是C++模板的缺点。
  • 阅读代码时,contex不能直接关联到模板函数。可以对代码布局进行调整,将函数声明与模板函数书写在相近位置。