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

上一篇文章中我们介绍了如何使用 getopts 来获取 GMT 脚本执行时需要的命令行参数。通过使用 getopts 我们已经基本实现了 GMT 绘图过程的对象化,即给一个网格文件画一张图这样的基本操作。这样的工作流程的确很方便,但是它也有它不方便的地方。

其一是你不太可能在编写脚本之初就考虑到以后在使用过程中的所有需求,所以你可能还是需要经常为你的脚本添加和修改命令选项。这当然很好,你的脚本会越来越强大。但是过多的命令选项会让脚本的使用变得越来越困难,其他人可能需要花很多时间来学习它。而且,有的需求其实是一次性的,这时候去修改脚本显然并不划算。另一个不足之处是分享你的脚本将变得困难。设想这样一个情景,你想分享你的数据和对应的绘图脚本给你的同事,你发现你面对的并不是简单地将文件夹打包一下的情况。你需要将你的模版脚本拷贝到文件夹打包发送,然后告诉你的同事怎么使用这个脚本绘图,或者另写一个说明。这无疑增加了整个分享过程的复杂度。要是你的同事想将压缩包分享给第三人,那情况就更复杂了。

看到这里你可能会想“赶紧来点干的啊!”。

干的来了。

本篇教程我们就是要来解决分享与扩展性的问题,即如何将脚本特例化。什么意思?为了应对分享与扩展性这两个问题,我们的解决思路是在需要的时候为特定的数据(集)自动生成对应的绘图脚本。这个脚本内只包含 GMT 绘图命令,所以可以在任何装有 GMT 的电脑上运行。你的同事面对的只是单纯的 GMT 脚本,因此不需要额外学习任何其他知识。同时,你自己可以使用生成的脚本进行二次编辑,从而满足额外的绘图需求。这样就大大提高了模版脚本的扩展性。

说了这么多,到底怎么弄?

答:写一个 Shell 函数,接收一个规则字符串和一个命令字符串,视规则执行或者打印命令字符串。就是说每当我们要执行一条 GMT 绘图命令时都要经过这个函数。如果我们想要绘图,这个时候就执行这个绘图命令;如果我们想要得到的是命令本身,就打印这条命令。

有了这个思路,我们来直接看这个函数:

# 执行输入的语句或者将其显示在屏幕上 参数1(0显示 1执行)参数2(命令语句)参数3(命令语句)
RunOrEcho()
{
	# 预处理 去除命令语句中的制表符
	first_str=`echo ${2//	/''}`
	# 判断第三个参数是否存在
	if [[ x${3} != x ]]; then
		sec_str=`echo ${3//	/''}`
	fi
	
	# 如果第一个参数为1则执行命令语句 否则在屏幕上显示命令语句
	if [[ ${1} == 1 ]]; then
		${first_str}
		if [[ x${3} != x ]]; then
			${sec_str}
		fi
	else
		if [[ x${3} != x ]]; then
			printf "%s\n%s\n" "${first_str}" "${sec_str}"
		else
			printf "%s\n" "${first_str}"
		fi
	fi
}

简单地测试一下这个函数:

#!/bin/bash

. echo_run.sh

RunOrEcho 0 "# test cmd" "gmt gmtdefaults"

RunOrEcho 1 "gmt gmtdefaults"

得到如下输出:

$ # test cmd
$ gmt gmtdefaults
$ gmt gmtdefaults [core] 6.0.0 [64-bit] - List current GMT default settings
$ 
$ usage: gmt gmtdefaults [-D[s|u]]
$ 
$ 	-D Print the current GMT default settings.
$ 	   Append s to print the SI version of the system defaults.
$ 	   Append u to print the US version of the system defaults.
$ 
$ 	ALL settings will be written to standard output.

第一个测试命令为显示命令字符串,所以我们看到函数在终端显示了这两个字符串。而第二个测试命令则为执行命令语句,所以我们在终端看到了 gmt gmtdefaults 执行后的结果。通过这个函数我们就达成了执行或者显示一个命令的目的。

现在对我们上一篇文章中编写的 imgnc.sh 脚本进行简单的修改,首先添加一个命令 run_command 来控制是否执行绘图命令或者只是输出特例化的脚本内容,然后在相应的绘图命令前使用 RunOrEcho 函数。

# 输出一个bash脚本的头部到屏幕
	if [[ ${run_command} == 0 ]]; then
		echo "#!/bin/bash"
	fi

	# 设置绘图参数
	RunOrEcho ${run_command} "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"

	RunOrEcho ${run_command} "gmt grd2cpt ${nc_data} -C${color} -R${range} -Z -D > ${cptfile}"
	if [[ ${run_command} == 0 ]]; then
		echo "gmt grdimage ${nc_data} -R${range} -C${cptfile} -Bxaga+l\"x (m)\" -Byaga+l\"y (m)\" -JX1.5i -K -P > ${psfile}"
	else
		gmt grdimage ${nc_data} -R${range} -C${cptfile} -Bxaga+l"x (m)" -Byaga+l"y (m)" -JX1.5i -K -P > ${psfile}
	fi
	RunOrEcho ${run_command} "gmt psscale -Dx0.1i/-0.2i+w1.3i/0.05i+h -C${cptfile} -Bxa -By+l${unit} -O >> ${psfile}"
	RunOrEcho ${run_command} "gmt psconvert ${psfile} -A -TEG -E300"

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

我们注意到对于 gmt grdimage 命令我们没有使用函数,而是使用了 if 条件语句。这是因为 grdimage 命令中有双引号包含的参数,所以在使用 RunOrEcho 函数时会有一些问题。如果你有什么好办法改进这个函数也请分享给大家吧~

好了,现在我们来测试改进后的脚本,分别使用如下两个命令可以分别得到:

输入:

$ ./imgnc2.sh -i example.nc

输出:

直接执行绘图命令得到的图像

和输入:

$ ./imgnc2.sh -i example.nc -S

输出:

$ #!/bin/bash
$ 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 example.nc -Crainbow -R0/1000/0/1000 -Z -D > user.cpt
$ gmt grdimage example.nc -R0/1000/0/1000 -Cuser.cpt -Bxaga+l"x (m)" -Byaga+l"y (m)" -JX1.5i -K -P > example.ps
$ gmt psscale -Dx0.1i/-0.2i+w1.3i/0.05i+h -Cuser.cpt -Bxa -By+lm -O >> example.ps
$ gmt psconvert example.ps -A -TEG -E300
$ rm user.cpt example.ps gmt.history gmt.conf

其中,第一个命令为使用数据绘制图形,而第二个命令为输出一个特例化的绘图脚本。使用重定向将生成的脚本保存在文件并执行,就可以得到和上面图形一样的图形啦。现在如果你想要分享某个数据文件夹,只需要在原有绘图命令后添加 -S 命令就可以生成针对指定数据的绘图脚本了。

示例文件下载

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

内含:

  • echo_run.shRunOrEcho 函数定义;
  • func_test.sh:函数测试脚本;
  • imgnc2.sh:支持 -S 选项输出特例化脚本的完整绘图脚本;
  • example.ncexample.epsexample.jpg:测试数据与输出结果。

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