Linxu 世界中有許多有趣的小工具, 剛好可以拿來當學習 Powershell 撰寫腳本檔的範例, 本篇就以 nl 為目標。
幫文字檔編行號的 nl
nl 是個可以幫文字檔編行號的小工具, 如果你需要將原始碼放到文件上, 那這個小工具就可以幫上你的忙, 最簡單的用法就像是這樣:
$ nl test.c
1 #include <locale.h>
2 #include <stdio.h>
3 #include <wchar.h>
4 int main() {
5 char* loc = setlocale(LC_CTYPE, "C.UTF-8");
6 printf("%s\n", loc);
7 wchar_t str[] = L"扣人心弦CD";
8 printf("total:%d bytes\n", sizeof(str));
9 wprintf(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 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
-n
選項決定是否要加上 -f
格式化運算器中可在數字左方補零的 "d" 格式, 以及是否要將寬度變成負數, 讓數字向左對齊。
接著定義根據選項輸出單一行的工具函式:
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
}
}
}
- 如果是空白行且有指定
-b t
選項, 就單純印出空白行, 不加行號。 - 如果並非上述狀況, 再根據是否有指定
-b n
選項決定要不要加上行號。 - 最後根據選項使用
-f
格式化運算器幫我們編排這一行內容。
處理從管線收到的陣列
由於 nl 是以命令列選項優先, 有指定檔名的前提下並不會處理管線送來的內容, 所以在 process
中會先確認 $pathes
陣列內的元素數量:
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]
}
}
}
接著要判斷是否真的有收到管線送來的內容, 這是因為即使沒有管線來的資料, process
區塊也會執行一次, 如果不做判斷, 就會多輸出一行空白行, 讓結果不正確。我們特別定義以陣列接收管線資料, 這樣當沒有資料從管線送來時, 陣列內的元素數量就會是 0, 如此就可以區別是否有從管線接收到資料。
最後透過剛剛定義的 printLine
工具函式輸出收到的這一行內容。
處理命令列指定的檔案與萬用字元
在 end
中就依序處理命令列中指定的各個檔案:
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)
}
}
}
}
- 為了讓使用者可以在檔案名稱中使用萬用字元, 先以
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)