Linxu 世界中有許多有趣的小工具, 剛好可以拿來當學習 Powershell 撰寫腳本檔的範例, 本篇就以nl 為目標。
幫文字檔編行號的 nl
nl 是個可以幫文字檔編行號的小工具, 如果你需要將原始碼放到文件上, 那這個小工具就可以幫上你的忙, 最簡單的用法就像是這樣:
$nltest.c1#include<locale.h>2#include<stdio.h>3#include<wchar.h>4intmain(){5char*loc=setlocale(LC_CTYPE,"C.UTF-8");6printf("%s\n",loc);7wchar_tstr[]=L"扣人心弦CD";8printf("total:%d bytes\n",sizeof(str));9wprintf(L"%ls is %ld chars.",str,wcslen(str));10}
預設的情況下它會以 6 位數從 1 開始編行號, 並且在行號後面加上定位鍵再顯示內容。如果你希望客製化輸出的格式, 可以使用以下常用的選項:
選項名稱 | 可能值 | 說明 |
---|---|---|
-s | 字串 | 行號與內文間的分隔字串, 預設是 "\t" |
-v | 數值 | 起始行號, 預設從 1 開始 |
-w | 數值 | 行號寬度,預設為 6 |
-n | 字串 | 對齊格式: rn:靠右對齊 (預設) rz:靠右對齊, 開頭補 0 ln:靠左對齊 |
-b | 字元 | a 每一行都編號 n 不加編號 t 非空白行才編號 (預設) |
上表並未列出所有的選項, 有興趣可自行參考, 下一節 Powershell 的實作也僅以上述選項為準。
Powershell 的簡易實作
在實作 Powershell 版本的時候, 我們盡量簡化內容, 這樣才符合小工具的稱呼。
選項的定義
以下是 Powershell 版本 nl 工具的選項定義:
param([Parameter(ValueFromRemainingArguments=$True,position=0)][alias("path")]$pathes,# all unnames Parameter[int]$w=6,# digits width[Parameter(ValueFromPipeline=$true)][String[]]$lines,# pipelined input[String]$s="`t", # separator [ValidateScript({$_ -ge 0})][int]$v=1, # starting number [ValidateSet("ln","rn","rz")][String]$n="rn", # adjustment [ValidateSet("a","t","n")][String]$b="t" # number style)
Powershell 的好處是可以直接用定義變數的方式定義命令行的選項, 它會幫你剖析命令行, 取出個別的選項轉換成正確的型別後設定給變數。因此, 如果這樣執行腳本檔:
.\nl.ps1-s"--"
變數$s
的內容就是 "--", 而且還可以直接設定預設值, 如果像是這樣執行腳本檔:
.\ml.ps1
那麼$s
就會是預設的 "`t"。
選項定義時還可以指定驗證方式, 這裡我們使用了兩種驗證方式:
[ValidateSet("ln","rn","rz")][String]$n="rn"
這表示-n
選項只能接受 "ln","rn","rz" 其中的一種。
[ValidateScript({$_-ge 0})][int]$v=1
這表示-v
選項的值可由括號內的程式區塊來驗證, 這裡就是很簡單的確認參值是 0 或正整數。
你也可以指定哪個選項可以接收從管線傳入的資料:
[Parameter(ValueFromPipeline=$true)][String[]]$lines,# pipelined input
這裡$lines
可以依序接受從管線傳入的一行行字串。
為了讓這個小工具符合 Powershell 的慣例, 採用-path
指定檔名, 並且指定位置編號為 0, 表示是第一個位置選項, 同時加上ValueFromRemainingArguments=$True
的屬性, 剩餘沒有指定選項名稱的選項就會自動集合成一個陣列對應到-path
。
處理選項與格式化字串
要注意的是, 因為我們的腳本檔可以接受從管線輸入的字串, 所以必須要採用begin/prcoess/end
程式區塊,begin
和end
都只會執行一次, 但對於收到的每一個字串, 都會執行一次process
。
我們在begin
中根據選項設定必要的變數:
begin{$paddind=""# defualt no paddingif($n-eq"rz"){$paddind=":d$w"}# right adjustment with zero paddingif($n-eq"ln"){$w= -$w}# left adjustment$curr= 0# absolute start number
-n
選項決定是否要加上-f
格式化運算器中可在數字左方補零的 "d" 格式, 以及是否要將寬度變成負數, 讓數字向左對齊。
接著定義根據選項輸出單一行的工具函式:
functionprintLine{ param([String]$line)if($line-eq""-and$b-eq"t"){# -b t: nonempty lines write-host""}else{$numbers=($v +$script:curr)# -b a: all linesif($b-eq'n'){$numbers=""}# -b n: no numbers"{0,$w$paddindh}$s{1}"-f$numbers,$line$script:curr += 1}}}
- 如果是空白行且有指定
-b t
選項, 就單純印出空白行, 不加行號。 - 如果並非上述狀況, 再根據是否有指定
-b n
選項決定要不要加上行號。 - 最後根據選項使用
-f
格式化運算器幫我們編排這一行內容。
處理從管線收到的陣列
由於 nl 是以命令列選項優先, 有指定檔名的前提下並不會處理管線送來的內容, 所以在process
中會先確認$pathes
陣列內的元素數量:
process{if($pathes.count-eq 0){# if no pathes specifiedif($lines.count-gt 0){# check if there's any pipelined input printLine$lines[0]}}}
接著要判斷是否真的有收到管線送來的內容, 這是因為即使沒有管線來的資料,process
區塊也會執行一次, 如果不做判斷, 就會多輸出一行空白行, 讓結果不正確。我們特別定義以陣列接收管線資料, 這樣當沒有資料從管線送來時, 陣列內的元素數量就會是 0, 如此就可以區別是否有從管線接收到資料。
最後透過剛剛定義的printLine
工具函式輸出收到的這一行內容。
處理命令列指定的檔案與萬用字元
在end
中就依序處理命令列中指定的各個檔案:
end{ foreach($pathin$pathes){$allPathes= get-item$path foreach($filenamein$allPathes){if(test-path-pathtype leaf$filename){$contents= get-content-path$filename foreach($linein$contents){ printLine$line}} elseif(test-path-pathtype container$filename){ write-error("nl :{0}: Is a directory"-f$filename)}else{ write-error("nl :{0}: No such file"-f$filename)}}}}
- 為了讓使用者可以在檔案名稱中使用萬用字元, 先以
get-item
幫我們處理萬用字元, 取得所有的檔案清單。 - 接著針對檔案清單一一處理, 首先使用
test-path
的-pathtype leaf
參數確認指定的檔案存在, 而且不是資料夾, 就將檔案內容讀入, 一一輸出每一行。 - 如果透過
get-path
加上-pathtype container
發現指定的檔名是資料夾, 就輸出錯誤訊息。 - 如果指定的檔案不存在, 也送出錯誤訊息。
完整程式
param([Parameter(ValueFromRemainingArguments=$True,position=0)][alias("path")]$pathes,# all unnames Parameter[int]$w=6,# digits width[Parameter(ValueFromPipeline=$true)][String[]]$lines,# pipelined input[String]$s="`t", # separator [ValidateScript({$_ -ge 0})][int]$v=1, # starting number [ValidateSet("ln","rn","rz")][String]$n="rn", # adjustment [ValidateSet("a","t","n")][String]$b="t" # number style)begin{$paddind = "" # defualt no padding if($n -eq "rz"){$paddind = ":d$w"} # right adjustment with zero padding if($n -eq "ln"){$w = -$w} # left adjustment$curr = 0 # absolute start number function printLine { param( [String]$line ) if($line -eq "" -and$b -eq "t") { # -b t: nonempty lines write-host "" } else {$numbers = ($v +$script:curr) # -b a: all lines if($b -eq 'n') {$numbers = ""} # -b n: no numbers "{0,$w$paddindh}$s{1}" -f$numbers,$line$script:curr += 1 } }}process{ if($pathes.count -eq 0) { # if no pathes specified if($lines.count -gt 0) { # check if there's any pipelined input printLine$lines[0] } }}end{ foreach($path in$pathes) {$allPathes = get-item$path foreach($filename in$allPathes) { if(test-path -pathtype leaf$filename) {$contents = get-content -path$filename foreach($line in$contents) { printLine$line } } elseif (test-path -pathtype container$filename){ write-error ("nl :{0}: Is a directory" -f$filename) } else { write-error ("nl :{0}: No such file" -f$filename) } } }}
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse