# **第 17 章 IO 类**
到目前为止出现的程序中有一些是关于处理文件中的数据的。`IO` 类的主要作用就是让程序与外部进行数据的输入(input)/ 输出(output)操作。在本章中,我们将会讨论以下内容。
-
**输入/ 输出的种类**
介绍 `IO` 类支持的输入 / 输出对象。
-
**基本的输入/ 输出操作**
介绍 `IO` 类的基本操作,以行为单位或者以大小为单位进行读写操作。
-
**文件指针、二进制模式 / 文本模式、缓冲处理**
深入介绍一些与输入 / 输出有关的细节。二进制模式与文本模式是 Windows 特有的规范,若不注意常常会出现问题。
-
**与命令进行交互**
介绍与外部命令进行数据交换的方法
### **17.1 输入 / 输出的种类**
我们首先来了解一下输入 / 输出的对象到底是什么。
### **17.1.1 标准输入 / 输出**
程序在启动后会预先分配 3 个 `IO` 对象。
-
**标准输入**
标准输入可以获取从键盘输入的内容。通过预定义常量 `STDIN` 可调用操作标准输入的 `IO` 对象。另外,用全局变量 `$stdin` 也可以引用标准输入的 `IO` 对象。不指定接收者的 `gets` 方法等都会默认从标准输入中获取数据。
-
**标准输出**
向标准输出写入的数据会显示在屏幕中。通过预定义常量 `STDOUT` 可调用操作标准输出的 `IO` 对象。另外,用全局变量 `$stdout` 也可以引用标准输出的 `IO` 对象。不指定接收者的 `puts`、`print`、`printf` 等方法会默认将数据写入到标准输出。
-
**标准错误输出**
向标准错误输出写入的数据会显示在屏幕中。通过预定义常量 `STDERR` 可调用操作标准错误输出的 `IO` 对象。另外,用全局变量 `$stderr` 也可以引用标准错误输出的 `IO` 对象。
标准错误输出原本是用于输出错误信息的,但实际上,除输出警告或者错误之外,在希望与程序正常输出的信息做出区别时也可以使用它(代码清单 17.1)。
**代码清单 17.1 out.rb**
~~~
$stdout.print "Output to $stdout.\n" # 标准输出
$stderr.print "Output to $stderr.\n" # 标准错误输出
~~~
> **执行示例**
~~~
> ruby out.rb
Output to $stdout.
Output to $stderr.
~~~
将输出结果重定向到文件时,标准输出的内容会被写入到文件,只有标准错误输出的内容被 输出到屏幕中。
> **执行示例**
~~~
> ruby out.rb > log.txt
Output to $stderr.
~~~
> **备注** 在执行程序时,在命令后加上 `>` 文件名,就可以将程序执行时的输出结果保存到文件中。我们把这个控制台的功能称为“重定向”。通过这个功能,不仅 ruby 命令,程序的输出内容也都可以保存在文件中。
根据需要灵活使用标准输出与标准错误输出,可以使我们很方便地分开获取正常信息与错误信息。
> **备注** ruby 命令的错误信息也会被输出到标准错误输出。
通常标准输入、标准输出、标准错误输出都是与控制台关联的。但是将命令的输出重定向到文件,或者使用管道(pipe)将结果传递给其他程序时则与控制台没有关系。根据实际的使用情况,程序的输入 / 输出状态也各异。`IO` 对象是否与控制台关联,我们可以通过 `tty?` 方法判断。
代码清单 17.2 是一个检查标准输入是否为屏幕的例子。
**代码清单 17.2 tty.rb**
~~~
if $stdin.tty?
print "Stdin is a TTY.\n"
else
print "Stdin is not a TTY.\n"
end
~~~
下面我们用不同的方式调用这个程序,看看有何不同。首先是普通调用。
> **执行示例**
~~~
> ruby tty.rb
Stdin is a TTY.
~~~
将命令的输出结果传给管道,或者通过文件输入内容时,程序的结果会不一样。
> **执行示例**
~~~
> echo | ruby tty.rb
Stdin is not a TTY.
> ruby tty.rb < data.txt
Stdin is not a TTY.
~~~
> **备注** TTY 是 TeleTYpe 的缩写。
### **17.1.2 文件输入 / 输出**
通过 `IO` 类的子类 `File` 类可以进行文件的输入 / 输出处理。`File` 类中封装了文件删除、文件属性变更等文件专用的功能,而一些基本的输入 / 输出处理则使用继承自 IO 类的方法。
-
***io*= `File.open`(*file, mode*)
*io* = `open`(*file, mode*)**
通过 `File.open` 方法或 `open` 方法打开文件并获取新的 `IO` 对象。
模式(mode)会指定以何种目的打开文件(表 17.1)。缺省模式为只读模式(`"r"`)。在 Windows 环境下,在各模式后加上 `b`、通过 `"rb"`、`"rb+"` 等这样的形式即可表示二进制模式(后述)。
**表 17.1 模式**
| 模式 | 意义 |
|-----|-----|
| `r` | 用只读模式打开文件。 |
| `r+` | 用读写模式打开文件。 |
| `w` | 用只写模式打开文件。文件不存在则创建新的文件;文件已存在则清空文件,即将文件大小设置为0。 |
| `w+` | 读写模式,其余同 `"w"` 。 |
| `a` | 用追加模式打开文件。文件不存在则创建新的文件。 |
| `a+` | 用读取/ 追加模式打开文件。文件不存在则创建新的文件。 |
-
***io*.`close`**
使用 `close` 方法关闭已打开的文件。
1 个程序中同时打开文件的数量是有限制的,因此使用完的文件应该尽快关闭。如果打开多个文件而不进行关闭操作,程序就很可能会在使用 `open` 方法时突然产生异常。
`File.open` 方法如果使用块,则文件会在使用完毕后自动关闭。这种情况下,`IO` 对象会被作为块变量传递给块。块执行完毕后,块变量引用的 `IO` 对象也会自动关闭。这种写法会使输入 / 输出的操作范围更加清晰。
~~~
File.open("foo.txt") do |io|
while line = io.gets
┊
end
end
~~~
-
***io*.`close?`**
用 `close?` 方法可以检查 `IO` 对象是否关闭了。
~~~
io = File.open("foo.txt")
io.close
p io.closed? #=> true
~~~
-
**`File.read`(*file*)**
使用类方法 `read` 可以一次性读取文件 `file` 的内容。
~~~
data = File.read("foo.txt")
~~~
> **备注** 在 Windows 中不能使用 `File.read` 方法读取像图像数据等二进制数据。`File.read` 方法使用文本模式打开文件时,会对换行符等进行转换,因此无法得到正确的结果。详细内容请参考 17.4 节。
### **17.2 基本的输入 / 输出操作**
输入 / 输出操作的数据为字符串,也就是所谓的 `String` 对象。执行输入操作后,会从头到尾按顺序读取数据,执行输出操作后,则会按写入顺序不断追加数据。
### **17.2.1 输入操作**
-
***io*.`gets`(*rs*)
*io*.`each`(*rs*)
*io*.`each_line`(*rs*)
*io*.`readlines`(*rs*)**
从 `IO` 类的对象 *io* 中读取一行数据。用参数 *rs* 的字符串分行。省略 *rs* 时则用预定义变量 `$/`(默认值为 `"\n"`)。
这些方法返回的字符串中包含行末尾的换行符。用 `chmop!` 方法可以很方便地删除字符串末尾的换行符。
输入完毕后再尝试获取数据时,`gets` 方法会返回 `nil`。另外,我们还可以使用 `eof?` 方法检查输入是否已经完毕。
~~~
while line = io.gets
line.chomp!
┊ # 对line 进行的操作
end
p io.eof? #=> true
~~~
`while` 条件表达式中同时进行了变量赋值与条件判断的操作。将 `gets` 方法的返回值复制给 `line`,并将该值作为 `while` 语句的条件来判断。上面是 `gets` 方法的经典用法,大家应该尽快掌握这种写法。
用 `each_line` 方法也可以实现同样的效果。
~~~
io.each_line do |line|
line.chomp!
┊ # 对line 进行的操作
end
~~~
另外,用 `readlines` 方法可以一次性地读取所有数据,并返回将每行数据作为元素封装的数组。
~~~
ary = io.readlines
ary.each_line do |line|
line.chomp!
┊ # 对line 进行的操作
end
~~~
> **备注** `gets` 方法与 `puts` 方法,分别是“get string”、“put string”的意思。
-
***io*.`lineno`
*io*.`lineno`=(*number*)**
使用 `gets` 方法、`each_line` 方法逐行读取数据时,会自动记录读取的行数。这个行数可以通过 `lineno` 方法取得。此外,通过 `lineno=` 方法也可以改变这个值,但值的改变并不会对文件指针(后述)有影响。
在下面的例子中,逐行读取标准输入的数据,并在行首添加行编号。
~~~
$stdin.each_line do |line|
printf("%3d %s", $stdin.lineno, line)
end
~~~
-
***io*.`each_char`**
逐个字符地读取 *io* 中的数据并执行块。将得到的字符(`String` 对象)作为块变量传递。
~~~
io.each_char do |ch|
┊ # 对line 进行的操作
end
~~~
-
***io*.`each_byte`**
逐个字节地读取 *io* 中的数据并启动块。将得到的字节所对应的 ASCII 码以整数值的形式传递给块变量。
-
***io*.`getc`**
只读取 *io* 中的一个字符。根据文件编码的不同,有时一个字符会由多个字节组成,但这个方法只会读取一个字符,然后返回其字符串对象。数据全部读取完后再读取时会返回 `nil`。
~~~
while ch = io.getc
┊ # 对line 进行的操作
end
~~~
-
***io*.`ungetc`(*ch*)**
将参数 *ch* 指定的字符退回到 *io* 的输入缓冲中。
~~~
# hello.txt 中的内容为“Hello, Ruby.\n”
File.open("hello.txt") do |io|
p io.getc #=> "H"
io.ungetc(72)
p io.gets #=> "Hello, Ruby.\n"
end
~~~
指定一个字符大小的字符串对象。对可退回的字符数没有限制。
-
***io*.`getbyte`**
只读取 *io* 中的一个字节,返回得到的字节转换为 ASCII 码后的整数对象。数据全部读取完后再读取时会返回 `nil`。
-
***io*.`ungetbyte`(*byte*)**
将参数 *byte* 指定的一个字节退回到输入缓冲中。参数为整数时,将该整数除以 256 后的余数作为 ASCII 码字符返回一个字节;参数为字符串时,只返回字符串的第一个字节。
-
***io*.`read`(*size*)**
读取参数 *size* 指定的大小的数据。不指定大小时,则一次性读取全部数据并返回。
~~~
# hello.txt 中的内容为"Hello, Ruby.\n"
File.open("hello.txt") do |io|
p io.read(5) #=> "Hello"
p io.read #=> ",Ruby.\n"
end
~~~
### **17.2.2 输出操作**
-
***io*.`puts`(*str0, str1,* …)**
对字符串末尾添加换行符后输出。指定多个参数时,会分别添加换行符。如果参数为 `Sting` 类以外的对象,则会调用 `to_s` 方法,将其转换为字符串后再输出。
**代码清单 17.3 stdout_put.rb**
~~~
$stdout.puts "foo", "bar", "baz"
~~~
> **执行示例**
~~~
> ruby stdout_put.rb
foo
bar
baz
~~~
-
***io*.`putc`(*ch*)**
输出参数 *ch* 指定的字符编码所对应的字符。参数为字符串时输出首字符。
**代码清单 17.4 stdout_putc.rb**
~~~
$stdout.putc(82) # 82 是R 的ASCII 码
$stdout.putc("Ruby")
$stdout.putc("\n")
~~~
> **执行示例**
~~~
> ruby stdout_putc.rb
RR
~~~
-
***io*.`print`(*str0, str1,* …)**
输出参数指定的字符串。参数可指定多个字符串。参数为 `String` 以外的对象时会自动将其转换为字符串。
-
***io*.`printf`(*fmt, arg0, arg1,* …)**
按照指定的格式输出字符串。格式 *fmt* 的用法与 `printf` 方法一样,请参考第 192 页专栏《printf 方法与 sprintf 方法》。
-
***io*.`write`(*str*)**
输出参数 *str* 指定的字符串。参数为 `String` 以外的对象时会自动将其转换为字符串。方法返回值为输出的字节数。
~~~
size = $stdout.write("Hello.\n") #=> Hello.
p size #=> 6
~~~
-
***io*<<*str***
输出参数 *str* 指定的字符串。`<<` 会返回接收者本身,因此可以像下面这样写:
~~~
io << "foo" << "bar" << "baz"
~~~
### **17.3 文件指针**
一般情况下,我们会以行为单位处理文本数据。由于只有当读取到换行符时才能知道行的长度,因此,如要读取第 100 行的数据,就意味着要将这 100 行的数据全部读取。另外,如果我们修改了数据,行的长度也会随之变更,这样一来,文件中后面的数据就都要做出修改。
为了提高读取效率,可以将文件分成固定长度的文件块,来直接访问某个位置的数据(虽然据此可以访问任意位置的数据,但却不能处理超过指定长度的数据)。
我们用文件指针(file pointer)或者当前文件偏移量(current file offset)来表示 `IO` 对象指向的文件的位置。每当读写文件时,文件指针都会自动移动,而我们也可以使文件指针指向任意位置来读写数据。
-
***io*.`pos`
*io*.`pos`=(*position*)**
通过 `pos` 方法可以获得文件指针现在的位置。改变文件指针的位置用 `pos=` 方法。
~~~
# hello.txt 中的内容为"Hello, Ruby.\n"
File.open("hello.txt") do |io|
p io.read(5) #=> "Hello"
p io.pos #=> 5
io.pos = 0
p io.gets #=> "Hello, Ruby.\n"
end
~~~
-
***io*.`seek`(*offset, whence*)**
移动文件指针的方法。参数 *offset* 为用于指定位置的整数,参数 *whence* 用于指定 *offset* 如何移动(表 17.2)。
**表 17.2 whence 中指定的值**
| whence | 意义 |
|-----|-----|
| `IO::SEEK_SET` | 将文件指针移动到 *offset* 指定的位置 |
| `IO::SEEK_CUR` | 将 *offset* 视为相对于当前位置的偏移位置来移动文件指针 |
| `IO::SEEK_END` | 将 *offset* 指定为相对于文件末尾的偏移位置 |
-
***io*.`rewind`**
将文件指针返回到文件的开头。`lineno` 方法返回的行编号为 0。
~~~
# hello.txt 中的内容为"Hello, Ruby.\n"
File.open("hello.txt")do |io|
p io.gets #=> "Hello,Ruby.\n"
io.rewind
p io.gets #=> "Hello, Ruby.\n"
end
~~~
-
***io*.`truncate`(*size*)**
按照参数 *size* 指定的大小截断文件。
~~~
io.truncate(0) # 将文件大小置为0
io.truncate(io.pos) # 删除当前文件指针以后的数据
~~~
### **17.4 二进制模式与文本模式**
正如我们在第 14 章的专栏《关于换行符》中介绍的那样,不同平台下的换行符也不同。
虽然各个平台的换行符不一样,但为了保证程序的兼容性,会将字符串中的 `"\n"` 转换为当前 OS 的换行符并输出。此外,在读取的时候也会将实际的换行符转换为 `"\n"`。
图 17.1 为在 Windows 中转换换行符的情形。
![{%}](https://box.kancloud.cn/2015-10-26_562e0200a247d.png)
**图 17.1 Windows 环境中字符 `"\n"` 的转换**
当需要确定文件大小进行输入 / 输出处理时,或者直接使用从其他平台拷贝的文件时,如果进行换行符转换,就有可能会引发问题。
为了解决上述那样的问题,Ruby 中还提供了不进行换行符转换的方法。换行符处理的前提是以行为单位做输入 / 输出处理,需要转换时称为文本模式,反之不需要转换时则称为二进制模式。
-
***io*.`binmode`**
新的 `IO` 对象默认是文本模式,使用 `binmode` 方法可将其变更为二进制模式。
~~~
File.open("foo.txt", "w") do |io|
io.binmode
io.write "Hello, world.\n"
end
~~~
这样就可以既不用转换换行符,又能得到与文件中一模一样的数据。
> **备注** 转换为二进制模式的 `IO` 对象无法再次转换为文本模式。
### **17.5 缓冲**
即使对 `IO` 对象输出数据,结果也并不一定马上就会反映在控制台或者文件中。在使用 `write`、`print` 等方法操作 `IO` 对象时,程序内部会开辟出一定的空间来保存临时生成的数据副本(图 17.2)。这部分空间就称为缓冲(buffer)。缓冲里累积一定量的数据后,就会做实际的输出处理,然后清空缓冲。
![{%}](https://box.kancloud.cn/2015-10-26_562e0200b434c.png)
**图 17.2 缓冲的状态**
像这样,使用临时缓冲进行数据处理称为缓冲处理(buffering)。
在向控制台输出的两种方式(标准输出与标准错误输出)中,标准错误输出完全不采用缓冲处理。因此,当两种方式混合使用时,程序实际输出的顺序可能会与程序代码中记录的顺序不一样。
我们来看看代码清单 17.5 的例子(不同平台下的运行结果可能会不一样)。
**代码清单 17.5 test_buffering1.rb**
~~~
$stdout.print "out1 "
$stderr.print "err1 "
$stdout.print "out2 "
$stdout.print "out3 "
$stderr.print "err2\n"
$stdout.print "out4\n"
~~~
> **执行示例**
~~~
> ruby test_buffering1.rb
err1 err2
out1 out2 out3 out4
~~~
标准错误输出的主要目的是输出如警告、错误等信息,因此执行结果必须马上反映出来。再次强调,建议在显示程序中正常信息以外的信息时使用标准错误输出。
虽然缓冲处理可以提高输出效率,但有时候我们会希望执行结果可以马上反映出来,这时我们就可以用下面的方法来同步数据的操作与输出。
-
***io*.`flush`**
强制输出缓冲中的数据。在代码清单 17.5 的基础上追加 `$stdout.flush` 的调用(代码清单 17.6)。
**代码清单 17.6 test_buffering2.rb**
~~~
$stdout.print "out1 "; $stdout.flush
$stderr.print "err1 "
$stdout.print "out2 "; $stdout.flush
$stdout.print "out3 "; $stdout.flush
$stderr.print "err2\n"
$stdout.print "out4\n"
~~~
> **执行示例**
~~~
> ruby test_buffering2.rb
out1 err1 out2 out3 err2
out4
~~~
-
***io*.`sync`
*io*.`sync`=(*state*)**
通过 `io.sync = true`,程序写入缓冲时 `flush` 方法就会被自动调用。
**代码清单 17.7 test_buffering3.rb**
~~~
$stdout.sync = true # 同步输出处理
$stdout.print "out1 "
$stderr.print "err1 "
$stdout.print "out2 "
$stdout.print "out3 "
$stderr.print "err2\n"
$stdout.print "out4\n"
~~~
即使不逐次调用 flush 方法,也可以像下面那样按顺序输出:
> **执行示例**
~~~
> ruby test_buffering3.rb
out1 err1 out2 out3 err2
out4
~~~
### **17.6 与命令进行交互**
虽然 Ruby 是几乎什么都能实现的强大的语言,但是也会有与其他命令进行数据交换的时候。例如,读取使用 GUN zip 压缩的数据的时候,使用 gunzip 命令会很方便。在 Ruby 中,使用 `IO.popen` 方法可以与其他命令进行数据处理。
-
**IO.`popen`(*command, mode*)**
参数 *mode* 的使用方法与 `File.open` 方法是一样的,参数缺省时默认为 `"r"` 模式。
用 `IO.popen` 方法生成的 `IO` 对象的输入 / 输出,会关联启动后的命令 *command* 的标准输入 / 输出。也就是说,`IO` 对象的输出会作为命令的输入,命令的输出则会作为 `IO` 对象的输入。
我们来改造一下第 3 章的 simple_grep.rb,利用 gunzip 命令解压处理扩展名为 .gz 的文件(-c 为将解压后的结果写入到标准输出时的选项)。
**代码清单 17.8 simple_grep_gz.rb**
~~~
pattern = Regexp.new(ARGV[0])
filename = ARGV[1]
if /.gz$/ =~ filename
file = IO.popen("gunzip -c #{filename}")
else
file = File.open(filename)
end
file.each_line do |text|
if pattern =~ text
print text
end
end
~~~
> **注** 执行代码清单 17.8 时需要 gunzip 命令。
-
**`open`("*|command*"*, mode*)**
将带有管道符号的命令传给 `open` 方法的效果与使用 `IO.popen` 方法是一样的。
~~~
filename = ARGV[0]
open("|gunzip -c #{filename}") do |io|
io.each_line do |line|
print line
end
end
~~~
### **17.7 open-uri 库**
除了控制台、文件以外,进程间通信时使用的管道(pipe)、网络间通信时使用的套接字(socket)也都可以作为 `IO` 对象使用。管道、套接字的使用方法超出了本书的范围,因此不做详细说明,这里我们简单地介绍一下利用 HTTP、FTP 从网络获取数据的方法。
通过 `require` 引用 `open-uri` 库后,我们就可以像打开普通的文件一样打开 HTTP、FTP 的 URL(代码清单 17.9)。使用 `open-uri` 库的功能时,不要使用 `File.open` 方法,只使用 `open` 方法即可。
**代码清单 17.9 read_uri.rb**
~~~
require "open-uri"
# 通过HTTP 读取数据
open("http://www.ruby-lang.org") do |io|
puts io.read # 将Ruby 的官方网页输出到控制台
end
# 通过FTP 读取数据
url = "ftp://www.ruby-lang.org/pub/ruby/2.0/ruby-2.0.0-p0.tar.gz"
open(url) do |io|
open("ruby-2.0.0-p0.tar.gz", "w") do |f| # 打开本地文件
f.write(io.read)
end
end
~~~
通过 HTTP 协议时,服务器会根据客户端的状态改变应答的内容,比如返回中文或英语的网页等。为了实现这个功能,请求时就需要向服务器发送元信息(meta information)。例如,代码清单 17.10 中的 HTTP 头部信息 Accept-Language 就表示优先接收中文网页。指定 HTTP 头部信息时,会将其以散列的形式传递给 `open` 方法的第 2 个参数。
**代码清单 17.10 read_uri_cn.rb**
~~~
require "open-uri"
options = {
"Accept-Language" => "zh-cn, en;q=0.5",
}
open("http://www.ruby-lang.org", options){|io|
puts io.read
}
~~~
### **17.8 stringio 库**
在测试程序时,虽然我们会希望知道向文件或控制台输出了什么,但程序实际执行的结果却往往很难知道。为此,我们可以通过向模拟 `IO` 对象的对象进行输出来确认执行结果。
`StringIO` 就是用于模拟 `IO` 对象的对象。通过 `require` 引用 `stringio` 库后,就可以使用 `StringIO` 对象了(代码清单 17.11)。
**代码清单 17.11 stringio_puts.rb**
~~~
require "stringio"
io = StringIO.new
io.puts("A")
io.puts("B")
io.puts("C")
io.rewind
p io.read #=> "A\nB\nC\n"
~~~
实际上,向 `StringIO` 对象进行的输出并不会被输出到任何地方,而是会被保存在对象中,之后就可以使用 `read` 方法等来读取该输出。
`StringIO` 对象还有另外一种用法,那就是将字符串数据当作 `IO` 数据处理。将大数据保存在文件中,并将小数据直接传输给别的处理时,通过使用 `StringIO` 对象,程序就可以不区分对待 `IO` 对象和字符串了。实际上,之前介绍的用 `open-uri` 库打开 URI 时,也是有时候返回 `IO` 对象,有时候返回 `StringIO` 对象。不过一般情况下,我们不需要在意这两者的区别。通过将数据字符串传递给 `StringIO.new` 方法的参数,就可以由字符串创建 `StringIO` 对象(代码清单 17.12)。
**代码清单 17.12 stringio_gets.rb**
~~~
require "stringio"
io = StringIO.new("A\nB\nC\n")
p io.gets #=> "A\n"
p io.gets #=> "B\n"
p io.gets #=> "C\n"
~~~
`StringIO` 对象可以模拟本章中介绍的大部分输入 / 输出操作。
### **练习题**
1. 创建脚本,读取文本文件中的内容,按以下条件进行处理。这里将空白和换行以外的连续字符串称为“单词”。
(a)统计文本的行数
(b)统计文本的单词数
(c)统计文本的字符数
2. 创建脚本,按照以下条件覆盖文本文件中原有的数据。
(a)将文件中的行逆序排列
(b)保留文件中第 1 行数据,其余全部删除
(c)保留文件中最后 1 行数据,其余全部删除
3. 定义一个功能类似于 Unix 中的 tail 命令的 `tail` 方法。
`tail` 方法有两个参数。
**`tail`( 行数, 文件名)**
从文件的末尾开始数,输出参数指定的行数的内容。也就是说,假设有一个有 100 行数据的文件 some_file.txt,执行 `tail(10, "some_file.txt")` 后,程序就会跳过前 90 行数据,只在标准输出中输出最后 10 行数据。
> 参考答案:请到图灵社区本书的“随书下载”处下载([http://www.ituring.com.cn/book/1237](http://www.ituring.com.cn/book/1237))。
- 推荐序
- 译者序
- 前言
- 本书的读者对象
- 第 1 部分 Ruby 初体验
- 第 1 章 Ruby 初探
- 第 2 章 便利的对象
- 第 3 章 创建命令
- 第 2 部分 Ruby 的基础
- 第 4 章 对象、变量和常量
- 第 5 章 条件判断
- 第 6 章 循环
- 第 7 章 方法
- 第 8 章 类和模块
- 第 9 章 运算符
- 第 10 章 错误处理与异常
- 第 11 章 块
- 第 3 部分 Ruby 的类
- 第 12 章 数值类
- 第 13 章 数组类
- 第 14 章 字符串类
- 第 15 章 散列类
- 第 16 章 正则表达式类
- 第 17 章 IO 类
- 第 18 章 File 类与 Dir 类
- 第 19 章 Encoding 类
- 第 20 章 Time 类与 Date 类
- 第 21 章 Proc 类
- 第 4 部分 动手制作工具
- 第 22 章 文本处理
- 第 23 章 检索邮政编码
- 附录
- 附录 A Ruby 运行环境的构建
- 附录 B Ruby 参考集
- 后记
- 谢辞