第 3 章 创建命令

第 3 章 创建命令

本章将介绍 Ruby 从命令行读取并处理数据的方法。另外,作为第 1 部分的总结,我们来实际体验一下如何用 Ruby 实现 Unix 的 grep 命令,以便大家了解用 Ruby 编写程序的大概流程。

3.1 命令行的输入数据

到目前为止,我们写的程序都是向屏幕输出数据。现在我们考虑一下怎么输入数据。在创建命令前,我们首先得知道怎么使用命令。那么,我们就先来看看怎么把数据传递给程序。

向程序传递数据,最简单的方法就是使用命令行。Ruby 程序中,使用 ARGV 这个 Ruby 预定义好的数组来获取从命令行传递过来的数据。数组 ARGV 中的元素就是在命令行中指定的脚本字符串参数。

我们来看看代码清单 3.1。在命令行指定多个脚本参数时,各参数之间用空格隔开。

代码清单3.1 print_argv.rb

puts " 首个参数: #{ARGV[0]}"
puts " 第2 个参数: #{ARGV[1]}"
puts " 第3 个参数: #{ARGV[2]}"
puts " 第4 个参数: #{ARGV[3]}"
puts " 第5 个参数: #{ARGV[4]}"

执行示例

> ruby print_argv.rb 1st 2nd 3rd 4th 5th
首个参数: 1st
第2 个参数: 2nd
第3 个参数: 3rd
第4 个参数: 4th
第5 个参数: 5th

使用数组 ARGV 后,程序需要用到的数据就不必都写在代码中。同时,取出元素并赋值给变量等普通的数组操作对于 ARGV 都是适用的(代码清单 3.2)。

代码清单3.2 happy_birth.rb

name = ARGV[0]
print "Happy Birthday, ", name, "!\n"

执行示例

> ruby happy_birth.rb Ruby
Happy Birthday, Ruby!

从参数得到的数据都是字符串,因此将其用于运算时需要进行类型转换。把字符串转换为整数,可以使用 to_i 方法(代码清单 3.3)。

代码清单3.3 arg_arith.rb

num0 = ARGV[0].to_i
num1 = ARGV[1].to_i
puts "#{num0} + #{num1} = #{num0 + num1}"
puts "#{num0} - #{num1} = #{num0 - num1}"
puts "#{num0} * #{num1} = #{num0 * num1}"
puts "#{num0} / #{num1} = #{num0 / num1}"

执行示例

> ruby arg_arith.rb 5 3
5 + 3 = 8
5 - 3 = 2
5 * 3 = 15
5 / 3 = 1

3.2 文件的读取

Ruby 脚本除了读取命令行传递过来的字符串参数外,还可以读取预先写在文件中的数据。

Ruby 的源代码中有一个名为 ChangeLog 的文本文件,里面记录了 Ruby 相关的修改日志。

文件内容如下所示。

                                         ┆
Mon Dec 29 19:38:01 2014  Yukihiro Matsumoto  <matz@ruby-lang.org>

        * version.h (RUBY_VERSION): 2.3.0 development has started.
                                         ┆

备注 Ruby 的源代码可以从 Ruby 官网下载。ChangeLog 文件可以在 Ruby 的 GitHub 库里找到。

我们就利用这个文件,练习一下用 Ruby 如何进行文件操作。

3.2.1 从文件中读取内容并输出

首先,我们先创建一个单纯读取文件内容的程序。读取文件内容的流程如下所示。

打开文件。

读取文件的文本数据。

输出文件的文本数据。

关闭文件。

程序代码如代码清单 3.4 所示。

代码清单3.4 read_text.rb

1: filename = ARGV[0]
2: file = File.open(filename) # ①
3: text = file.read           # ②
4: print text                 # ③
5: file.close                 # ④

与之前的例子相比,这个例子的代码终于有点程序的模样了,接下来我们逐行分析。

第 1 行,将命令行参数 ARGV[0] 赋值给变量 filename。也就是说,filename 表示希望读取的文件名。第 2 行,File.open(filename) 表示打开名为 filename 的文件,并返回读取该文件所需的对象。可能会有读者不太明白什么是“读取该文件所需的对象”,不过不要紧,目前暂时只需要知道有这么一个对象就可以了。我们会在第 17 章中详细说明这个对象。

“读取该文件所需的对象”实际在第 3 行使用。在这里,read 方法读取文本数据,并将读取到的数据赋值给 text 变量。接下来,第 4 行的代码会输出 text 的文本数据。到目前为止,我们使用过好多次 print 方法了,大家应该不会陌生了吧。然后,程序执行最后一段代码的 close 方法。这样,就可以关闭之前打开的文件了。

像下面这样执行这个程序后,指定的文件内容会一下子全部输出到屏幕中。

> ruby read_text.rb 文件名

其实,如果只是读取文件内容,直接使用 read 方法会使程序更简单(代码清单 3.5)。

代码清单3.5 read_text_simple.rb

1: filename = ARGV[0]
2: text = File.read(filename)
3: print text

关于 File.read 方法的详细用法,我们会在第 17 章进行说明。

更进一步,如果不使用变量,一行代码就可以搞定了(代码清单 3.6)。

代码清单3.6 read_text_oneline.rb

1: print File.read(ARGV[0])

3.2.2 从文件中逐行读取内容并输出

现在我们了解了如何使用 Ruby 读取并输出文件中的所有内容,但是刚才的程序有如下的问题。

  • 一下子读取全部文件内容会很耗时

  • 读取的文件内容会暂时保存在内存中,遇到大文件时,程序有可能因此而崩溃

例如一个文件有 100 万行数据,我们只希望读取最初的几行。这种情况下,如果程序不管三七二十一读取文件的全部内容,无论从时间还是从内存角度来讲,都是严重的浪费。

因此,我们只能放弃“读取文件全部内容”的做法(图 3.1)。

{%}

图3.1  文本读取方式的差异

将代码清单 3.4 的程序改为逐行读取并输出(代码清单 3.7)。这样,只需要具备当前行数据大小的内存就足够了。

代码清单3.7 read_line.rb

1: filename = ARGV[0]
2: file = File.open(filename)
3: file.each_line do |line|
4:   print line
5: end
6: file.close

程序的第 1 行和第 2 行与代码清单 3.4 是一样的,从第 3 行开始有了变化。程序的第 3 行到第 5 行使用了 each_line 方法。

each_line 方法很像第 2 章介绍的 each 方法。each 方法是用于逐个处理数组元素,顾名思义,each_line 方法就是对文件进行逐行处理。因此,程序会逐行读取文件的内容,使用 print 方法输出该行的文件内容 line,直到所有行的内容输出完为止。

3.2.3 从文件中读取指定模式的内容并输出

Unix 中有一个叫 grep 的命令。grep 命令利用正则表达式搜索文本数据,输出按照指定模式匹配到的行。我们试试用 Ruby 实现 grep 命令(代码清单 3.8)。

代码清单3.8 simple_grep.rb

 1: pattern = Regexp.new(ARGV[0])
 2: filename = ARGV[1]
 3:
 4: file = File.open(filename)
 5: file.each_line do |line|
 6:   if pattern =~ line
 7:     print line
 8:   end
 9: end
10: file.close

在命令行输入以下命令,以执行代码清单 3.8。

> ruby simple_grep.rb 模式 文件名

程序有点长,我们逐行分析一下。

Ruby 执行该脚本时,需要有两个命令行参数——ARGV[0]ARGV[1]。第 1 行,程序根据第 1 个参数创建了正则表达式对象,并赋值给变量 patternRegexp.new(str) 表示把字符串 str 转换为正则表达式对象。接着第 2 行,把第 2 个参数赋值给作为文件名的变量 filename

第 4 行,打开文件,创建文件对象,并将其赋值给变量 file

第 5 行,与代码清单 3.7 一样,读取一行数据,并将其赋值给变量 line

第 6 行,使用 if 语句,判断变量 line 的字符串是否匹配变量 pattern 的正则表达式。如果匹配,则在程序第 7 行输出该字符串。这个 if 语句没有 else 部分,因此,若不匹配,程序什么都不会做。

当全部文本读取完毕后,关闭文件,结束程序。

假设我们希望输出 Changelog 文件中含有 matz 的行,可以执行以下命令。

> ruby simple_grep.rb matz Changelog

matz 是松本行弘先生的昵称,这样我们就可以轻松找到他的修改之处了。

3.3 方法的定义

到目前为止,我们用过很多 Ruby 的方法了,其实我们也能定义方法,定义方法的语法如下所示。

def 方法名
   希望执行的处理
end

假设我们需要定义一个输出“Hello, Ruby.”的方法。

def hello
  puts "Hello, Ruby."
end

执行这 3 行代码,实际上并不会输出任何结果。这是由于在调用 hello 方法前程序就已经结束了。因此方法定义好后,还要通过“调用”告诉 Ruby,我们要执行这个方法。

代码清单3.9 hello_ruby2.rb

1: def hello
2:   puts "Hello, Ruby."
3: end
4:
5: hello()

执行示例

> ruby hello_ruby2.rb
Hello, Ruby.

通过调用 hello() 方法,程序就会执行第 1 ~ 3 行定义的内容。

3.4 其他文件的引用

有时我们希望在其他程序中也能重复使用程序的某部分。例如,在某个程序中写好某个方法后,希望在其他程序中也可以调用。

大部分编程语言都提供了把多个不同程序组合为一个程序的功能。像这样,被其他程序引用的程序,我们称为(library)。

Ruby 使用 require 方法或者 require_relative 方法来引用库。

require 希望使用的库名

或者

require_relative 希望使用的库名

库名可以省略后缀 .rb。

调用 require 方法后,Ruby 会搜索参数指定的库,并读取库的所有内容(图 3.2)。库内容读取完毕后,程序才会执行 require 方法后面的处理。

{%}

图3.2 库与引用库的程序

require 方法用于引用已存在的库。只需要指定库名,程序就会在预先定义好的路径下查找并读取与 Ruby 一起安装的库。而 require_relative 方法在查找库时,则是根据执行中的程序目录(文件夹)来进行的,这有利于程序读取写在不同文件的代码。

我们来实际操作一下,将刚才已经完成的 simple_grep.rb 作为库提供给其他程序引用。作为库的文件不用做特别的修改,只需把定义了 simple_grep 方法的文件(代码清单 3.10)和引用该文件的程序文件(代码清单 3.11)放在同一个文件夹即可。

代码清单3.10 grep.rb

def simple_grep(pattern, filename)
  file = File.open(filename)
  file.each_line do |line|
    if pattern =~ line
      print line
    end
  end
  file.close
end

代码清单3.11 use_grep.rb

require_relative "greb"         # 读取grep.rb(省略“.rb”)

pattern = Regexp.new(ARGV[0])
filename = ARGV[1]
simple_grep(pattern, filename)  # 调用simple_grep 方法

这里,程序把执行 simple_grep 方法时所需要的检索模式以及文件名两个参数,分别赋值给 pattern 变量以及 filename 变量。

请注意,在这个例子中,use_grep.rb 调用了在 grep.rb 中定义的 simple_grep 方法。与代码清单 3.8 一样,执行以下命令可以输出 Changelog 文件中包含 matz 字符串的行。

> ruby use_grep.rb matz Changelog

Ruby 提供了很多便利的标准库,在我们的程序需要用到时,都可以使用 require 方法加以引用。

例如,通过引用 date 库,程序就可以使用返回当前日期的 Date.today 的方法,或者返回指定日期对象的 Date.new 方法。下面是一个求从 Ruby 的生日——1993 年 2 月 24 日到今天为止的天数的小程序。关于 date 库,我们将会在第 20 章详细说明。

require "date"

days = Date.today - Date.new(1993, 2, 24)
puts(days.to_i)  #=> 8323

专栏

pp 方法

Ruby 除了提供 p 方法外,还提供了一个有类似作用的方法——pppp 是英语 pretty print 的缩写。要使用 pp 方法,我们需要使用 require 方法引用 pp 库。

代码清单3.12  p_and_pp.rb

require "pp"
    
books = [
  { title: " 猫街", author: " 萩原朔太郎" },
  { title: " 猫的事务所", author: " 宫泽贤治" },
  { title: " 猫语教科书", author: "Paul Gallico" },
]
p books
pp books

执行示例

> ruby p_and_pp.rb
[{:title=>" 猫街", :author=>" 萩原朔太郎"}, {:title=>" 猫的事务所", :author=>" 宫泽
贤治"}, {:title=>" 猫语教科书", :author=>"Paul Gallico"}]
[{:title=>" 猫街", :author=>" 萩原朔太郎"},
{:title=>" 猫的事务所", :author=>" 宫泽贤治"},
{:title=>" 猫语教科书", :author=>"Paul Gallico"}]

p 方法有点不同,pp 方法在输出对象的结果时,为了更容易看懂,会适当地换行以调整输出结果。建议像本例的散列那样,在需要确认嵌套的内容时使用 pp 方法。

目录

  • 版权声明
  • 推荐序
  • 译者序
  • 前言
  • 第 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 参考集
  • 后记
  • 谢辞