用于 Shell 脚本的终端函数
“Shell Curses”是一个脚本函数库,这些函数为 Shell 程序员提供了将基于文本的光标移动到屏幕上指定位置的能力。这种能力允许使用 Shell 脚本创建菜单和数据输入系统,而不需要已编译的库。这些函数非常类似于“C”语言的“Curses”库。
0 评论:
引言
Shell Curses 创建于 1993 年,目的是为了解决需要一组不必针对每种新平台进行重新编译的可移植光标操作函数这一特定问题。这些函数最初采用 Bourne Shell 编写,后来迁移到了 Korn Shell 93,尽管当前的函数集在 Bash 中同样工作得非常好。自从 1993 年以来,Shell Curses 已被下载了两百多万次,并且目前全世界的组织正在各种各样的应用程序中使用它。作为 Shell Curses 的创作者,我可以告诉您当前版本是免费可用的,无需许可证即可用于任何目的。
函数
在 15 年前编写这一组函数时,大多数 UNIX® 系统管理员同时也是 C 语言程序员,非常熟悉名为 Curses 的 C 语言函数库,该函数库提供了光标操作和文本窗口函数。今天,由于图形界面的普及,许多 UNIX 系统管理员不再那么熟悉光标操作和文本窗口。尽管仍然存在对这些函数的需要,但知识库却似乎正在缩小。本文的目的是提高读者对 Shell Curses 函数库的持续需要和存在性的意识。
C 语言 Curses 与 Shell Curses 函数库之间的区别在于,C 语言库是已编译的二进制,并且必须编译并链接到其他已编译的程序中。Shell Curses 函数库是许多 Curses 函数的纯粹 Shell 脚本实现,并且不需要编译。Korn Shell 93 版本的 Shell Curses 使用的唯一 UNIX 实用工具是“tput
”。有关 tput 的更多信息,请参阅参考资料,本文将不对其进行讨论。Shell Curses 函数与已编译的 Curses 函数具有相同的名称和参数结构,因此熟悉 Curses 的程序员也熟悉如何使用 Shell Curses,虽然 Bourne 或 Korn Shell 脚本语法将与 C 语言有所不同。
Shell Curses 库
Shell Curses 库由以下函数组成(按逻辑使用顺序排列):
表 1:标准 Shell Curses 函数
函数 | 参数 | 描述 |
---|---|---|
initscr | 初始化 Shell Curses 屏幕寻址系统 | |
endwin | 取消初始化 Curses 屏幕寻址系统 | |
refresh | [BufferName] | 将屏幕中的逻辑缓冲区清空。 如果为此函数指定了参数字符串,则会将环境变量 BufferName 中存储的光标命令回显到屏幕上。 |
clear | 清屏。 | |
move | ${RowNbr } ${ColNbr} | 将逻辑光标移动到指定的行和列。需要行和列编号参数。 |
mvcur | ${RowNbr} > ${ColNbr} | 将物理光标移动到指定的行和列。需要行和列编号参数。 |
addch | X | 在屏幕上的当前位置打印字符。需要单个字符参数。 |
addstr | ${String} | 在屏幕上的当前位置打印 String 的值。 |
mvaddch | ${RowNbr} > ${ColNbr} X | 将逻辑光标移动到指定的行和列。在屏幕上指定的行和列打印指定的字符。需要行和列编号参数。 |
mvaddstr | ${RowNbr} ${ColNbr} ${String} | 将逻辑光标移动到指定的行和列。在屏幕上指定的行和列打印 String 的值。需要行和列编号参数,还需要一个 String 参数。 |
clrtoeol | 清除当前行从当前列到行尾的内容。 | |
clrtobot | 清除从当前列到屏幕结尾的屏幕内容。 | |
getch | 从标准输入检索一个字符。 | |
getstr | 从标准输入检索一个字符串。 | |
insch | 在当前屏幕位置插入一个字符。 | |
mvinsch | ${RowNbr} > ${ColNbr} | 将逻辑光标移动到指定的行和列。在 RowNbr, ColNbr 插入一个字符 需要行和列编号参数。 |
insertln | 在当前屏幕位置插入一行。 | |
delch | 删除位于当前屏幕位置的一个字符。 | |
mvdelch | ${RowNbr} ${ColNbr} | 将逻辑光标移动到指定的行和列。删除位于当前位置的一个字符。需要行和列编号参数。 |
deleteln | 删除位于当前行的行。 | |
attroff | 关闭所有屏幕属性。 | |
attron | 什么也不做。仅用于实现 Curses 兼容性。 | |
attrset | ${Attribute} | 设置由 Attribute 的定义的屏幕属性。 需要一个属性字符串定义。 有效的属性如下: rev reverse video blink Blinking mode bold Bold Video dim Half Bright video smul Start Underscore Mode rmul End Underscore Mode sgr0 Exit all Attributes |
增强的 Shell Curses 功能
下列函数包括在 Shell Curses 函数库中,并提供了超越普通“Curses”函数以外的增强功能:
表 2:增强的 Shell Curses 函数
函数 | 参数 | 描述 |
---|---|---|
savescr | [BufferName] | 将逻辑屏幕缓冲区保存到由 BufferName 定义的环境变量中。 需要一个屏幕名称字符串。 |
mvclrtoeol | ${RowNbr} ${ColNbr} | 将逻辑光标移动到指定的行和列。需要行和列编号参数。 |
clrtobol | 清除行从当前列到行首的内容。 | |
mvclrtobol | ${RowNbr} ${ColNbr} | 将逻辑光标移动到指定的行和列。清除行从当前列到行首的内容。需要行和列编号参数。 |
mvclrtobot | ${RowNbr} ${ColNbr} | 将逻辑光标移动到指定的行和列。清除从当前列到屏幕结尾的屏幕内容。需要行和列编号参数。 |
getwd | 从标准输入检索一个单词。 | |
beep | 鸣叫显示铃声。 | |
chkparm | ${String} | 确定“String”的值是否为 Null,如果是则返回 false,如果不是则返回 true。需要单个字母数字参数。 |
chkint | ${Nbr} | 确定“Nbr”的值是否为数字,如果是则返回 true,如果不是则返回 false。需要单个数字参数。 |
chklines | ${Nbr} | 确定“Nbr”的值是否小于或等于屏幕上的行数。如果是则返回 true,如果不是则返回 false。需要单个数字参数。 |
chkcols | ${Nbr} | 确定 Nbr 的值是否小于或等于屏幕上的行数。如果是则返回 true,如果不是则返回 false。需要单个数字参数。 |
示例
由于篇幅限制,本文没有提供这些函数的实际代码。有关下载信息,请参阅参考资料部分。
人们已使用 Shell Curses 创建了许多工具集,以便为安装系统、菜单程序、数据输入应用程序、数据库等等提供前端界面。下面将提供一些说明各个函数用法的示例。
在编写 Shell 脚本时,必须首先做出的决定之一为是否要使用某个函数库,或者是否要包括某个文件作为包含所有必需函数的“点”脚本。Shell Curses 可以通过任一种机制进行利用,具体使用哪一种机制将由 Shell 程序员做出决定。下面的示例将把 Shell Curses 的使用表示为函数库,因为对本讨论来说,这是最高效的机制。作为函数库,每个函数应该在某个目录中作为与函数具有相同名称的单独文件存在。在这些示例中,用于 Shell Curses 函数库的目录为 /usr/local/function/shellcurses
。这些示例还使用 Korn Shell 93 作为脚本解释器,这是在每个脚本开头的 shebang 行进行定义的 (#!/usr/bin/ksh93
)。
在 IBM® AIX® 下面使用 Shell Curses 函数库的一个问题在于,AIX 已经提供了名为 clear
和 refresh
的已编译库,这些库将与具有相同名称的 Shell 函数产生混淆。要解决此问题,应该将 clear
、refresh
和 initscr
函数全都复制到单个文件中名为 initscr
的函数库中。这可以确保在调用 initscr
函数时,将同时初始化 clear
和 refresh
的 Shell Curses 函数,从而避免与 AIX 二进制命令 clear
和 refresh
产生混淆。
此示例使用 Shell Curses 函数来清屏;将光标移动到第 10 行,第 25 列;并在该位置打印单词 Hello World!
。然后将光标移动到第 23 行,第 1 列。请注意对 refresh
函数的调用。这是实际将命令发送到屏幕以进行显示的函数。
#!/usr/bin/ksh93 FPATH="/usr/local/functions/shellcurses" initscr clear move 10 25 addstr "Example 1: Hello World!" move 23 1 refresh endwin exit 0
下一个示例与前一个示例相同,只不过是使用单个 mvaddstr
函数,而不是使用两个函数,即使用 move
然后再使用 addstr
:
#!/usr/bin/ksh93 FPATH="/usr/local/functions/shellcurses" initscr clear mvaddstr 10 25 "Example 2: Hello World!" move 23 1 refresh endwin exit 0
第三个示例提示用户输入一些数据,然后在屏幕下方的某个位置显示该数据。请注意,提示包含在一个 Shell 变量中,提示中的字符数量用于确定要在屏幕上放置光标的位置,以便用户输入数据。还要注意,refresh
函数在 getstr
函数之前调用,以便在提示用户输入数据之前,将命令刷新到屏幕:
#!/usr/bin/ksh93 FPATH="/usr/local/functions/shellcurses" initscr clear PROMPT="Example 3: Enter some data...:" LEN="${#PROMPT}" (( COL = LEN + 2 + 1 )) mvaddstr 10 2 "${PROMPT}" move 10 ${COL} refresh ANS=$( getstr ) mvaddstr 15 2 "Here is the data you entered.: ${ANS}" move 23 1 refresh endwin exit 0
第四个示例提供一个完整的菜单系统,并具有标题和针对无效选择的错误检查。此示例还在做出选择并显示用户的选择之后重新绘制屏幕。
#!/usr/bin/ksh93 FPATH="/usr/local/functions/shellcurses" SCRWID="80" initscr clear #### Display a screen header, centered on the screen HEADER="Shell Curses Example 4" HDRWID="${#HEADER}" (( HDRBEGIN = ( SCRWID - HDRWID ) / 2 )) mvaddstr 1 ${HDRBEGIN} "${HEADER}" MENU[0]="First Menu Line" MENU[1]="Second Menu Line" MENU[2]="Third Menu Line" MENU[3]="Fourth Menu Line" MENU[4]="Fifth Menu Line" ITEMCNT="${#MENU[@]}" MENUWID="${#MENU[0]}" #### Determine the maximum length of the longest menu item for MENULINE in "${MENU[@]}" do (( ${#MENULINE} > MENUWID )) && (( MENUWID = ${#MENULINE} + 4 )) done #### Display the numbered menu item, centered on the screen (( COL = ( SCRWID - MENUWID ) / 2 )) NBR="0" for MENULINE in "${MENU[@]}" do mvaddstr $(( ++NBR + 4 )) ${COL} "${NBR} = ${MENULINE}" done #### Prompt the user for a selection mvaddstr 22 2 "Enter the number of your selection: " mvaddstr 23 2 "( 0 = Exit )" #### Read the users selection from the screen ANSWER="99" while [[ "_${ANSWER}" != _[0-9] ]] || (( ANSWER < 1 )) || (( ANSWER > ITEMCNT )) do [[ "_${ANSWER}" == _0 ]] && exit 0 mvclrtoeol 22 40 refresh ANSWER="$( getstr )" done #### Display the item number selected by the user clear mvaddstr 1 ${HDRBEGIN} "${HEADER}" mvaddstr 4 2 "You selected menu item number ${ANSWER}" move 23 1 refresh endwin exit ${ANSWER}
第五个示例提供了一个带有错误检查功能的完整多行数据输入系统。此示例利用了动态确定的屏幕宽度和高度,它还使用了反转的视频屏幕属性来强调屏幕上显示的错误消息。
#!/usr/bin/ksh93 FPATH="/usr/local/functions/shellcurses" initscr SCRWID="${MAX_COLS}" SCRLEN="${MAX_LINES}" clear #### Display a two line header, centered on the screen HEADER1="Shell Curses Example 5" HEADER2="Data Entry Screen" mvaddstr 1 $(( ( SCRWID - ${#HEADER1} ) / 2 )) "${HEADER1}" mvaddstr 2 $(( ( SCRWID - ${#HEADER2} ) / 2 )) "${HEADER2}" #### Define the user entry prompts DOTS="........................................" PRMPT[1]="Enter an existing directory name" PRMPT[2]="Enter an existing regular file name" PRMPT[3]="Enter an alpha-numeric value" PRMPT[4]="Enter a numeric value" PRMPT[5]="Enter an executable file name" PCNT="${#PRMPT[@]}" #### Display the numbered data entry prompt with trailing dots for IDX in "${!PRMPT[@]}" do (( DOTWID = ( SCRWID / 2 ) - ( ${#PRMPT[IDX]} + 4 ) )) PRMPT[IDX]="${PRMPT[IDX]}${DOTS:0:${DOTWID}}" mvaddstr $(( IDX + 4 )) 2 "${IDX}: ${PRMPT[IDX]}...:" done DATACOL="$(( ${#PRMPT[1]} + 10 ))" #### Prompt the user for a selection mvaddstr $(( SCRLEN - 3 )) 2 "Select a line number to enter data:" #### Read the users data line number selection from the screen while : do #### Read the line number entered by the user mvclrtoeol $(( SCRLEN - 3 )) 40 refresh ANS="$( getstr )" #### clear the status line and re-display the default status message mvclrtoeol $(( SCRLEN - 2 )) 2 mvaddstr $(( SCRLEN - 2 )) 2 "( 0 = Exit )" #### Validate the user entered line number, if invalid go back and ask again [[ "_${ANS}" == "_0" ]] && exit 0 if [[ "_${ANS}" != _+([[:digit:]]) ]] || (( ANS < 1 )) || (( ANS > PCNT )) then continue fi #### Read the line data from the user entry mvclrtoeol $(( ANS + 4 )) ${DATACOL} refresh DATA[${ANS}]="$( getstr )" mvclrtoeol $(( SCRLEN - 2 )) 2 attrset rev #### Check the validity of the line 1 data as entered by the user if (( ANS == 1 )) && [[ ! -d "${DATA[${ANS}]}" ]] then mvclrtoeol $(( ANS + 4 )) ${DATACOL} mvaddstr $(( SCRLEN - 2 )) 2 "ERROR: Invalid Directory Name" fi #### Check the validity of the line 2 data as entered by the user if (( ANS == 2 )) && [[ ! -f "${DATA[${ANS}]}" ]] then mvclrtoeol $(( ANS + 4 )) ${DATACOL} mvaddstr $(( SCRLEN - 2 )) 2 "ERROR: Invalid Regular File Name" fi #### Check the validity of the line 3 data as entered by the user if (( ANS == 3 )) && [[ "_${DATA[${ANS}]}" != _+([[:alnum:]]) ]] then mvclrtoeol $(( ANS + 4 )) ${DATACOL} mvaddstr $(( SCRLEN - 2 )) 2 "ERROR: Invalid Alpha-Numeric Value" fi #### Check the validity of the line 4 data as entered by the user if (( ANS == 4 )) && [[ "_${DATA[${ANS}]}" != _+([[:digit:]]) ]] then mvclrtoeol $(( ANS + 4 )) ${DATACOL} mvaddstr $(( SCRLEN - 2 )) 2 "ERROR: Invalid Numeric Value" fi #### Check the validity of the line 5 data as entered by the user if (( ANS == 5 )) && [[ ! -x "${DATA[${ANS}]}" ]] then mvclrtoeol $(( ANS + 4 )) ${DATACOL} mvaddstr $(( SCRLEN - 2 )) 2 "ERROR: File is not executable or does not exist" fi attroff refresh done endwin exit ${ANS}
这些示例代表了 Shell Curses 函数库的应用和用法的一小部分。French Menus 下面存在一个基于 Shell Curses 的标准化菜单和数据输入系统。
该菜单系统为 Shell 程序员提供了创建多级菜单系统和多屏数据输入程序的能力,这些多级菜单系统和多屏数据输入程序可与广泛的程序和数据库连接。如果没有指定后端程序或数据库,则使用内置的数据库系统来存储用户指定的信息。为了演示这些功能,下面将提供一个演示程序。French Menus 也充分(如果不是全部的话)利用了 Shell Curses 库中的函数,并且是设计和实现其他应用程序时的理想参考工具。
结束语
本文的目的是强调对基于文本的应用程序的持续需要,以及提高人们对为此目的而创建的现有工具的意识。诸如 Shell Curses 和 French Menus 等函数库使得基于文本的应用程序的创建更加容易,并提供了据以衡量和评估 Shell 编程的标准。由于许多 UNIX 程序员知道 Shell Curses,或者至少知道 Curses,基于这些函数集编写自己的 Shell 程序将会使得这些程序可移植、可扩展和可维护。