计算机擅长计算,该含义比看起来深奥。如果只需坐在计算机前,并在需要时使用CPU周期和引用RAM,生活就太轻松了。
然而,只独自思考的计算机没多大用途,用户迟早需要输入信息并让计算机输出信息,这使生活变得复杂起来。
几个因素导致I/O非常复杂。首先,输入和输出是两码事,但被视为一个整体。其次,I/O操作(及其用法)变化多端,就像昆虫物种一样。
历史上使用过各种设备,如圆桶、纸带、磁带、打孔卡和电传打字机等。有些是机械设备,其他的是电磁设备;有些是只读的,有些是只写或可读写的;有些可写介质是可擦除的,其他的则不能。有些设备是顺序存取的,有些是随机存取的;有些介质是永久性的,其他的是暂时或易失的;有些设备需要人工干预,其他的不需要;有些是面向字符的,其他的是面向块的;有些块设备是长度固定的,其他的则是长度可变的;有些设备被轮询,其他的是中断驱动的。中断可通过硬件、软件或两者来实现。有缓冲I/O和非缓冲I/O。有内存映射的I/O和面向通道的I/O,而随着UNIX等操作系统的出现,I/O设备被映射到文件系统中的文件。可用机器语言、汇编语言或高级语言处理I/O。有些语言提供了I/O功能,而有些语言的规范根本没有涉及I/O。执行I/O操作时,可使用设备驱动程序和抽像层,也可以不使用。
如果这些显得混乱,那是因为它们原本就如此。这种复杂性部分源于输入/输出概念固有的特征,部分源于设计时做出的权衡,部分源于计算机科学的传统和惯例及各种语言和操作系统的独特性。
由于I/O通常很复杂,因此Ruby中的I/O也很复杂,但本章将尽量使其容易理解,并概述各种技术的用法以及在什么情况下使用它们。
IO类是所有Ruby I/O的核心,它定义了每种输入/输出操作的行为。和IO密切相关(并继承它)的是File类,File内嵌了一个Stat类,后者封装了程序员可能需要查看的有关文件的各种细节(如权限和时间戳)。方法stat和lstat返回类型为File::Stat的对象。
模块FileTest也提供了可用于检测这些属性的方法,该模块被混合插入到File类中,它也可独立使用。
最后,Kernel模块也有I/O方法,该模块被混合插入到Object(它是包括类在内的所有对象的祖先)中。程序员总是可使用这些简单的I/O例程,而不用考虑它们的接受方是谁。这些方法默认使用标准输入和标准输出。
初学者可能发现这些类很乱,它们的功能重叠。好消息是,通常只需使用该框架的一小部分。
在更高层次上,Ruby提供了持久化对象的特性。使用Marshal可执行简单的对象序列化,更复杂的PStore库是基于Marshal的,将在同一节中介绍它们和DBM库,虽然DBM库是基于字符串的。
在最高层次上,可通过独立的数据库管理系统(如MySQL和Oracle)来访问数据。这个主题很复杂,足够编写一本或多本专著。本章只概述让程序员入门的知识,在有些情况下,作者将提供在线文档链接。
10.1 处理文件和目录
文件通常指的是磁盘文件,虽然并非总是如此。和其他编程语言中一样,Ruby中的文件概念是一种有意义的抽像,目录指的是Windows或UNIX系统中的目录。
File类同其父类IO紧密相关,Dir类和IO类的关系不那么紧密,但作者将文件和目录放在一起讨论,因为它们在概念上还是相关的。
10.1.1 打开和关闭文件
类方法File.new实例化一个File对象并打开该文件。第一个参数自然是文件名。
可选的第二个参数被称为模式字符串(mode string),它指定如何打开文件(用于读取、写入等,模式字符串和权限模式毫无关系),其默认值为“r”,表示用于读取。下列代码演示了如何打开文件用于读取和写入。
file1 = File.new("one") # Open for reading
file2 = File.new("two", "w") # Open for writing
new的另一个形式接受三个参数。在这种情况下,第二个参数指定文件的原始权限(通常是八进制常量),而第三个参数是一组用OR连接的标记。标记是常量,如File::CREAT(如果要打开的文件不存在,则创建它)和File::RDONLY(以只读模式打开)。这种形式很少使用。
file = File.new("three", 0755, File::CREAT|File::WRONLY)
作为对操作系统和运行环境的一种尊重,总是应关闭打开的文件。以写入模式打开文件时,这不仅是出于礼貌,也可防止数据丢失。close方法用于关闭文件:
out = File.new("captains.log", "w")
# Process as needed...
out.close
还有open方法,其最简单形式是new的同义词,如下所示:
trans = File.open("transactions","w")
但open还可以接受代码块作为参数,这是一种更有趣的形式。指定了代码块时,打开的文件将作为参数传送给代码块。文件在代码块的作用域内始终处于打开状态,并在代码块执行完毕时自动关闭。例如:
File.open("somefile","w") do |file|
file.puts "Line 1"
file.puts "Line 2"
file.puts "Third and final line"
end
# The file is now closed
显然,这是一种确保文件用完后被关闭的优美方式。另外,处理文件的代码看起来是一个整体。
10.1.2 更新文件
要以读写模式打开文件,只需在打开文件时在文件模式后面加上加号(+)(请参阅第10.1.1节):
f1 = File.new("file1", "r+")
# Read/write, starting at beginning of file.
f2 = File.new("file2", "w+")
# Read/write; truncate existing file or create a new one.
f3 = File.new("file3", "a+")
# Read/write; start at end of existing file or create a
# new one.
10.1.3 文件的追加
要追加信息到已有的文件中,只需在打开文件时将文件模式设置为“a”(请参阅第10.1.1节):
logfile = File.open("captains_log", "a")
# Add a line at the end, then close.
logfile.puts "Stardate 47824.1: Our show has been canceled."
logfile.close
10.1.4 随机存取文件
要随机而不是顺序读取文件,可使用seek方法,它是File类从IO类那里继承而来的。最简单的用法是定位到特定的字节位置。位置是相对于文件开头的,第一个字节的编号为0。
# myfile contains only: abcdefghi
file = File.new("myfile")
file.seek(5)
str = file.gets # "fghi"
如果确定每行的长度是固定的,可以定位到特定行,如下例所示:
# Assume 20 bytes per line.
# Line N starts at byte (N-1)*20
file = File.new("fixedlines")
file.seek(5*20) # Sixth line!
# Elegance is left as an exercise.
如果要进行相对定位,可使用第二个参数,常量IO::SEEK_CUR假设偏移量是相对当前位置的(偏移量可以是负数)。
file = File.new("somefile")
file.seek(55) # Position is 55
file.seek(-22, IO::SEEK_CUR) # Position is 33
file.seek(47, IO::SEEK_CUR) # Position is 80
也可以相对于文件末尾进行定位,但这种情况下只有负偏移量才有意义:
file.seek(-20, IO::SEEK_END) # twenty bytes from eof
还有第三个常量IO::SEEK_SET,但它是默认值(相对于文件开头进行定位)。
方法tell指出所处的文件位置,pos是其别名:
file.seek(20)
pos1 = file.tell # 20
file.seek(50, IO::SEEK_CUR)
pos2 = file.pos # 70
还可使用rewind方法将文件指针重置到文件开头,该术语来自磁带的用法。
对文件进行随机存取时,可能要以更新(读写)模式打开它,通过在模式字符串中指定加号(+)可更新文件,请参阅第10.1.2节。
10.1.5 处理二进制文件
在以前,C语言程序员在打开文件时在模式字符串末尾添加字符b,以便将文件作为二进制文件打开(早期的UNIX版本也是如此,这和很多人想的不同)。在很多情况下,出于兼容性考虑仍允许使用该字符,但现今的二进制文件不像以前那么难以处理。用Ruby字符串很容易存储二进制数据,因此不需要以特殊方式读取文件。
一种例外是Windows操作系统,在这些操作系统中这种差别仍存在。在这些平台中,二进制和文本文件的主要区别是,在二进制模式下,行尾(end-of-line)没有转换为单个换行符,而仍然是回车—换行对。
另一个重要的区别是,如果文件不是以二进制模式打开,control-Z将被视为文件尾,如下所示:
# Create a file (in binary mode)
File.open("myfile","wb") {|f| f.syswrite("12345\0326789\r") }
# Above note the embedded octal 032 (^Z)
# Read it as binary
str = nil
File.open("myfile","rb") {|f| str = f.sysread(15) }
puts str.size # 11
# Read it as text
str = nil
File.open("myfile","r") {|f| str = f.sysread(15) }
puts str.size # 5
下面的代码段表明,在Windows的二进制模式下,回车没有被转换:
# Input file contains a single line: Line 1.
file = File.open("data")
line = file.readline # "Line 1.\n"
puts "#{line.size} characters." # 8 characters
file.close
file = File.open("data","rb")
line = file.readline # "Line 1.\r\n"
puts "#{line.size} characters." # 9 characters
file.close
注意binmode方法将流切换到二进制模式,如下面的代码示例所示。切换后,就不能再切换回来。
file = File.open("data")
file.binmode
line = file.readline # "Line 1.\r\n"
puts "#{line.size} characters." # 9 characters
file.close
如果确实要执行低级输入/输出,可使用方法sysread和syswrite,前者接受字节数作为参数,后者接受字符串作为参数并返回实际写入的字节数(不应使用其他方法来读取上述流,否则结果可能无法预料)。
input = File.new("infile")
output = File.new("outfile")
instr = input.sysread(10);
bytes = output.syswrite("This is a test.")
注意,如果在文件末尾调用sysread,将引发EOFError(但如果在成功读取后到达文件尾,则不会)。发生错误时,这两个方法都将引发SystemCallError。
注意,处理二进制数据时,Array的方法pack和String的方法unpack很有用。
10.1.6 锁定文件
在支持这种功能的操作系统中,File类的flock方法可对文件加锁或解锁。第二个参数是下列常量之一:File::LOCK_EX、File::LOCK_NB、File::LOCK_SH、File::LOCK_UN,也可以是这些常量中两个或多个的逻辑或。当然,很多组合是没有意义的,非阻断标记是最常用的标记之一。
file = File.new("somefile")
file.flock(File::LOCK_EX) # Lock exclusively; no other process
# may use this file.
file.flock(File::LOCK_UN) # Now unlock it.
file.flock(File::LOCK_SH) # Lock file with a shared lock (other
# processes may do the same).
file.flock(File::LOCK_UN) # Now unlock it.
locked = file.flock(File::LOCK_EX | File::LOCK_NB)
# Try to lock the file, but don't block if we can't; in that case,
# locked will be false.
Windows系列操作系统不支持这种功能。
10.1.7 执行简单的I/O操作
读者已经熟悉了Kernel模块中的一些I/O方法,总是可以调用它们而不指定接受方。如gets和puts的调用就源自这里,其他的包括prints、printf和p(它调用对象的inspect方法,将对象以人类可读的方式显示出来)。
出于完整性考虑,还有其他一些方法需要介绍。putc方法输出单个字符(相应的方法getc由于技术原因没有在Kernel中实现,但每个IO对象都有该方法)。如果指定了一个String对象,putc方法使用该字符串的第一个字符。
putc(?\n) # Output a newline
putc("X") # Output the letter X
问题是,使用这些方法时如果没有指定接受方,将输出到哪里。首先,Ruby环境定义了三个常量,分别对应于UNIX中熟悉的三个I/O流。它们是STDIN、STDOUT和STDERR,都是IO类型的全局常量。
还有全局变量$stdout,它是所有Kernel方法的输出目的地。该变量被(间接地)初始化为STDOUT的值,因此,所有这些输出都写入到标准输出。随时可对变量$stdout重新赋值,使其指向其他IO对象。
diskfile = File.new("foofile","w")
puts "Hello..." # prints to stdout
$stdout = diskfile
puts "Goodbye!" # prints to "foofile"
diskfile.close
$stdout = STDOUT # reassign to default
puts "That's all." # prints to stdout
除gets外,Kernel还有用于输入的方法readline和readlines,前者和gets基本相同,但在遇到文件末尾时引发EOFError,而不是返回nil值。后者与IO.readlines方法等价(即将整个文件读入内存)。
输入从哪里来呢?有标准输入流$stdin,其默认值是STDIN。同样,也有标准错误流$stderr,其默认值为STDERR。
还有一个有趣的全局对象——ARGF,它表示在命令行中指定的所有文件的拼接。虽然它像File对象,但并不是真正的File对象。如果在命令行中指定了文件,默认输入将连接到该对象。
# Read all files, then output again
puts ARGF.read
# Or more memory-efficient:
while ! ARGF.eof?
puts ARGF.readline
end
# Example: ruby cat.rb file1 file2 file3
从标准输入(STDIN)读取将绕过Kernel的方法。可绕过(或不绕过)ARGF的方法,如下所示:
# Read a line from standard input
str1 = STDIN.gets
# Read a line from ARGF
str2 = ARGF.gets
# Now read again from standard input
str3 = STDIN.gets
10.1.8 执行缓冲I/O和非缓冲I/O操作
在有些情况下,Ruby在内部执行缓冲。请看下面的代码段:
print "Hello... "
sleep 10
print "Goodbye!\n"
如果运行该代码段,将在休眠后同时显示hello和goodbye消息,且第一个消息后没有换行。
这个问题可通过调用flush来解决,它将输出缓冲清空。这里使用$defout流(所有Kernel方法的默认输出流)作为接受方,这样结果将像期望的那样,第一条消息比第二条消息更早些显示。
print "Hello... "
STDOUT.flush
sleep 10
print "Goodbye!\n"
可使用sync=方法关闭(或启用)这种缓冲功能,sync方法可用于获悉缓冲状态。
buf_flag = $defout.sync # true
STDOUT.sync = false
buf_flag = STDOUT.sync # false
后台至少还有一个更低级的缓冲。getc方法返回一个字符并移动文件或流指针,而ungetc将一个字符推回到流中。
ch = mystream.getc # ?A
mystream.ungetc(?C)
ch = mystream.getc # ?C
这里有三点要注意。首先,这里所说的缓冲和本节前面提到的缓冲无关,换句话说,sync=false不会关闭它。其次,只能推回一个字符,如果试图推回多个,实际上将只有最后一个字符被推回输入流。最后,ungetc方法不适用于本质上是非缓冲的读取操作(如sysread)。
10.1.9 操作文件所有者和权限
文件所有者和权限在很大程度上是与平台相关的。典型的UNIX提供这种功能的超集;而在其他的平台中,很多特性可能没有实现。
File::Stat有两个实例方法uid和gid,分别用于确定文件的所有者和所属的组(这些用整数表示),如下所示:
data = File.stat("somefile")
owner_id = data.uid
group_id = data.gid
类File::Stat有一个实例方法mode,该方法返回文件的模式(权限)。
perms = File.stat("somefile").mode
File有类方法和实例方法chown,用于修改文件的所有者和组ID。这个类方法可接受任意个文件名作为参数,对于不想修改的ID,可将相应的参数设置为nil或−1。
uid = 201
gid = 10
File.chown(uid, gid, "alpha", "beta")
f1 = File.new("delta")
f1.chown(uid, gid)
f2 = File.new("gamma")
f2.chown(nil, gid) # Keep original owner id
同样,权限也可使用chmod(它也被同时实现为类方法和实例方法)来修改。传统上,权限用八进制来表示,但并非必须这样。
File.chmod(0644, "epsilon", "theta")
f = File.new("eta")
f.chmod(0444)
进程通常要以某个用户(可能是root)的身份来运行,因此有一个用户id与进程相关联(这里说的是有效用户ID)。经常需要知道用户是否有权读、写或执行给定的文件,为此可使用File::Stat中的下述实例方法。
info = File.stat("/tmp/secrets")
rflag = info.readable?
wflag = info.writable?
xflag = info.executable?
有时需要区分有效用户ID和实际用户ID,相应的实例方法分别是readable_real?、writable_real?和executable_real?。
info = File.stat("/tmp/secrets")
rflag2 = info.readable_real?
wflag2 = info.writable_real?
xflag2 = info.executable_real?
可以通过比较当前进程的有效用户ID(和组ID)来测试文件的所有权。类File::Stat提供了实例方法owned?和grpowned?来实现这种功能。
注意,这些方法中有很多在模块FileTest中也有:
rflag = FileTest::readable?("pentagon_files")
# Other methods are: writable? executable? readable_real? writable_real?
# executable_real? owned? grpowned?
# Not found here: uid gid mode
和进程相关联的umask决定了新建文件的初始权限。将标准模式0777与umask的非进行逻辑AND运算,这样,在umask中被设置的位将被“屏蔽”或清除。也可将此视为简单减法(不借位)。这样,umask 022将导致新建文件的模式为0755。
可以通过类File的类方法umask来获取或设置umask。如果指定了参数,umask将被设置为该参数的值(并返回原来的值)。
File.umask(0237) # Set the umask
current_umask = File.umask # 0237
严格地说,有些文件模式位(如sticky bit)与权限无关,有关这方面的讨论,请参阅第10.1.12节。
10.1.10 获取和设置时间戳信息
每个磁盘文件都有多个与之相关联的时间戳(但在不同操作系统中略有不同),Ruby能够理解的三种时间戳是修改时间(最后一次修改文件内容的时间)、访问时间(最后一次读取文件的时间)和改变时间(最后一次改变文件目录信息的时间)。
这三种信息可以通过三种不同的方式访问,这三种方式的结果相同。
File的类方法mtime、atime、ctime在不打开文件或实例化任何File对象的情况下返回这些时间。
t1 = File.mtime("somefile")
# Thu Jan 04 09:03:10 GMT-6:00 2001
t2 = File.atime("somefile")
# Tue Jan 09 10:03:34 GMT-6:00 2001
t3 = File.ctime("somefile")
# Sun Nov 26 23:48:32 GMT-6:00 2000
如果正好创建了File实例,可使用其实例方法。
myfile = File.new("somefile")
t1 = myfile.mtime
t2 = myfile.atime
t3 = myfile.ctime
如果正好创建了File::Stat实例,也可使用其实例方法:
myfile = File.new("somefile")
info = myfile.stat
t1 = info.mtime
t2 = info.atime
t3 = info.ctime
注意,File::Stat是由File的类(或实例)方法stat返回的。类方法lstat(或同名的实例方法)有相同的作用,但它返回链接本身的状态,而不沿链接找到实际文件。如果链接指向链接,将跟踪到倒数第二个链接。
文件访问时间和修改时间可使用utime方法来调整,它可改变指定的一个或多个文件的时间。指定时间时可使用Time对象或从纪元开始的秒数。
today = Time.now
yesterday = today - 86400
File.utime(today, today, "alpha")
File.utime(today, yesterday, "beta", "gamma")
由于它同时改变两个时间,因此如果要保持其中一个不变,必须首先将其保存。
mtime = File.mtime("delta")
File.utime(Time.now, mtime, "delta")
10.1.11 检查文件是否存在及其大小
有时需要知道特定名称的文件是否存在,FileTest模块中的exist?方法可进行这样的检查:
flag = FileTest::exist?("LochNessMonster")
flag = FileTest::exists?("UFO")
# exists? is a synonym for exist?
显然,这样的方法不能是File的类实例,因为实例化该对象时,文件已经打开。有人可能认为File应该有类方法exist?,但实际上没有。
和文件是否存在相关的问题是文件是否包含内容。毕竟,文件可能存在但长度为零(这与文件不存在差不多)。
如果只关心文件是否有内容,File::Stat提供的两个实例方法很有用。如果文件长度为零,方法zero?将返回true,否则返回false:
flag = File.new("somefile").stat.zero?
相反,如果文件的长度不为零,方法size?返回文件的大小,否则返回nil。返回nil而不是0的原因并非显而易见,答案是这个方法主要用做断言(predicate),而在Ruby中0是true,而nil才是false。
if File.new("myfile").stat.size?
puts "The file has contents."
else
puts "The file is empty."
end
FileTest模块中也有方法zero?和size?:
flag1 = FileTest::zero?("file1")
flag2 = FileTest::size?("file2")
这自然会引出“文件有多大”的问题。正如读者在前面看到的,当文件不为空时,size?返回长度;但如果不将其用做断言,nil值可能令人感到困惑。
File类有一个类方法(但不是实例方法)可回答这个问题,它的同名实例方法是从IO类继承来的,File::Stat也有相应的实例方法。
size1 = File.size("file1")
size2 = File.stat("file2").size
如果要得到以块数而不是字节数表示的文件长度,可使用File::Stat的实例方法blocks。这显然和操作系统相关(方法blksize返回在当前操作系统中块是多大的)。
info = File.stat("somefile")
total_bytes = info.blocks * info.blksize
10.1.12 检查特殊的文件属性
文件有很多可以测试的方面,这里将总结在其他地方没有讨论过的相关内置方法,其中大部分是断言。
在本节(及本章的大部分)请记住两点。首先,由于File混合插入了FileTest,因此如果测试可通过调用该模块名限定的方法来进行,则也可通过调用File对象的实例方法进行。其次,FileTest模块和由stat(或lstat)返回的File::Stat在功能上有很大的重叠。在有些情况下,有三种调用相同方法的不同方式,作者不会每次都说明这一点。
有些操作系统有面向块设备的概念,与之对应的是面向字符的设备,每个文件只能属于其中的一种。FileTest模块中的方法blockdev?和chardev?用于测试这一点:
flag1 = FileTest::chardev?("/dev/hdisk0") # false
flag2 = FileTest::blockdev?("/dev/hdisk0") # true
有时需要知道流是否与终端相关联,IO的类方法tty?可测试这一点(同义词isatty也可以):
flag1 = STDIN.tty? # true
flag2 = File.new("diskfile").isatty # false
流可以是管道或套接字(socket),FileTest提供相应的方法测试这些情况:
flag1 = FileTest::pipe?(myfile)
flag2 = FileTest::socket?(myfile)
前面说过,目录实际上是一种特殊的文件,如果需要区分目录和普通文件,可使用File的两个方法:
file1 = File.new("/tmp")
file2 = File.new("/tmp/myfile")
test1 = file1.directory? # true
test2 = file1.file? # false
test3 = file2.directory? # false
test4 = file2.file? # true
File还有一个类方法ftype,它指出流的类型,File::Stat类也有这样的实例方法。该方法返回一个字符串,其可能取值如下:file、directory、blockSpecial、characterSpecial、fifo、link或socket(字符串fifo表示管道)。
this_kind = File.ftype("/dev/hdisk0") # "blockSpecial"
that_kind = File.new("/tmp").stat.ftype # "directory"
可设置或清除文件权限中的一些特定位,严格地说,这些位和第10.1.9节讨论的其他位无关。这些位是set-group-id(设置组ID)位、set-user-id(设置用户ID)位和sticky(粘性)位,FileTest提供了对它们进行操作的方法。
file = File.new("somefile")
info = file.stat
sticky_flag = info.sticky?
setgid_flag = info.setgid?
setuid_flag = info.setuid?
磁盘文件可能有指向它的符号链接或硬链接(在支持这些功能的操作系统中)。要测试文件实际上是否为到其他文件的符号链接,可使用FileTest的symlink?方法。要计算与文件相关联的硬链接数量,可使用nlink方法(只有File::Stat类有该方法)。硬链接和普通文件几乎无法区分,实际上,它是一个普通文件,只不过有多个名称且存在于多个目录中。
File.symlink("yourfile","myfile") # Make a link
is_sym = FileTest::symlink?("myfile") # true
hard_count = File.new("myfile").stat.nlink # 0
注意,上述例子使用File的类方法symlink创建了一个符号链接。
有时可能需要有关文件的低级信息——这种情况很少。File::Stat类提供三个实例方法可提供原始细节:dev方法返回一个指出文件所在设备的整数,rdev返回的整数指出了设备的类型,而对于磁盘文件,ino指出了文件的起始inode号。
file = File.new("diskfile")
info = file.stat
device = info.dev
devtype = info.rdev
inode = info.ino
10.1.13 使用管道
Ruby中有很多种用于读写管道的方式。类方法IO.popen打开管道,并将进程的标准输入和标准输出插入到返回的IO对象中。经常需要使用不同的线程来处理管道的两端,下面使用同一个线程先写后读:
check = IO.popen("spell","r+")
check.puts("'T was brillig, and the slithy toves")
check.puts("Did gyre and gimble in the wabe.")
check.close_write
list = check.readlines
list.collect! { |x| x.chomp }
# list is now %w[brillig gimble gyre slithy toves wabe]
注意,必须调用close_write,如果没有运行它,读取管道时将无法到达文件末尾。
代码块形式的工作原理如下:
File.popen("/usr/games/fortune") do |pipe|
quote = pipe.gets
puts quote
# On a clean disk, you can seek forever. - Thomas Steel
end
如果指定了字符串“-”,将启动一个新的Ruby实例。如果同时指定了代码块,代码块将作为两个独立的进程运行,而不像fork那样运行。子进程获得传递给代码块的nil,而父进程获得一个IO对象,子进程的标准输入输出连接到该对象。
IO.popen("-") do |mypipe|
if mypipe
puts "I'm the parent: pid = #{Process.pid}"
listen = mypipe.gets
puts listen
else
puts "I'm the child: pid = #{Process.pid}"
end
end
# Prints:
# I'm the parent: pid = 10580
# I'm the child: pid = 10582
pipe方法也返回互相连接的两个管道端。在下面的代码示例中,创建了两个线程,其中一个线程将一条消息传递给另一个线程(Samuel Morse通过电报发送的第一条信息)。如果读者对此有疑问,请参阅第13章。
pipe = IO.pipe
reader = pipe[0]
writer = pipe[1]
str = nil
thread1 = Thread.new(reader,writer) do |reader,writer|
# writer.close_write
str = reader.gets
reader.close
end
thread2 = Thread.new(reader,writer) do |reader,writer|
# reader.close_read
writer.puts("What hath God wrought?")
writer.close
end
thread1.join
thread2.join
puts str # What hath God wrought?
10.1.14 执行特殊的I/O操作
在Ruby中可执行低级I/O。这里只指出有这样的方法,如果要使用它们,请注意其中有些方法是和机器高度相关的(甚至随不同的UNIX版本而异)。
ioctl方法接受两个参数,第一个是整数,指定要执行的操作,第二个可以是整数或表示二进制数的字符串。
fcntl方法也依赖于系统,它对面向文件的流执行低级控制,其参数和ioctl相同。
select方法(在Kernel模块中)最多可接受四个参数:第一个是read-array(读取数组),其他三个(write-array、error-array和timeout值)是可选的。如果可从read-array中的一个或多个设备中获得输入,或write-array中的一个或多个设备可写,该方法将返回一个包含三个元素的数组,表示可进行I/O操作的各种设备。
Kernel方法syscall至少接受一个整数参数(最多可接受9个字符串或整数参数),其中第一个参数指定要执行的I/O操作。
fileno方法返回一个与I/O流相关联的老式文件描述符。在这里提及的所有方法中,它对系统的依赖程度最小。
desc = $stderr.fileno # 2
10.1.15 使用非阻断I/O
Ruby在后台执行协商以确保I/O不会阻断,因此在大多数情况下,可使用Ruby线程来管理I/O:当一个线程因I/O操作阻断时,另一个线程可以继续处理。
这不太好理解。Ruby线程都在同一个进程中,因为它们不是系统本地(native)线程。读者可能认为,阻断型I/O操作将阻断整个进程及所有与之相关联的线程。这种情况不会发生,原因是Ruby以一种对程序员透明的方式小心地管理其I/O。
然而,如果想关闭非阻断I/O,也可以做到。小型库io/nonblock为IO对象提供了一个简单setter、一个查询方法和一个面向块的setter。
require 'io/nonblock'
# ...
test = mysock.nonblock? # false
mysock.nonblock = true # turn off blocking
# ...
mysock.nonblock = false # turn on again
mysock.nonblock { some_operation(mysock) }
# Perform some_operation with nonblocking set to true
mysock.nonblock(false) { other_operation(mysock) }
# Perform other_operation with non-blocking set to false
10.1.16 使用readpartial
readpartial是个较新的方法,旨在使某些情况下的I/O更简单。它被用于像套接字这样的流。
readpartial要求提供“最大长度”参数,如果指定了缓冲参数,该参数应指向一个将用于存储数据的字符串。
data = sock.readpartial(128) # Read at most 128 bytes
readpartial方法不受非阻断标记的限制。它有时会阻断,但仅当下列三个条件为真时才这样做:IO对象的缓冲区是空的;流的内容为空;流还没有达到文件末尾。
因此,如果流中还有数据,readpartial将不会阻断。它读取指定的最大字节数,但如果没有这么多字节,它将只读取这些字节并继续运行。
如果流没有数据,且已到达文件末尾,readpartial将立刻引发EOFError。
如果调用阻断了,它将等待,直到收到数据或检测到EOF条件。如果收到数据,它将返回它们;如果检测到EOF,它将引发EOFError。
在阻断模式下调用sysread时,sysread的行为与readpartial类似。如果缓冲区是空的,两者的行为将完全相同。
10.1.17 操作路径名
操作路径名时,首先要了解的是类方法File.dirname和File.basename,它们的行为与UNIX中的同名命令相似,分别返回目录名和文件名。如果在basename的第二个参数中指定了扩展名,该扩展名将被删除。
str = "/home/dave/podbay.rb"
dir = File.dirname(str) # "/home/dave"
file1 = File.basename(str) # "podbay.rb"
file2 = File.basename(str,".rb") # "podbay"
虽然是File的方法,但它们实际上只执行字符串操作。
类似的方法还有File.split,返回一个包含两个元素的数组,其中的元素为这两个组成部分(目录和文件名)。
info = File.split(str) # ["/home/dave","podbay.rb"]
类方法expand_path将相对路径名转换为绝对路径名,如果操作系统能够理解~和~user等,这些内容也将被扩展。
Dir.chdir("/home/poole/personal/docs")
abs = File.expand_path("../../misc") # "/home/poole/misc"
对于已打开的文件,path实例方法返回打开该文件时使用的路径名。
file = File.new("../../foobar")
name = file.path # "../../foobar"
常量File::Separator指定用于分隔路径名组成部分的字符(通常在Windows中是反斜杠,在UNIX中是斜杠),它的一个别名是File::SEPARATOR。
类方法join使用该分隔符根据一个目录列表生成路径:
path = File.join("usr","local","bin","someprog")
# path is "usr/local/bin/someprog"
# Note that it doesn't put a separator on the front!
不要错误地认为File.join和File.split是逆操作,它们不是。
10.1.18 使用Pathname类
程序员还应知道标准库pathname,它提供了Pathname类。这个类实质上是Dir、File、FileTest和FileUtils的包装类,它以更合理、更直观的方式将这些功能放在一起。
path = Pathname.new("/home/hal")
file = Pathname.new("file.txt")
p2 = path + file
path.directory? # true
path.file? # false
p2.directory? # false
p2.file? # true
parts = path2.split # [Pathname:/home/hal, Pathname:file.txt]
ext = path2.extname # .txt
还有很多方便的方法。root?方法检测路径指的是否是根目录,它可能“受骗”,因为它只分析字符串而不访问文件系统。parent?方法返回父目录的路径名,children方法以列表方式返回当前路径的下一级子目录,其中包括文件和目录,但不进行递归。
p1 = Pathname.new("//") # odd but legal
p1.root? # true
p2 = Pathname.new("/home/poole")
p3 = p2.parent # Pathname:/home
items = p2.children # array of Pathnames (all files and
# dirs immediately under poole)
relative和absolute判断路径是否是相对的(通过检查开头是否有斜杠):
p1 = Pathname.new("/home/dave")
p1.absolute? # true
p1.relative? # false
很多方法(如size、unlink等)实际上被委托给File、FileTest和FileUtils,因为没必要重复实现这些功能。
有关Pathname的更详细信息,请参阅ruby-doc.org或其他优秀文献。
10.1.19 命令级文件操作
经常需要以类似于命令行的方式操作文件,即需要复制、删除、重命名等。
在实现这些功能的方法中,有很多是内置方法,还有几个在fileutils库的FileUtils模块中。注意,FileUtils以前用于通过重新打开File类将功能直接混合插入到其中,现在这些方法留在独立的模块中。
要删除文件,可使用File.delete或其同义词File.unlink:
File.delete("history")
File.unlink("toast")
要重命名文件,可使用File.rename,如下所示:
File.rename("Ceylon","SriLanka")
文件链接(硬链接和符号链接)可能分别使用File.link和File.symlink来创建:
File.link("/etc/hosts","/etc/hostfile") # hard link
File.symlink("/etc/hosts","/tmp/hosts") # symbolic link
通过使用实例方法truncate,可将文件截短为零个字节(或任何其他指定的字节数):
File.truncate("myfile",1000) # Now at most 1000 bytes
通过方法compare_file可比较两个文件,它有个别名cmp(还有compare_stream)。
require "fileutils"
same = FileUtils.compare_file("alpha","beta") # true
方法copy复制文件为新名称或复制到新位置。它有一个可选的标记参数,用于将错误消息写入到标准错误中,UNIX风格的名称cp是它的一个别名。
require "fileutils"
# Copy epsilon to theta and log any errors.
FileUtils.copy("epsilon","theta", true)
使用move方法(别名为mv)可移动文件,和copy一样,它也有可选的verbose标记。
require "fileutils"
FileUtils.move("/tmp/names","/etc") # Move to new directory
FileUtils.move("colours","colors") # Just a rename
方法safe_unlink删除指定的一个或多个文件,它首先使文件可写以避免错误。如果最后一个参数为true或false,该值将用做verbose标记。
require "fileutils"
FileUtils.safe_unlink("alpha","beta","gamma")
# Log errors on the next two files
FileUtils.safe_unlink("delta","epsilon",true)
最后,方法install实际上是执行syscopy,但它首先检查文件是否存在或有不同的内容。
require "fileutils"
FileUtils.install("foo.so","/usr/lib")
# Existing foo.so will not be overwritten
# if it is the same as the new one.
有关FileUtils的更详细信息,请参阅ruby-doc.org或其他参考文献。
10.1.20 从键盘抓取字符
这里之所以使用术语抓取(grab),是因为有时候需要在用户按下键后处理字符,而不是将字符存储到缓冲区并等待用户输入换行符。
这在UNIX系列平台和Windows系列平台中都可以做到。遗憾的是,两种平台使用的方法毫不相关。
UNIX版本比较直观,使用众所周知的技术:将终端设置为raw模式(且通常同时关闭回显功能)。
def getchar
system("stty raw -echo") # Raw mode, no echo
char = STDIN.getc
system("stty -raw echo") # Reset terminal mode
char
end
在Windows平台中,需要编写C语言扩展来实现。当前一种替代方案是使用Win32API库的一项小特性。
require 'Win32API'
def getchar
char = Win32API.new("crtdll", "_getch", [], 'L').Call
end
这两种方式的行为相同。
10.1.21 将整个文件读取到内存中
要将整个文件读取到数组中,甚至不用显式地打开文件。方法IO.readlines可完成这项工作,它负责打开和关闭文件。
arr = IO.readlines("myfile")
lines = arr.size
puts "myfile has #{lines} lines in it."
longest = arr.collect {|x| x.length}.max
puts "The longest line in it has #{longest} characters."
也可使用IO.read(它返回一个大型字符串,而不是由行组成的数组)。
str = IO.read("myfile")
bytes = arr.size
puts "myfile has #{bytes} bytes in it."
longest = str.collect {|x| x.length}.max # strings are enumerable!
puts "The longest line in it has #{longest} characters."
显然由于IO是File的超类,因此也可使用File.readlines和File.read。
10.1.22 逐行对文件进行迭代
要以每次一行的方式对文件进行迭代,可使用类方法IO.foreach或实例方法each。使用前者时,不需要在代码中显式地打开文件。
# Print all lines containing the word "target"
IO.foreach("somefile") do |line|
puts line if line =~ /target/
end
# Another way...
file = File.new("somefile")
file.each do |line|
puts line if line =~ /target/
end
注意,each_line是each的别名。
10.1.23 逐字节对文件进行迭代
要逐字节迭代,可使用实例方法each_byte。别忘了,它将字符(即整数)传递给代码块,如果需要转换为真正的字符,可使用chr方法。
file = File.new("myfile")
e_count = 0
file.each_byte do |byte|
e_count += 1 if byte == ?e
end
10.1.24 将字符串视为文件
有时需要知道如何将字符串当成文件进行处理,答案取决于这个问题的准确含义。
从很大程度上说,对象是由其方法定义的。下列代码将一个迭代器应用于对象source,每次迭代都将生成一行输出。通过这段代码,能够知道source的类型吗?
source.each do |line|
puts line
end
实际上,source可能是文件,也可能是包含换行符的字符串。因此,在类似这样的情况下,可将字符串视为文件。
在较新的Ruby版本中,标准库stringio使这成为可能。
该StringIO实现有一个接口几乎与本书第一版实现的相同,它还有存取器string,指向字符串本身的内容。
require 'stringio'
ios = StringIO.new("abcdefghijkl\nABC\n123")
ios.seek(5)
ios.puts("xyz")
puts ios.tell # 8
puts ios.string.dump # "abcdexyzijkl\nABC\n123"
c = ios.getc
puts "c = #{c}" # c = 105
ios.ungetc(?w)
puts ios.string.dump # "abcdexyzwjkl\nABC\n123"
puts "Ptr = #{ios.tell}"
s1 = ios.gets # "wjkl"
s2 = ios.gets # "ABC"
10.1.25 读取程序内嵌的数据
读者十二岁时,可能曾经通过赋值杂志中的程序来学习BASIC,此时为方便可能使用了DATA语句。这些信息内嵌在程序中,但可以像读取外部数据那样读取它们。
如果愿意,在Ruby中也可以这样做。Ruby程序末尾的指令__END__指出后面为内嵌数据,这些数据可使用全局常量DATA来读取。DATA是一个IO对象,其行为与其他IO对象一样(注意,__END__标记必须位于行首)。
# Print each line backwards...
DATA.each_line do |line|
puts line.reverse
end
__END__
A man, a plan, a canal... Panama!
Madam, I'm Adam.
,siht daer nac uoy fI
.drah oot gnikrow neeb ev'uoy
10.1.26 读取程序的源代码
如果要访问当前程序的源代码,可采用其他地方用过的技巧(请参阅第10.1.25节)的变体。
全局常量DATA是个IO对象,指向__END__指令后面的数据。但如果执行rewind操作,它将把文件指针重置到程序源代码的开头。
下面的程序生成其源代码清单,并加上了行号。这不是很有用,但读者可能发现这项功能的其他用途。
DATA.rewind
num = 1
DATA.each_line do |line|
puts "#{'%03d' % num} #{line}"
num += 1
end
__END__
注意,__END__指令是必不可少的,如果没有它,根本就不能访问DATA。
10.1.27 处理临时文件
在很多情况下需要处理匿名文件,此时不需要给文件命名或确保没有名称冲突,也不用删除它们。
所有这些问题都由Tempfile库处理。new方法(别名为open)将一个基本名(basename)作为种子字符串(seed string),在后面加上进程id和唯一的序列号。可选的第二个参数是要使用的目录,默认为环境变量TMPDIR、TMP或TEMP的值加上/tmp。
在程序执行期间,可打开和关闭生成的IO对象多次。程序结束后,临时文件也将被删除。
close方法接受一个可选标记,如果它被设置为true,文件将在关闭后立刻被删除(而不是等到程序结束后)。需要时,可使用path方法返回文件的实际路径名。
require "tempfile"
temp = Tempfile.new("stuff")
name = temp.path # "/tmp/stuff17060.0"
temp.puts "Kilroy was here"
temp.close
# Later...
temp.open
str = temp.gets # "Kilroy was here"
temp.close(true) # Delete it NOW
10.1.28 改变和设置当前目录
当前目录可使用Dir.pwd或其别名Dir.getwd来获悉,这两个缩写以前分别表示print working directory和get working directory。在Windows环境中,反斜杠可能显示为正(前)斜杠。
方法Dir.chdir可用于改变当前目录,在Windows中,盘符可能出现在字符串的前面。
Dir.chdir("/var/tmp")
puts Dir.pwd # "/var/tmp"
puts Dir.getwd # "/var/tmp"
该方法还接受一个代码块作为参数,如果指定了代码块,则仅在执行该代码块期间改变当前目录(执行完毕后将切换到原来的目录):
Dir.chdir("/home")
Dir.chdir("/tmp") do
puts Dir.pwd # /tmp
# other code...
end
puts Dir.pwd # /home
10.1.29 改变当前根目录
在大部分UNIX平台中,可以改变当前进程中的根目录(“斜杠”的含义)。通常这样做是出于安全考虑,例如,运行不安全或未测试过的代码时。chroot方法将新的根设置为指定的目录。
Dir.chdir("/home/guy/sandbox/tmp")
Dir.chroot("/home/guy/sandbox")
puts Dir.pwd # "/tmp"
10.1.30 迭代目录项
类方法foreach是个迭代器,它不断地将每个目录项传递给代码块,实例方法each的行为与此相同。
Dir.foreach("/tmp") { |entry| puts entry }
dir = Dir.new("/tmp")
dir.each { |entry| puts entry }
这两个代码段打印相同的输出(目录/tmp中所有的文件和子目录的名称)。
10.1.31 获取目录项列表
类方法Dir.entries返回由指定目录中的所有项组成的数组。
list = Dir.entries("/tmp") # %w[. .. alpha.txt beta.doc]
如上述代码所示,数组中包括当前目录和父目录,如果需要它们,必须手工进行删除。
10.1.32 创建目录链
有时需要创建一个目录链,其中中间的目录可能不存在。在UNIX命令行中,可使用mkdir –p来完成这项工作。
在Ruby代码中,可使用FileUtils.makedirs方法(来自fileutils库):
require "fileutils"
FileUtils.makedirs("/tmp/these/dirs/need/not/exist")
10.1.33 递归地删除目录
在UNIX环境中,可在命令行中执行rm –rf dir,这样从dir开始的整个子树都将被删除。显然,这样做时要特别小心。
在较新的Ruby版本中,Pathname有个rmtree方法可完成这项工作,FileUtils中的方法rm_r也如此。
require 'pathname'
dir = Pathname.new("/home/poole/")
dir.rmtree
# or:
require 'fileutils'
FileUtils.rm_r("/home/poole")
10.1.34 查找文件和目录
下面使用标准库find.rb来创建一个方法,它查找一个或多个文件,然后以数组的方式返回文件列表。第一个参数是起始目录,第二个可以是文件名(即字符串)或正则表达式。
require "find"
def findfiles(dir, name)
list = []
Find.find(dir) do |path|
Find.prune if [".",".."].include? path
case name
when String
list << path if File.basename(path) == name
when Regexp
list << path if File.basename(path) =~ name
else
raise ArgumentError
end
end
list
end
findfiles "/home/hal", "toc.txt"
# ["/home/hal/docs/toc.txt", "/home/hal/misc/toc.txt"]
findfiles "/home", /^[a-z]+.doc/
# ["/home/hal/docs/alpha.doc", "/home/guy/guide.doc",
# "/home/bill/help/readme.doc"]







