發(fā)布于:2021-01-25 10:35:43
0
111
0
我喜歡偶爾編寫自定義命令行工具。最近的一些例子是walk
和stest。
不久前我讀到Rob Landley寫一個words
工具的想法,因為我喜歡這個想法,我想(重新)實現(xiàn)這個工具,同時試著看看我是否能保持它比Rob的C版本更簡單。
Swift
閱讀/打印
Words
應該是一個過濾器,這意味著它應該從標準輸入讀取并寫入標準輸出。讓我們開始編寫一個最簡單的過濾器,它只回顯stdin而不做任何修改。
while let line = readline() {
print(line)
}
拆分/聯(lián)接
要解決手頭的問題,我們可以將每行處理為
將行拆分為單詞
選擇其中的一些單詞
按任意順序
允許多個選擇
將所選單詞組合成新行
讓我們實現(xiàn)更簡單的部分其中:
while let line = readLine() {
let words = line.components(separatedBy: .whitespaces)
print(words.joined(separator: " "))
}
不同之處在于,這個版本用單空格字符替換了空格序列。
透明/不透明
需要選擇哪些單詞(及其順序)
由用戶以某種方式指示
由程序以某種方式評估
稍后我們將處理用戶界面。程序可以將所需的信息存儲為(被動)數(shù)據(jù)結(jié)構(gòu)或(主動)函數(shù)或?qū)ο蟆?/span>
這說明了一個典型的設計沖突…嗯,不是函數(shù)式編程和面向?qū)ο缶幊讨g的沖突,我相信這是完全獨立于此的。我指的是Noel Welsh所描述的“不透明和透明的口譯員”。
在我們的程序中,差異如下所示:
透明
let wanted: [Int] = // TODO
let words = line.components(separatedBy: .whitespaces)
let selected = select(wanted, from: words)
print(selected.joined(separator: " "))
不透明
let select: ([String]) -> [String] = // TODO
let words = line.components(separatedBy: .whitespaces)
let selected = select(words)
print(selected.joined(separator: " "))
取舍可以歸結(jié)為一個問題:我們想把解決方案的哪些部分分開?
我們先來看看透明的變體,看看它會帶我們?nèi)ツ睦铩?/span>
論據(jù)
我們要在命令行中輸入選擇。我可以想到兩種方法:
每個命令行參數(shù)表示一個選擇,程序僅對標準輸入進行操作。
words 3 4 2 < file
第一個命令行參數(shù)指定所有選擇,其余參數(shù)指示要處理的文件。
words 3,4,2 file
我們將使用第一種方法,因為我相信它更容易實現(xiàn)
func index(_ string: String) -> Int {
guard let int = Int(string) else {
print("invalid index: (string)")
exit(EX_USAGE)
}
return int - 1
}
let arguments = CommandLine.arguments.dropFirst()
let wanted = arguments.map(index)
這里發(fā)生了很多事情,讓我們來看看代碼:
let arguments = CommandLine.arguments.dropFirst()
let wanted = arguments.map(index)
CommandLine.arguments
的第一個元素包含可執(zhí)行文件的路徑,我們將把剩余的參數(shù)轉(zhuǎn)換成索引
guard let int = Int(string) else {
print("invalid index: (string)")
exit(EX_USAGE)
}
我們把string
轉(zhuǎn)換成一個數(shù)字。如果失敗,比如當string
不包含數(shù)字時,我們打印一條錯誤消息,并使用EX_USAGE
中止。
return int - 1
命令行上提供的索引應該從1開始,因此我們在這里按1更正以獲得數(shù)組索引。
選擇
一旦我們有了索引,我們就可以從數(shù)組中獲取相應的元素。
func select(_ indices: [Int], from array: [A]) -> [A] {
return indices.map { array[$0] }
}
“致命錯誤:索引超出范圍”
只要有一個索引超出了anyline的范圍,這個簡單版本的程序就會崩潰。我們可以通過忽略超出給定行界限的索引來防止這個問題。
func select(_ indices: [Int], from array: [A]) -> [A] {
let valid = { array.indices.contains($0) }
return indices.filter(valid).map { array[$0] }
}
射程
一個非常有用的特性是,除了數(shù)字之外,還可以提供數(shù)字的范圍,如-f 3-6
以打印第三到第六個字段。
我們可以用Range
類型對這些范圍進行建模,所以讓我們嘗試將該特性添加到我們的程序中。
同樣,更復雜的部分是解析索引:
func index(_ string: String) -> CountableRange{
func parse(_ component: String, default empty: Int) -> Int {
if (component.isEmpty) {
return empty
}
if let int = Int(component) {
return int
}
print("invalid component: `(component)' in range: `(string)'")
exit(EX_USAGE)
}
let components = string.components(separatedBy: "-")
let lower = parse(components.first!, default: 1) - 1
let upper = parse(components.last!, default: Int.max)
return lower..<upper
}
我們將單個數(shù)字的解析提取到一個可以多次調(diào)用的內(nèi)部函數(shù)中。作為一個內(nèi)部函數(shù),它可以訪問其包含函數(shù)的參數(shù),我們可以利用它來提供更好的錯誤消息。此函數(shù)還期望在缺少范圍的一個邊界時返回一個默認值,這意味著我們可以將3-
寫為“從第三個開始的每個單詞”。
對于每個范圍,我們使用1
和Int.max
作為默認值,將第一個和最后一個組件分別解析為其下限和上限,并從這些邊界構(gòu)造一個CountableRange
。我們只需要將下限改為1,因為CountableRange
希望排除上限。
func select(_ ranges: [CountableRange], from array: [A]) -> [A] {
return ranges.flatMap { range in
return array[range.clamped(to: array.indices)]
}
}
要應用我們的選擇,我們獲取每個范圍,使用clamp
將其修剪到數(shù)組的邊界,然后從數(shù)組中獲取相應的元素。因為它返回集合而不是單個單詞,所以我們調(diào)用flatMap
而不是map
來展平所有這些集合。
結(jié)論
以下是整個代碼:
import Foundation
func index(_ string: String) -> CountableRange{
func parse(_ component: String, default empty: Int) -> Int {
if (component.isEmpty) {
return empty
}
if let int = Int(component) {
return int
}
print("invalid component: `(component)' in range: `(string)")
exit(EX_USAGE)
}
let components = string.components(separatedBy: "-")
let lower = parse(components.first!, default: 1) - 1
let upper = parse(components.last!, default: Int.max)
return lower..<upper
}
func select(_ ranges: [CountableRange], from array: [A]) -> [A] {
return ranges.flatMap { range in
return array[range.clamped(to: array.indices)]
}
}
let arguments = CommandLine.arguments.dropFirst()
let wanted = arguments.map(index)
while let line = readLine() {
let words = line.components(separatedBy: .whitespaces)
let selected = select(wanted, from: words)
print(selected.joined(separator: " "))
}
在生成和使用修改后的數(shù)據(jù)的兩個地方,需求的變化保持了令人愉快的局部性。我們沒有修改腳本的主體,因為我們省略了更改數(shù)據(jù)的類型簽名,并且在處理過程中不需要任何其他更改。
腳本的大小與C代碼相當,盡管兩個版本支持不同的功能(我們的版本支持范圍,而另一個版本支持自定義字分隔符),并且使用完全不同的框架(toybox基礎結(jié)構(gòu)用于Unix命令行工具,swift標準庫有一些關于它們的基本規(guī)定),所以把這個比喻為一個巨大的鹽粒。