新 KornShell—ksh93
Unix 系统是最早一批没有将命令解释器作为操作系统一部分或特权任务的系统之一。它被编写为普通的User进程,没有特殊的权限或对未公开函数的调用。这促成了一代又一代更优秀的 Shell 的出现。早期版本的 Unix 配备了一个由 Unix 系统的发明者之一 Ken Thompson 编写的命令 Shell。到了 20 世纪 70 年代末,出现了两个大大改进的 Shell。由贝尔电话实验室的 Steve Bourne 创建的 Bourne Shell 在语言方面有了很大的改进。由加州大学伯克利分校的 Bill Joy 创建的 C Shell 是一个改进很多的命令解释器,但语言方面很差。
KornShell 由贝尔电话实验室的 David Korn 编写,结合了这两种 Shell 的最佳特性,并增加了使用与 vi 或 Emacs 编辑器相同的按键来编辑和重新输入当前和以前命令的功能,这取决于用户的意愿。这个 Shell 变得非常流行,但其发行受到限制。因此,创建了几个免费的仿制品,如 pdksh 和 bash。为了向 C Shell 用户提供可视化编辑功能,创建了 C Shell 的增强版本 tcsh。
虽然 Bourne Shell 为编程提供了良好的基础,早期的 KornShell 版本也在此基础上有所改进,但如果不与其他语言(如 awk 编程语言)结合使用,它不足以进行通用脚本编写。虽然在大多数情况下,这两种语言可以很好地协同工作,但使用两种具有独立进程的语言所带来的性能损失通常是令人难以接受的。Perl 语言的创建是为了提供一种具有 Shell 和 awk 组合功能的单一语言。然而,许多人发现 Perl 的语法难以理解。
ksh93 是 KornShell 语言的最新主要修订版,它提供了 Tcl 和 Perl 的替代方案。作为一种编程语言,它在速度和功能上与这两种语言相当,但可以说是最好的交互式 Shell。它是 POSIX 1003.2 Shell 标准的超集。与 Tcl 一样,它是可扩展的和可嵌入的,具有 C 语言应用程序编程接口。事实上,已经使用 ksh93 创建了两个图形 Shell。其中一个,dTksh,是由 Novell 开发的基于 Motif 的语言。另一个 Tksh,由普林斯顿大学的 Jeff Korn 编写,使用 Tk 库,并在此处简要讨论。
描述 ksh93 中新功能的最佳方式是通过示例来说明它们。我们将创建一个名为 lsc 的 Shell 脚本,如清单 1 所示,以提供 ls 输出,并将子目录名称以粗体打印。我们需要保持与标准 ls 相关的多列输出。
lsc 脚本将为作为命令行参数提供的每个目录名称生成 ls 输出。默认操作是为当前目录生成 ls 输出。可以对 lsc 脚本进行一些修改以提高性能。我们将它们作为读者的练习留给读者。我们对要处理的每个目录名称执行以下高级操作。
对于每个目录执行
load directory entries into array entries load entries calculate number of columns in multi-column output calculate maximum number of rows print the current directory name determine output layout add entries to row[] array add entries to col[] array calculate the column widths display the output
完成
ksh93 提供一维索引数组和关联数组。数组元素被引用为 varName[subscript]。索引数组使用算术表达式作为下标。这允许在下标表达式中进行计算。例如,语句 varName[3+8] 引用索引数组的第 11 个元素。(算术表达式在下面有更完整的描述)。
可以使用 varName=(....) 命令从列表中初始化索引数组的元素。这为初始化数组以包含给定目录中的文件名提供了一种方便的表示法。数组中条目的数量描述了找到的文件数量。例如,考虑以下语句,用于使用当前目录中找到的文件名初始化索引数组条目:entries=(*)
关联数组使用任意字符串作为下标。例如,我们可以创建一个州税关联数组,并通过州名引用元素。即使对于字符串中以空格分隔的标记,例如 New Jersey,这也有效。
typeset -A StateTax StateTax[New Jersey]=0.06 print ${StateTax[New Jersey]}
为数组处理提供了几个特殊的 positional 参数展开。使用 ${varName[@]} 引用数组的所有元素。可以使用 ${!varName[@]} 引用数组的下标。符号 ${#varName[@]} 提供数组中元素的数量。可以使用 ${varName[@]:offset:length} 引用数字下标范围内的元素。这种特殊表示法适用于索引数组和关联数组。
数组在示例 lsc 脚本中被广泛使用。我们将 video 定义为关联数组,其中 terminfo 数据库中的功能名称作为下标。video 的定义作为关联数组的复合赋值提供。
video=( [bold]=$(tput bold) [reset]=$(tput reset) [reverse]=$(tput reverse) )
每个元素都从 tput 执行的功能名称的标准输出中分配一个值。例如,video[bold] 是粗体字母的 terminfo 序列。类似地,video[reverse] 将提供反向视频输出。
使用 $(command) 符号将导致 command 在当前 ksh 的子 Shell 中执行。在许多情况下,当 command 是内置函数或 Shell 函数时,ksh 实际上不会 fork/exec 子 Shell。(内置函数在下面描述)。
在 ksh93 中,变量由 name=value 对定义。变量命名空间是分层的,使用 .(点)分隔符。扩展的命名空间允许变量的聚合定义。
lsc 脚本将生成多列输出。我们将输出可视化为一个由行和列组成的表格。行和列的通用定义由名为 cell 的复合变量的定义提供。
cell=( # maximum number of cells integer maximum=0 # maximum width based on entries integer width=0 # current index within the cell integer index=0 # content of the cell typeset entries )
这定义了变量 cell,它具有聚合成员 maximum、width、index 和 entries。引用 ${cell.index} 提供与 index 聚合关联的值。使用 eval 命令,我们可以创建具有相同聚合的其他变量。例如,我们可以定义变量 row 和 col,使其具有与 cell 相同的定义
eval row="$cell" eval col="$cell"
ksh93 提供对国际化的支持。以 $ 开头的双引号字符串会检查消息替换。如果该字符串出现在消息目录中,则 ksh93 将用消息目录中的相应字符串替换该字符串。否则,字符串保持不变。
在 lsc 示例中,对于任何不可读目录的命令行参数,我们显示错误消息 "not found"。我们提供的错误消息是使用国际化支持定义的(参见清单 1 的第 33 行)。如果 Shell 变量 LANG 定义为 POSIX 以外的某种区域设置,则 ksh 将尝试使用国际化支持替换错误消息。否则,消息保持不变。
在 Shell 脚本上执行 ksh -D 将输出为国际化标识的所有消息。例如,在 lsc 脚本中,ksh -D 将输出以下消息。
"${video[reverse]} not found ${video[reset]}"
ksh93 可以通过 KornShell 开发工具包 (KDK) 进行扩展。您可以用 C 语言编写自己的内置函数,并通过 builtin 命令将它们加载到当前的 Shell 环境中。此功能在具有在运行时将代码加载和链接到当前进程的能力的操作系统上可用。
内置命令的执行无需创建单独的进程。相反,该命令由 ksh 作为 C 函数调用。如果此函数在 Shell 进程中没有副作用,则此内置函数的行为与等效的独立命令的行为相同。在这种情况下,主要区别在于性能:消除了进程创建的开销。对于持续时间短的命令,效果可能是显着的。例如,在 SUN OS 4.1 上,对于一个大约 1000 字节的小文件,wc 作为内置命令的运行速度比作为单独进程的运行速度快约 50 倍。
此外,可以编写对 Shell 环境具有副作用的内置命令。通过 KornShell 开发工具包提供的 API,您可以扩展 Shell 编程的应用领域。例如,一个大量使用 Shell 变量命名空间的 X-Windows 扩展被添加为一组内置命令。结果是一个窗口 Shell,可用于编写 X-Windows 应用程序。
虽然添加内置命令肯定有优势,但也存在一些缺点。由于内置命令和 ksh 共享相同的地址空间,因此内置程序中的编码错误可能会影响 ksh 的行为,可能导致其核心转储或挂起。调试也更加复杂,因为内置代码现在是更大实体的一部分。单独进程提供的隔离保证了当命令完成时,命令使用的所有资源都将被释放;此保证不适用于内置函数。此外,由于 ksh 的地址空间将更大,这可能会增加 ksh fork() 和 exec() 非内置命令所需的时间 [尽管在更高级的操作系统(如 Linux)上不是这样,Linux 通过在 fork 时执行“写时复制”来节省内存和时间——编者注]。添加一个运行时间很长或只运行一次的内置命令是没有意义的,因为性能优势将可以忽略不计。在当前 Shell 环境中具有副作用的内置函数具有增加内置函数和 ksh 之间耦合的缺点,从而使整个系统更少模块化和更单片。
尽管存在这些缺点,但在许多情况下,通过添加内置命令来扩展 ksh 是有意义的,并且允许在特定于应用程序的领域中重用 Shell 脚本编写功能。
在 lsc 示例中,我们需要确定字符串列表中的最大字符串大小。这是确定多列显示中初始列数所必需的。我们还将使用它来确定条目列的最大宽度。典型的 Shell 实现将给出为
(( max_stringSize = 0 )) for fileName in * do if (( max_stringSize < ${#fileName} )) then (( max_stringSize = ${#fileName} )) fi done
(有关 (( 和 )) 的说明,请参见下面的算术表达式。)
为了提高性能,我们可以用 C 语言重写此函数。在一个简单的示例中,Shell 等效函数需要 0.58 秒的 CPU 时间。对于相同的任务,C 内置函数提供了 0.08 秒的 CPU 时间。函数名称以 “b_” 开头,表示它是一个内置函数。编译后,strlenList.o 对象将被存档到共享库中。要引用 strlenList 函数,我们必须通过 builtin 命令将其加载到当前的 ksh 环境中(参见清单 1 的第 29 行)。
#pragma prototyped #include "shell.h" #include "stdio.h" int b_strlenList(int argc, char **argv, void *extra) { register int max, n = 0 char **cp = NULL; cp=argv; while ( *(++cp) ) { n = strlen(*cp); max = max < n ? n : max; } fprintf(stdout,"%d\n", max); return(0); }
ksh93 提供了两种函数定义方法。格式如下:
function name { body } name() { body }
提供第二种函数格式是为了与 POSIX 标准兼容。主要区别在于变量名称作用域。在 POSIX 函数中,变量定义具有全局作用域。在以下 POSIX 函数 bar 中,变量 foo 被重新定义为值 6。
typeset foo=5 bar() { typeset foo=6 echo $foo } bar 6 echo $foo 6
ksh93 函数中的变量定义具有局部作用域。在以下 ksh93 函数 bar 中,定义了一个局部变量 foo,它优先于全局变量 foo。
typeset foo=5 function bar { typeset foo=6 echo $foo } bar 6 echo $foo 5
ksh93 通过一系列 规程 函数提供活动变量。从 Shell 级别,您可以编写 get、set 和 unset 规程。通过 KornShell 开发工具包,您还可以添加特定于您的环境的规程。
当引用变量时,例如在 $foo 中,ksh 将调用与 foo 关联的 get 规程。默认规程是简单地返回与 foo 关联的当前值。从 Shell 级别,您可以定义 foo.get 规程函数。
当为变量赋值时,将调用 set 规程。在 set 规程中,特殊变量 .sh.name 是其值正在设置的变量的名称。
在 lsc 的第 31 行中,我们定义了一个 max_stringSize.get 规程函数。每次引用 ${max_stringSize} 都会导致执行此函数。特殊变量 .sh.value 的值是从规程返回的值。
在 ksh93 中,可以使用遵循 ANSI C printf 定义的 printf 语句。这允许将格式规范应用于每个参数。要理解标准 print 和 printf 语句之间的区别,请考虑如何输出 entries 数组(来自 lsc 示例)的内容,每行一个。标准 print 语句会将文件名显示为单行上以空格分隔的标记。但是,使用带有 "%s\n" 格式的 printf 语句将产生所需的结果。
形式为 (( expression )) 的 ksh93 语句称为 算术 命令。当封闭表达式的值为非零时,算术命令返回 True,当表达式求值为零时,返回 False。构造 $((expression)) 可以用作单词或单词的一部分。它被 expression 的值替换。
在 lsc 示例的第 38 行中,我们使用以下代码评估规程函数的值
(( .sh.value = $(strlenList ${entries[@]}) + 3 ))
ksh93 将评估表达式,其中包括对 .sh.value 变量的赋值。请注意,
$(strlenList ${entries[@]})
将调用 strlenList 内置函数,并返回 entries[] 数组中字符串(作为元素值给出)的最大宽度。为了格式化目的,我们在此值上加 3。
ANSI C 字符串通过在 单引号 字符串前加上 $ 来定义。例如,$'*' 是字面量星号 *。使用 ANSI C 字符串,单引号之间的所有字符都保留其字面含义,转义序列除外。转义序列由转义字符 \ 引入。
ANSI C 字符串支持为 Shell 程序员提供了一个基本功能。例如,考虑必须处理值中嵌入制表符的变量。在没有 ANSI C 字符串支持的情况下,我们将无法有效地测试变量的值中是否嵌入了制表符。例如,考虑以下脚本
print "foo\tbar" > /tmp/foobar read aline < /tmp/foobar if [[ "${aline}" == "foo\tbar" ]] then print TRUE fi
比较(参见下面的条件命令)将失败。我们可以用 ANSI C 字符串替换条件,并确保正确的功能。上面的示例应重写为
print "foo\tbar" > /tmp/foobar read aline < /tmp/foobar if [[ "${aline}" == $'foo bar' ]] then print TRUE fi
在清单 1 的第 45 行中,我们必须测试目录是否为空。如果未找到文件,则前面的 entries=(*) 在空目录中会将 entries 变量设置为字面量星号。
ksh93 中的条件命令评估测试表达式并返回 True 或 False。条件命令可以用作 “或列表” (||)、“与列表” (&&) 的一部分,或用作 if-elif-else 命令的一部分。条件命令的格式为
[[ test-expression ]]
当与 “与列表” 结合使用时,ksh93 会评估测试表达式,并且仅当测试表达式评估为 True 时才执行 “与组件”。我们将条件命令用作 “与列表” 的一部分,以便仅当测试表达式为 True 时才执行 return 语句。
[[ ${entries[0]} == $'*' ]] && return 2
for 命令有两种格式。提供传统格式是为了迭代列表中的每个单词。格式为
for variableName [ in word-list ] do compound-list done
已经提供了一个算术 for 命令,它与 C 编程语言 for 语句非常相似。格式为
for (( initExpr ; condition ; loopExpr )) do compound-list done
initExpression 由 ksh 在执行 for 命令之前评估。然后,在每次迭代 compound-list 之前评估 condition。如果 condition 为非零,则 ksh 执行 compound-list。loopExpression 在每次迭代结束时评估。
为名称引用添加了一个新的 typeset 选项。使用 typeset -n nameReference=variableName 将 nameReference 与 variableName 关联。提供了一个特殊的别名 nameref,它等效于 typeset -n。Shell 脚本可以使用引用名称来引用变量名称。名称引用提供了一种方便的机制,可以将复合变量或数组的名称传递给 ksh 函数。这比传递变量的内容更有效。
在 lsc 示例中,函数 setOutput 必须将目录条目添加到适当的行和列。我们可以定义名为 addToRow 和 addToColumn 的单独函数来实现此目的。但是,函数的主体将是等效的。相反,我们选择编写一个使用 nameref 将单元格类型作为参数传递的单个函数 addToCell。
addToCell 函数接受三个参数,其中前两个是必需的。第一个参数是单元格类型,必须是 row 或 col。我们创建一个 nameref,使用局部变量 cell 等效于指定的单元格类型。因此,对 ${cell.index} 的引用将等效于 ${row.index} 或 ${col.index}。
ksh 函数不会跨 ksh 的调用继承。例如,子 Shell 进程无权访问在父 ksh 调用中定义的函数。从历史上看,这限制了 ksh 函数的可重用性。作为一种解决方案,ksh93 将在 FPATH 变量值给出的冒号分隔的目录列表中搜索与函数名称相同名称的可执行文件。在 lsc 示例中,我们可以消除最后一个语句
lsc "${@}"
然后可以将 FPATH 设置为包含 lsc 文件的目录。从 Shell 级别,我们现在可以调用 lsc。ksh93 将加载 lsc 脚本,并将使用指定的命令行参数调用 lsc 函数。请注意,lsc 脚本中定义的支持函数可用于 lsc 函数。
提供了一个函数自动加载功能,其中自动加载的函数定义在首次引用函数名称时加载并保留在 ksh93 环境中。这提供了更好的性能,因为消除了后续引用的搜索和加载步骤。
David G. Korn AT&T Research,技术经理
Charles J. Northrup Global Technologies Ltd., Inc.,首席信息官
Jeffery Korn 普林斯顿大学,计算机科学系