首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

计算机擅长计算,该含义比看起来深奥。如果只需坐在计算机前,并在需要时使用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"]

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论