ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# **第 3 章 创建命令** 本章介绍 Ruby 从命令行读取并处理数据的方法。另外,作为第 1 部分的总结,让我们来实际体验一下如何用 Ruby 实现 Unix 的 grep 命令,以便大家了解用 Ruby 编写程序的大概流程。 ### **3.1 命令行的输入数据** 到目前为止,我们写的程序都是向屏幕输出数据。现在我们考虑一下怎么输入数据。在创建命令前,我们首先得知道怎么使用命令。那么,让我们先来看看怎么把数据传递给程序。 向程序传递数据,最简单的方法就是使用命令行。Ruby 程序中,使用 `ARGV` 这个 Ruby 预定义好的数组来获取从命令行传递过来的数据。数组 `ARGV` 中的元素,就是在命令行中指定的脚本 字符串参数。 在命令行指定多个脚本参数时,各参数之间用空格间隔。 **代码清单 3.1 print_argv.rb** ~~~ puts "首个参数: #{ARGV[0]}" puts "第2 个参数: #{ARGV[1]}" puts "第3 个参数: #{ARGV[2]}" ~~~ > **执行示例** ~~~ > ruby print_argv.rb 1st 2nd 3rd 首个参数: 1st 第 2 个参数: 2nd 第 3 个参数: 3rd ~~~ 使用数组 `ARGV` 后,程序需要用到的数据就不必都写在代码中。同时,抽取数据、保存数据等普通的数组操作对于 `ARGV` 都是适用的。 **代码清单 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 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 Feb 27 23:46:09 2012 Yukihiro Matsumoto <matz@ruby-lang.org> * parse.y (opt_bv_decl): allow newline at the end. [ruby-dev:45292] ┊ ~~~ > **备注** Ruby 的源代码可以从 Ruby 官网下载。ChangeLog 文件可以在 Ruby 的 github 库里找到。 > - > Ruby 源码下载地址:[https://www.ruby-lang.org/zh_cn/downloads/](https://www.ruby-lang.org/zh_cn/downloads/) > - > github 上的 ChangeLog 文件地址:[https://raw.github.com/ruby/ruby/v2_0_0_0/ChangeLog](https://raw.github.com/ruby/ruby/v2_0_0_0/ChangeLog) > 我们就利用这个文件,练习一下用 Ruby 如何进行文件操作。 ### **3.2.1 从文件中读取内容并输出** 首先,我们先做一个简单文件内容读取程序。读取文件内容的流程,如下所示: ① **打开文件。** ② **读取文件的文本数据。** ③ **输出文件的文本数据。** ④ **关闭文件。** 程序代码如下: **代码清单 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 read_text_simple.rb** ~~~ 1: filename = ARGV[0] 2: text = File.read(filename) 3: print text ~~~ 关于 `File.read` 方法的详细用法,我们会在第 17 章进行说明。 更进一步,如果不使用变量,一行代码就可以搞定了。 **代码清单 3.6 read_text_oneline.rb** ~~~ 1: print File.read(ARGV[0]) ~~~ ### **3.2.2 从文件中逐行读取内容并输出** 现在,我们了解了如何使用 Ruby 读取并输出文件里的所有内容。但是,刚才的程序有如下的问题: - **一下子读取全部文件内容会很耗时;** - **读取文件的内容会暂时保存在内存中,遇到大文件时,程序有可能因此而崩溃。** 例如一个文件有 100 万行数据,我们只希望读取其最初的几行。这种情况下,如果程序不管三七二十一读取文件的全部内容,无论从时间还是内存角度来讲,都是严重的浪费。 因此,我们只能放弃“读取文件全部内容”的做法(图 3.1),将代码清单 3.4 的程序改为逐行读取并输出(代码清单 3.7)。这样,只需要具备当前行数据大小的内存就足够了。 ![{%}](https://box.kancloud.cn/2015-10-26_562e01d8b9826.png) **图 3.1 read 方法和 each_line 方法的区别** **代码清单 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 个参数创建了正则表达式对象,并赋值给变量 `pattern`。`Regexp.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 print "Hello, Ruby.\n" 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()` 调用了 `hello` 方法后,程序就会执行第 1 ~ 3 行定义的内容。 ### **3.4 其他文件的引用** 有时,我们希望在其他的程序里也能重复使用程序的某部分。例如,在某个程序里写好某个方法,在其他程序里也可以调用。 大部分的编程语言都提供了把多个不同程序组合为一个程序的功能。像这样,被其他程序引用的程序,我们称为库(library)。 Ruby 使用 `require` 方法来引用库。 **`require` 希望使用的库名** 库名可以省略后缀 .rb。 调用 `require` 方法后,Ruby 会搜索参数指定的库,并读取库的所有内容(图 3.2)。库内容读取完毕后,程序才会执行 `require` 方法后面的处理。 ![{%}](https://box.kancloud.cn/2015-10-26_562e01d8d1f37.png) **图 3.2 库与引用库的程序** 我们来实际操作一下,将刚才已经完成的 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 "./grep" # 读取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) #=> 7396 ~~~ > **专栏** > **pp 方法** > Ruby 除了提供 `p` 方法外,还提供了一个有类似作用的方法——`pp`。`pp` 是英语 pretty print 的缩写。使用 `pp`方法,我们需要使用 `require` 方法引用 `pp` 库。 > **代码清单 p_and_pp.rb** ~~~ require "pp"   v = [{ key00: "《Ruby 基础教程 第4 版》", key01: "《Ruby 秘笈》", key02: "《Rails3 秘笈》" }] p v pp v ~~~ > > **执行示例** ~~~ > ruby p_and_pp.rb [{:key00=>"《Ruby 基础教程 第4 版》", :key01=>"《Ruby 秘笈》", :key02=>"《Rails3 秘笈》"}] [{:key00=>"《Ruby 基础教程 第4 版》", :key01=>"《Ruby 秘笈》", :key02=>"《Rails3 秘笈》"}] ~~~ > 与 `p` 方法有点不同,`pp` 方法在输出对象的结果时,为了更容易看懂,会适当地换行以调整输出结果。建议像本例的散列那样,在需要确认嵌套的内容时使用 `pp` 方法。