本系列共 4 篇:(一)Shell 技巧 · (二)怎样 getopts · (三)分享与扩展 · (四)C 语言 API

上一篇文章中我们介绍了一些在 GMT 脚本编写中可能会用到的 Shell 编程基础知识,今天我们将介绍 Linux 中一个很棒的内置命令 getopts。使用它我们可以方便地自定义标签并获取命令行参数。本篇教程将首先通过一个例子介绍 getopts 的使用方法,然后结合之前我们学习的 gmt_shell_functions.sh 脚本编写一个自动化绘制平面图的 Shell 脚本。如果你准备好了,我们就开始吧。

getopts 简短说明

话不多说,先上例子(personal_info.sh,不用着急看脚本,我们会在后面逐步讲解):

#!/bin/bash
# 这个简单脚本会拾取姓名、年龄与性别三个参数并显示对应的信息。

# 初始化三个变量name age gender为Unknown
name="Unknown"
age="Unknown"
gender="Unknown"

# 显示个人信息 参数依次为 name age gender
show_info()
{
	if [[ ${2} -ge 18 && ${3} == 'm' ]]; then
		echo ${1} is a ${2} years ago man.
	elif [[ ${2} -ge 18 && ${3} == 'f' ]]; then
		echo ${1} is a ${2} years ago woman.
	elif [[ ${2} -lt 18 && ${3} == 'm' ]]; then
		echo ${1} is a ${2} years ago boy.
	elif [[ ${2} -lt 18 && ${3} == 'f' ]]; then
		echo ${1} is a ${2} years ago girl.
	else
		echo "Alien."
	fi
}

# 使用getopts从命令行获取参数
while getopts "hn:a:g:" arg
do
	case $arg in
		# 拾取到-h选项,显示帮助信息后退出
		h)
			printf "usage: ${0##*/} -n<name> -a<age> -g<gender> [-h]\n"
			printf "%s\t%s\n" "-n" "Your name"
			printf "%s\t%s\n" "-a" "Your age."
			printf "%s\t%s\n" "-g" "Your gender. Enter 'm' for male and 'f' for female."
			printf "%s\t%s\n" "-h" "Show help information."
			exit 0;;
		n)
			name=$OPTARG;; # 对变量进行赋值,注意后跟双分号结束。
		a)
			age=$OPTARG;;
		g)
			gender=$OPTARG;;
		?) # 拾取到未知参数,显示帮助信息后退出。
			printf "error: unknow argument\nuse -h option to see help information.\n"
			exit 1;;
	esac
done

# 检查参数值并调用函数显示个人信息
if [[ ${name} != "Unknown" && ${age} != "Unknown" && ${gender} != "Unknown" ]]; then
	show_info ${name} ${age} ${gender}
fi

在终端运行此脚本:

$ ./personal_info.sh -h
usage: personal_info.sh -n<name> -a<age> -g<gender> [-h]
-n	Your name
-a	Your age.
-g	Your gender. Enter 'm' for male and 'f' for female.
-h	Show help information.
$ ./personal_info.sh -j
./personal_info.sh: illegal option -- j
error: unknow argument
use -h option to see help information.
$ ./personal_info.sh -n Joe -a 18 -g f
Joe is a 18 years ago woman.

首先,我们注意到 getopts 选项标签在使用中的格式为 - 加标签字母,如 -h。如果这个选项可以接收参数,则参数需紧跟标签之后(有没有空格都可以)。此时脚本显示了一些帮助信息。仔细来看:

personal_info.sh -n<name> -a<age> -g<gender> [-h]

表示这个脚本有 -n-a-g-h 四个选项。在命令行工具的选项表示中,尖括号内包含参数的名称或者简要提示。中括号表示某个选项是非必需的、可选的。因此,我们可以看到上面这个脚本有三个必需选项,参数的内容分别是姓名、年龄和性别。OK,现在我们来试着用用看,可以我错误地输入了一个 -j 选项。可以见到基本返回了一个错误信息。还好,它能知道我错了。我们再次输入参数 -n Joe -a 18 -g f 得到了正确的输出:

Joe is a 18 years ago woman.

你是不是在想为什么不直接挨个拾取命令行参数呢?现在这样我还要多输入几个字母。这就要提到 getopts 的几个主要优点:

  1. 自动的错误标签识别;
  2. 标签出现的顺序是任意的,所以不用记住参数的顺序;
  3. 通过标签与帮助信息我们可以随时参看参数的具体内容,方便使用;
  4. 提供了多种标签类型,可以满足不同的选项要求。

如果我们的脚本需要提供十多个甚至更多选项时,如何组织获取参数就非常的重要了。此时 getopts 真正的威力也就发挥出来了。

好了,说了这么多,下面正式开始讲解这个脚本。前面的变量设置与函数部分我们略过不讲,有疑问的同学请参考上一篇文章。我们首先来看:

while getopts "hn:a:g:" arg

这一句包含了 getopts 标签设置的主要语法。while getopts 表示循环使用 getopts 匹配符号标签的选项。双引号中每一个字母表示一个标签,其中带冒号的标签表示可接收参数,没有冒号的标签即表示不接收标签。这里我们一共定义了四个标签,分别为 h(帮助)、n(姓名)、a(年龄)和 g(性别)。arg 为储存选项与参数的一个数组。

下面我们看循环体内的部分:

do
	case $arg in
		# 拾取到-h选项,显示帮助信息后退出
		h)
			printf "usage: ${0##*/} -n<name> -a<age> -g<gender> [-h]\n"
			printf "%s\t%s\n" "-n" "Your name"
			printf "%s\t%s\n" "-a" "Your age."
			printf "%s\t%s\n" "-g" "Your gender. Enter 'm' for male and 'f' for female."
			printf "%s\t%s\n" "-h" "Show help information."
			exit 0;;
		n)
			name=$OPTARG;; # 对变量进行赋值,注意后跟双分号结束。
		a)
			age=$OPTARG;;
		g)
			gender=$OPTARG;;
		?) # 拾取到未知参数,显示帮助信息后退出。
			printf "error: unknow argument\nuse -h option to see help information.\n"
			exit 1;;
	esac
done

首先,Shell 的 case 语句与 C 语言类似,只是要注意结尾为 case 的反写 esac。这个语句会依次匹配 arg 中储存的选项标签,如果符合则执行对应语句。比如识别到 h 标签则显示帮助信息,其他情况就对相应的变量进行赋值。特别地,? 标签为 getopts 内置的标签,表示未知的标签,因此此时要输出一个错误信息供使用者参考。${OPTARG}getopts 内置的变量,等于当前所匹配标签的参数值。例如匹配到 n 标签,则 ${OPTARG} 即为姓名的值。

最后,得到所有参数并进行必要的检查后,就可以调用函数显示相应的信息啦。

平面图绘制脚本

了解 getopts 的用法后,现在我们就来编写一个绘制规则网格图像的脚本吧。这里假设你已经有了一定的 GMT 基础,我们直接看下面这个脚本:

#!/bin/bash

# 设置网格文件名称
nc_data=example.nc
#设置色标文件名称
color="rainbow"
#设置color bar单位
unit="m"
#设置网格文件范围
range=0/1000/0/1000
# 参数设置区域结束,请勿轻易修改下面内容

# 初始化临时文件名
cptfile=user.cpt
psfile=${nc_data%.*}.ps # 使用输入文件命令输出文件
jpgfile=${nc_data%.*}.png

# 设置绘图参数 具体作用参见GMT官方文档
gmt gmtset \
FONT_ANNOT_PRIMARY=10.5p,Times-Roman,black \
MAP_FRAME_PEN=thinnest,black \
MAP_GRID_PEN_PRIMARY=thinnest,black \
MAP_TICK_PEN_PRIMARY=thinnest,black \
MAP_TICK_LENGTH_PRIMARY=1p/0.5p \
MAP_TITLE_OFFSET=7.5p \
MAP_GRID_CROSS_SIZE_PRIMARY=2p \
FONT_LABEL=10.5p,Times-Roman,black \
MAP_FRAME_AXES=WesNZ \
MAP_LABEL_OFFSET=2.5p \
MAP_ANNOT_OFFSET_PRIMARY=2.5p

# 计算适用于网格文件的色标文件
gmt grd2cpt ${nc_data} -C${color} -R${range} -Z -D > ${cptfile}
# 绘制数据图像
gmt grdimage ${nc_data} -R${range} -C${cptfile} -Bxaga+l"x (m)" -Byaga+l"y (m)" -JX1.5i -K -P > ${psfile}
# 绘制色标
gmt psscale -Dx0.1i/-0.2i+w1.3i/0.05i+h -C${cptfile} -Bxa -By+l${unit} -O >> ${psfile}
# 将ps文件转换为eps和png格式
gmt psconvert ${psfile} -A -TEG -E300

# 删除临时文件
rm ${cptfile} ${psfile} gmt.history gmt.conf

运行这个脚本我们可以得到输出图像:

使用 rainbow 色标绘制的规则网格图像

这个脚本很不错,我们只需要修改脚本顶部的这些变量就可以使用不同色标来绘制网格文件。

# 设置网格文件名称
nc_data=example.nc
#设置色标文件名称
color="rainbow"
#设置color bar单位
unit="m"
#设置网格文件范围
range=0/1000/0/1000

但我们还想要进一步优化这个脚本,比如可以利用 gmt_shell_functions.sh 中的 gmt_get_gridregion() 函数来自动获取网格文件的范围。相应的修改如下:

# 在脚本顶部包含GMT自带脚本文件
. gmt_shell_functions.sh
# 将range=0/1000/0/1000替换为
range=$(gmt_get_gridregion ${nc_data})

进一步的,利用今天我们学习的 getopts 命令,我们可以编写如下脚本来获取其他变量的值。这样我们就得到了一个通用的绘制图像的脚本,将修改后的脚本放置在你电脑的环境变量地址中,这样你就能在任何文件夹使用它了。Nice!

# 获取网格文件名称
nc_data="null"
#设置色标文件名称
color="rainbow"
#设置color bar单位
unit="m"

# 从命令行获取参数
while getopts "hi:u:c:" arg
do
	case $arg in
		h)
			printf "simple grid file mapping using GMT script under x-y plane.\n" 
			printf "usage: ${0##*/} -i<grid-data> [-u<unit>] [-c<cpt-file>] [-h]\n"
			printf "%s\t%s\n" "-i" "Input grid file"
			printf "%s\t%s\n" "-u" "Data unit. The default is meter."
			printf "%s\t%s\n" "-c" "Color cpt. The default is rainbow."
			printf "%s\t%s\n" "-h" "Show help information."
			exit 0;;
		i)
			nc_data=$OPTARG;;
		u)
			unit=$OPTARG;;
		c)
			color=$OPTARG;;
		?)
			printf "error: unknow argument\nuse -h option to see help information\n"
			exit 1;;
	esac
done

在这个脚本中,我们只有一个必需的参数,那就是网格文件的名称。当然你也可以使用别的色标或者单位,只不过它们是可选的。设置可选参数来测试脚本是否能正常运行:

./imgnc.sh -i example.nc -c relief -u cm

改用 relief 色标与 cm 单位后的输出图像

很棒吧,试着添加更多的选项来强化你的脚本吧!

示例文件下载

本文涉及的脚本与示例数据:GMT脚本进阶-2.zip

内含:

  • personal_info.shgetopts 拾取命令行参数的示例脚本;
  • imgnc.sh:自动化绘制平面网格图像的通用脚本;
  • example.nc:测试用网格文件;
  • example.pngexample2.png:两种参数下的输出图像。

也可关注微信公众号 SciByte 获取更多内容。