级别: 中级 Martin Streicher, 软件开发人员, Pixels, Bytes, and Commas
2009 年 5 月 21 日 如果您不满足于特定的 shell 仅能在某个特殊平台上运行,那么可尝试使用 Squirrel Shell。Squirrel Shell 提供了一种高级的、面向对象的脚本语言,在 UNIX®、Linux®、Mac OS X™ 和 Windows® 系统上都可以良好地运行。只需要编写一次脚本,就可以在多个平台上运行。
1799 年,一名法国陆军工程师取得了一项重大发现。不,不是鹅肝酱、卡门培尔奶酪、巴氏消毒法或沙特(Sartre)— 实际上,他发现了能够破译埃及古代象形文字的钥匙 —— 罗塞塔石碑(参见 图 1)。
图 1. 罗塞塔石碑,1100 磅重,其上使用三国语言篆刻了税收策略。碑文展示的是减免僧侣税款的诏书。
这块石碑制作于公元前 196 年,篆刻了对同一段文字的三种不同语言版本 — 分别是象形文字、通俗体文字(埃及草书)和希腊文字。通过对照翻译,或在不同语言版本之间寻找对应的词汇,罗塞塔石碑解读出已经失传已久的象形文字的含义。
换句话说,将罗塞塔石碑想像成 Babelfish。即使在公元前 196 年,就出现了使用一种以上的语言进行表达。
公元 2000 年末,软件开发人员面对着一个相似的问题。有太多的语言和方法可以用来表达同一内容。即使对于命令行,也有许多类似的内容可供选择,包括各种 shell 和不同的命令组合。
通常来讲,多样性是件好事,但是它也会让人觉得害怕。应该选择哪种解决方案?这种技术是否能够跟上需求的变化?时间和精力方面的投入能否得到回报?这些编写良好的代码(或 Perl 代码)是否会过时?更糟糕的是,是否需要针对其他环境转换(重写)所有内容?
如果您不希望局限于 Fish shell、Bash shell、Z shell、Windows operating system 的 cmd.exe 或其他一些 shell 脚本语言的特性,那么请尝试使用 Squirrel Shell。Squirrel Shell 提供了一种高级的、面向对象的脚本语言,在 UNIX、Linux、Mac OS X 和 Windows 系统上都可以良好地运行。您只需要编写一次脚本,就可以在任意平台上运行。
更妙的是,您需要做的工作非常简单。
获得 Squirrel
根据 GNU Public License version 3 (GPLv3) 的条款,Squirrel Shell 很容易获得并且可以免费使用。最新的版本为 2008 年 10 月 11 日发布的 1.2.2。Squirrel Shell 的创建者和维护者是 Constantin "Dinosaur" Makshin。
Squirrel Shell 的下载页面(参见 参考资料)提供了针对 32 位和 64 位 Windows 的源代码和二进制代码。如果您使用 UNIX 或 Linux,请检查发行版附带的库,寻找合适的二进制文件或从头构建 Squirrel Shell。
从头构建 Squirrel Shell 非常简单。下载并提取源代码 tarball 文件,放到源代码目录,然后使用非常典型的构建 shell,如 清单 1 所示。
清单 1. 从头构建 Squirrel Shell
$ ./configure --with-pcre=system && make && sudo make install
Checking CPU architecture... x86
Checking for install... /usr/bin/install
...
Configuration has been completed successfully.
Build for x86 CPU architecture
Installation prefix: /usr/local
Allow debugging: no
Build static libraries
Use system PCRE 6.7 library
Install MIME information: auto
Create symbolic link: no
Compile C code with 'gcc'
Compile C++ code with 'g++'
Create static libraries with 'ar rc'
Create executables and shared libraries with 'g++'
Install files with 'install'
|
要查找与包有关的选项列表以进行配置,需在命令行中输入 ./configure --help。
为方便起见,Squirrel Shell 打包了 Perl Compatible Regular Expression (PCRE) 库的源代码,这些内容在程序中被大量使用。如果系统缺少 PCRE,打包后的代码可以使构建变得简单快捷。然而,如果系统已经有了 PCRE,那么可以通过指定 --with-pcre=system 选项来使用它。另一种方法是指定 --with-pcre=auto 以链接到更新的系统库或 Squirrel Shell 的副本。
构建的结果是得到一个新的二进制文件,名为 squirrelsh。假设此文件被安装到 PATH 变量的某个目录中,比如 /usr/local/bin,那么输入 squirrelsh 以启动该 shell。在命令行提示符下,输入命令 printl(getenv("HOME")); 以输出主目录的路径:
$ squirrelsh
> printl( getenv( "HOME" ) );
/home/strike
> exit();
|
Squirrel Shell 基于 Squirrel 编程语言(参见 参考资料 获得更多信息的链接)。该语言类似于 C++,并且提供了非常类似于 Python 和 Ruby 等面向对象脚本语言的特性。Squirrel Shell 纳入了 Squirrel 中的所有特性和数据类型,并添加了一些专门为常见 shell 脚本任务编写的新功能,比如复制文件和读取环境变量。
尽管 Squirrel Shell 的语法对于日常的命令行使用过于繁杂 —echo $HOME 是和 Squirrel Shell 的 printl( "~") 具有等效功能的 Bash 命令 — 但是它拥有出色的脚本。您只需要编写一次,就可以到处运行,而不需要针对 UNIX 和 Windows 分别编写。正如 Dinosaur 这样评价他的工作,“Squirrel Shell 主要是充当一个脚本翻译器”。
使用 Squirrel 编写脚本
让我们看一看一个 Squirrel Shell 脚本的示例。清单 2 展示了文件 listing2.nut,此脚本将递归地列出您的主目录的内容。
清单 2. listing2.nut
#!/usr/bin/env squirrelsh
function reveal( filedir ) {
if ( !exist( filedir ) ) {
return;
}
if ( filename( filedir ) == ".." || filename( filedir ) == "." ) {
return;
}
if ( filetype( filedir ) == FILE ) {
printl( filename( filedir, true ) );
return;
}
printl("directory: " + filename( filedir, true) );
local names = readdir( filedir );
foreach( index, name in names ) {
reveal( name );
}
}
local previous = getcwd();
chdir( "~" );
reveal( getcwd() );
chdir( previous );
exit( 0 );
|
按照规定,每个 shell 脚本的第一行将向操作系统表明要启动哪个程序来解释脚本。通常,这一行会显示 #! /usr/bin/bash 或 #! /bin/zsh 以从某个位置启动特定 shell 或解释器。
#!/usr/bin/env squirrelsh 有一些不同。它启动了一个特殊的程序 env,此程序又启动 PATH 变量中找到的第一个 squirrelsh 实例。因此,可以修改 PATH 变量以支持某个程序的本地版本 — 即您自己的、修改后的 squirrelsh 副本,位于 $HOME/bin/squirrelsh — 而不要修改 shell 脚本的内容。
注意:这个技巧适用于所有解释器。例如,#!/usr/bin/env ruby 将按照 PATH 设置的指示,调用您喜欢的 Ruby 版本。总之,如果计划发布所编写的任何 shell 脚本,在第一行中使用 #!/usr/bin/env application 表单,因为它的 “移植性” 更强:它将运行用户 在他/她的 PATH 变量中已经配置好的应用程序版本。
清单 2 的其余部分应该比较熟悉,至少对于方法是这样。函数 reveal() 是递归的:
- 如果为
reveal() 传递一个无效的路径或 “小圆点”(.,当前目录)或 “两个小圆点”(..,父目录),那么递归将结束。
- 否则,如果参数
filedir 是一个文件,代码将输出其名称并返回,并再一次停止进一步的递归。函数 filename() 可以接受一到两个参数。如果只有一个参数,或者第二个参数为 false,那么将忽略扩展文件名。如果提供 true 作为第二个参数,将返回完整的文件名。
- 如果参数是一个目录,代码将输出其名称,然后扫描内容(不需要执行深度优先处理,因为目录内容并没有按特定的顺序排列。下一个示例将改进输出)。
需要注意一点:由于对 reveal() 的调用是同一个函数中的最后一条语句,Squirrel 虚拟机(VM)— 运行脚本代码的引擎 — 可以通过称为尾递归(tail recursion)的技术将递归改为迭代。实际上,尾递归消除了对递归使用调用栈的需要;因此,可以实现任意深度的递归并且可以避免栈溢出。
Squirrel 的语法相当简单,因此使用这种语言编写代码非常快捷,特别是如果您曾经使用过 C、C++ 或任何更高级的语言编写过代码的话,这一点则体现得更充分。
最妙的是,这个 shell 代码是可移植的。将它转移到 Windows 机器上,在其上安装 Squirrel Shell,然后就可以运行您的代码。
改进表
与典型 shell 相比,Squirrel 的优秀特性之一就是它丰富的数据结构。如果数据可以进行良好地组织,那么即使是复杂的问题通常也能够快速得到解决。Squirrel 提供了真正的对象、异构数组和关联数组(在 Squirrel 中称为 表)。
一个 Squirrel 表由一些 slot 或 (键-值)对组成。除 Null 以外的任何值都可以充当一个键;任何值都可以被分配给一个 slot。您将使用 “箭头” 操作符创建一个新的 slot(<-)。
让我们对 清单 2 的代码稍加改进,在将目录转变为任何子目录之前展示它的内容。使用什么方法?使用一个本地表在单独的 slot 中存放文件和子目录,然后相应地处理两个类别。清单 3 展示了新的代码。
清单 3. 增强后的清单 2 将首先输出目录的内容,然后递归到子目录
#!/usr/bin/env squirrelsh
function reveal( filedir ) {
local tally = {};
tally[FILE] <- [];
tally[DIR] <- [];
if ( !exist( filedir ) ) {
return;
}
if ( filename( filedir ) == ".." || filename( filedir ) == "." ) {
return;
}
local names = readdir( filedir );
foreach( index, name in names ) {
tally[ filetype( name ) ].append( name ) ;
}
foreach( index, file in tally[FILE] ) {
printl( file );
}
foreach( index, dir in tally[DIR] ) {
printl( filename( dir ) + "/" );
}
foreach( index, dir in tally[DIR] ) {
reveal( dir );
}
}
local entries = readdir( (__argc >= 2) ? __argv[1] : "." );
exit( 0 );
|
在这里非常适合使用表这种数据结构。reveal() 中的表有两个 slot:一个用于文件,另一个用于目录。filetype( name ) 函数的返回值 — 常量 FILE 或常量 DIR — 将文件系统中的每一项整理到相应的 slot 中。
此外,每个 slot 是一个数组,由 tally[FILE] <- [] 和 tally[DIR] <- []; 这两条语句创建。([] 是一个空数组)。由于 tally 是函数内的本地变量,它将在每次调用时重新创建并清空范围,并且在每个调用被返回时自动销毁。
数组函数 append( arg ) 将 arg 添加到数组的末尾,从而在此过程中形成了一个列表。在执行完 foreach( index, name in names ) 循环后,所有项都被添加到这两个 slot 中其中一个的列表中。函数其余部分的代码将输出文件,接着输出目录,然后是递归。
当然,如果没有命令行参数的话,shell 脚本的价值就没有那么大了。特殊 Squirrel Shell 变量 __argc 和 __argv 分别以字符串数组形式包含命令行参数的计数和参数列表。根据约定,__argv[0] 始终都作为 shell 脚本的名称;因此,如果 __argc 的值至少为 2,那么将提供额外的参数。为了简单起见,这个脚本只处理第一个额外参数 argv[1]。
作为参考,清单 4 展示了一个 Ruby 脚本(作者为 Mr. Makshin),此脚本的功能与清单 3 相同。即使该脚本已像 Ruby 那样简洁,但它在简洁性方面仍然逊色于 Squirrel Shell 代码。
清单 4. 使用 Ruby 重新实现清单 3
!/usr/bin/ruby
# List directory contents.
path = ARGV[0] == nil ? "." : ARGV[0].dup
# Remove trailing slashes
while path =~ /\/$/
path.chop!
end
entries = Dir.open(path)
for entry in entries
unless entry == "." || entry == ".."
filePath = "#{path}/#{entry}"
fileStat = File.stat(filePath)
if fileStat.directory?
puts "dir : #{filePath}"
elsif fileStat.file?
puts "file: #{filePath}"
end
end
end
entries.close()
|
有关 Squirrel 语言的更多信息,请参阅 Squirrel Programming Language Reference(参见 参考资料 获得链接)。
巧妙的是,Squirrel Shell 中的几乎所有函数都去掉了底层操作系统的细节,因此您的代码可以尽可能保持通用。例如,filename() 函数(在前两个清单中使用)将引导路径(leading path)从文件路径名中分离 — 比如,将 /home/example/some/directory/file.txt 简化为 file.txt — 而不管您使用的是何种平台。类似地,readdir() 和 filetype() 允许您不必了解真实的、底层操作和文件系统的圈套和陷阱。通常,普通的 shell 并不能提供这种抽象(较为高级的脚本语言则可以)。
其他有用的、独立于平台的功能包括 convpath() 和 run(),前者可以将路径名转换成本地路径名格式,而后者可以调用另一个可执行文件。convpath() 函数可以执行双向转换,因此对于编写跨平台脚本非常有用。
正则表达式
Shell 脚本通常用于自动化系统管理和维护工作。实现这种自动化主要依靠正则表达式,它是用来查找、匹配和分解字符串的一组真正的象形文字。如前所述,Squirrel Shell 需要 PCRE 库,这种库在 Perl、PHP、Ruby 和其他许多解释器和程序中都可找到。PCRE 是用于数据处理的重要武器。
尽管非常完整,Squirrel Shell 的正则表达式实现有一些不同,可能会令您想起 PHP 实现。要在 Squirrel Shell 中使用正则表达式,需要先定义正则表达式,对其进行编译,进行比较,然后再迭代结果(如果有的话)。
清单 5 展示的示例程序演示了 Squirrel Shell 中的正则表达式(代码由 Mr. Makshin 编写并且得到使用许可)。
清单 5. 演示 Squirrel Shell 中的正则表达式
#!/usr/bin/env squirrelsh
// Match a regular expression against text
print("Text: ");
local text = scan();
print("Pattern: ");
local pattern = scan();
local re = regcompile(pattern);
if (!re)
{
printl("Failed to compile regular expression - " + regerror());
exit(1);
}
local matches = regmatch(re, text);
if (!matches)
{
printl("Failed to match regular expression - " + regerror());
regfree(re);
exit(1);
}
regfree(re);
printl("Matches found:");
foreach (match in matches)
printl("\t\"" + substr(text, match[0], match[1]) + "\"");
|
在这里,scan() 从标准输出中读取一些文本和一个模式,但是并不包含通常用于确定正则表达式的起始和结束部分的前斜杠(/)字符。
对于一个模式,函数 reqgcompile() 将编译此模式,这将提高匹配的速度。您可以对 reqgcompile() 函数使用一个标记以启用或禁用区分大小写的功能(等同于 PCRE /i 修饰符),并且可以使用另一个选项针对一行或多行进行匹配(等同于 PCRE /m 选项)。如果没有对正则表达式执行编译,那么所有匹配将失败。
regmatch(re, text) 函数将比较正则表达式和文本,如果没有匹配的话就生成 Null 值,否则生成一个由成对整数组成的数组(双元素数组)。每一对中的第一个整数表示匹配的开始;第二个整数表示匹配结束。这解释了最后一行代码中 substr(text, match[0], match[1]) 的使用。
执行完比较后,可以迭代结果。如果在任何时候不再需要编译后的正则表达式,则使用 regfree() 删除它。还有一个 regfreeall() 函数可以处理所有已编译表达式所持有的所有资源。
Squirrel Shell 的限制
在理想情况下,相同的编程逻辑将应用到 UNIX、Linux 和 Windows 中,并且效率至少和以前一样高,这样程序员会更加高兴。可惜操作系统各不相同,您经常需要为了某个特定系统而求助于定制代码。
在这些情况下,无论是 Squirrel Shell 还是您都无法脱离平台,Squirrel Shell 提供了一个方便的函数来探测操作系统,这样代码就可以适当的执行。
清单 6 展示了如何使用 platform() 函数作出决策。该函数始终返回一个值,但是该值可能是 unknown。
清单 6. platform() 函数生成操作系统类型
print( "Made by ... ");
local platform = platform();
switch ( platform ) {
case "linux":
printl( "Linus." );
break;
case "macintosh":
printl( "Steve." );
break;
case "win32":
case "win64":
printl( "Bill." );
break;
default:
printl( "Unknown" );
}
|
您可以通过 Squirrel Shell 环境变量 PLATFORM 查找当前平台的类型:
> printl( PLATFORM );
linux
|
环境变量 CPU_ARCH 生成处理器,shell 将针对该处理器进行编译:
> printl( CPU_ARCH );
x86
|
结束语
Squirrel Shell 的其他函数将管理文件、处理环境和执行策略。实际上,它的三角学内置函数就有 20 余种。Version 2.0 目前正在规划之中,并且将包含更多类、对 Unicode 的支持、改进的交互模式,以及一个模块化的插件架构。
Squirrel Shell 并不算得上一种交互式 shell,但是这没关系。在这方面已经出现了很多选择。作为一种脚本运行程序,Squirrel Shell 要比其同类出色许多。其数据结构要比传统 shell 更加强大,它的语法简单易懂,其底层虚拟引擎支持从枚举类型到线程等所有内容。Squirrel 引擎也很小巧,不超过 6000 行代码。您甚至可以将完整的 Squirrel 嵌入到另一个应用程序中。
当您需要为两个平台编写代码时,请尝试使用 Squirrel Shell!它使您能够轻松编写自己的代码。
参考资料 学习
获得产品和技术
讨论
关于作者  | 
|  | Martin Streicher 是一位 Ruby on Rails 自由开发人员和 Linux Magazine 的前主编。Martin 毕业于 Purdue University 并获得计算机科学学位,从 1986 年起他一直从事 UNIX 类系统的编程工作。他喜欢收集艺术品和玩具。 |
对本文的评价
|