对话 UNIX: Squirrel--可移植的 shell 和脚本语言

为多个平台编写面向对象的 shell 脚本

如果您不满足于特定的 shell 仅能在某个特殊平台上运行,那么可尝试使用 Squirrel Shell。Squirrel Shell 提供了一种高级的、面向对象的脚本语言,在 UNIX®、Linux®、Mac OS X™ 和 Windows® 系统上都可以良好地运行。只需要编写一次脚本,就可以在多个平台上运行。

Martin Streicher, 软件开发人员, Pixels, Bytes, and Commas

http://www.ibm.com/developerworks/i/p-mstreicher.jpgMartin Streicher 是一位 Ruby on Rails 自由开发人员和 Linux Magazine 的前主编。Martin 毕业于 Purdue University 并获得计算机科学学位,从 1986 年起他一直从事 UNIX 类系统的编程工作。他喜欢收集艺术品和玩具。



2009 年 5 月 21 日

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 的语法相当简单,因此使用这种语言编写代码非常快捷,特别是如果您曾经使用过 CC++ 或任何更高级的语言编写过代码的话,这一点则体现得更充分。

最妙的是,这个 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!它使您能够轻松编写自己的代码。

参考资料

学习

  • 查阅 Squirrel Shell
  • Squirrel Shell 基于 Squirrel 编程语言。您可以选择并阅读 Squirrel 编程语言参考 以了解 Squirrel 及其众多特性的更多信息。此外,还提供了有关 Squirrel Shell 函数的 手册
  • 一定要查看 PCRE
  • 对话 UNIX:查看这个系列的其他部分。
  • 了解更多有关 UNIX shells 的信息。
  • AIX and UNIX 专区:developerWorks 的“AIX and UNIX 专区”提供了大量与 AIX 系统管理的所有方面相关的信息,您可以利用它们来扩展自己的 UNIX 技能。
  • AIX and UNIX 新手入门:访问“AIX and UNIX 新手入门”页面可了解更多关于 AIX 和 UNIX 的内容。
  • AIX and UNIX 专题汇总:AIX and UNIX 专区已经为您推出了很多的技术专题,为您总结了很多热门的知识点。我们在后面还会继续推出很多相关的热门专题给您,为了方便您的访问,我们在这里为您把本专区的所有专题进行汇总,让您更方便的找到您需要的内容。
  • 浏览 技术书店,查找有关这个主题和其他技术主题的图书。

获得产品和技术

  • 可以免费 下载 Squirrel Shell。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=AIX and UNIX, Linux
ArticleID=390819
ArticleTitle=对话 UNIX: Squirrel--可移植的 shell 和脚本语言
publish-date=05212009