DEV Community

codemee
codemee

Posted on • Edited on

仿 Linux 的 Powershell 小工具--nl

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  }
Enter fullscreen mode Exit fullscreen mode

預設的情況下它會以 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
)
Enter fullscreen mode Exit fullscreen mode

Powershell 的好處是可以直接用定義變數的方式定義命令行的選項, 它會幫你剖析命令行, 取出個別的選項轉換成正確的型別後設定給變數。因此, 如果這樣執行腳本檔:

.\nl.ps1 -s "--"
Enter fullscreen mode Exit fullscreen mode

變數 $s 的內容就是 "--", 而且還可以直接設定預設值, 如果像是這樣執行腳本檔:

.\ml.ps1
Enter fullscreen mode Exit fullscreen mode

那麼 $s 就會是預設的 "`t"。

選項定義時還可以指定驗證方式, 這裡我們使用了兩種驗證方式:

[ValidateSet("ln","rn","rz")][String]$n="rn"
Enter fullscreen mode Exit fullscreen mode

這表示 -n 選項只能接受 "ln","rn","rz" 其中的一種。

[ValidateScript({$_ -ge 0})][int]$v=1
Enter fullscreen mode Exit fullscreen mode

這表示 -v 選項的值可由括號內的程式區塊來驗證, 這裡就是很簡單的確認參值是 0 或正整數。

你也可以指定哪個選項可以接收從管線傳入的資料:

[Parameter(ValueFromPipeline=$true)][String[]]$lines, # pipelined input
Enter fullscreen mode Exit fullscreen mode

這裡 $lines 可以依序接受從管線傳入的一行行字串。

為了讓這個小工具符合 Powershell 的慣例, 採用 -path 指定檔名, 並且指定位置編號為 0, 表示是第一個位置選項, 同時加上 ValueFromRemainingArguments=$True的屬性, 剩餘沒有指定選項名稱的選項就會自動集合成一個陣列對應到 -path

處理選項與格式化字串

要注意的是, 因為我們的腳本檔可以接受從管線輸入的字串, 所以必須要採用 begin/prcoess/end 程式區塊, beginend 都只會執行一次, 但對於收到的每一個字串, 都會執行一次 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
Enter fullscreen mode Exit fullscreen mode

-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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. 如果是空白行且有指定 -b t 選項, 就單純印出空白行, 不加行號。
  2. 如果並非上述狀況, 再根據是否有指定 -b n 選項決定要不要加上行號。
  3. 最後根據選項使用 -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]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

接著要判斷是否真的有收到管線送來的內容, 這是因為即使沒有管線來的資料, 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)
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. 為了讓使用者可以在檔案名稱中使用萬用字元, 先以 get-item 幫我們處理萬用字元, 取得所有的檔案清單。
  2. 接著針對檔案清單一一處理, 首先使用 test-path-pathtype leaf 參數確認指定的檔案存在, 而且不是資料夾, 就將檔案內容讀入, 一一輸出每一行。
  3. 如果透過 get-path 加上 -pathtype container 發現指定的檔名是資料夾, 就輸出錯誤訊息。
  4. 如果指定的檔案不存在, 也送出錯誤訊息。

完整程式

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)
      }
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

Top comments (0)

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay