# **第 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)。這樣,只需要具備當前行數據大小的內存就足夠了。

**圖 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` 方法后面的處理。

**圖 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` 方法。
- 推薦序
- 譯者序
- 前言
- 本書的讀者對象
- 第 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 參考集
- 后記
- 謝辭