DEV Community

codemee
codemee

Posted on

仿 Linux 的 Powershell 小工具--wc

wc 也是另一個 *nix 世界常用的小工具, 也很適合當成模擬實作的對象。

統計文字的工具--wc

最簡單的用法類似這樣:

$ wc *.txt 0 1 5 a.txt 1 1 6 hello.txt 1 1 6 oneline_pico.txt 1 1 6 oneline.txt 2 1 6 test1.txt 1 3 14 test.txt 6 8 43 total 
Enter fullscreen mode Exit fullscreen mode

預設它會幫你統計個別檔案的行數、單字數、位元組數, 以及全部檔案的總計數。其中單字指的是以空白類字元區隔開的連續字元, 例如 "hello world" 就有 2 個單字。你也可以透過選項指定要顯示哪一項統計數字:

選項 說明
-c
-m 字元數
-l 行數
-w 單字數

如果只有單一檔案, 因為總計數和此檔案的計數相同, 就不會印出總計數。如果是從管線送入資料, 則沒有檔案名稱, 最後一欄就不會顯示檔名。

Powershell 的簡易實作

以下實作和仿 Linux 的 Powershell 小工具--nl 一文類似, 重複的部分不再詳述, 僅針對特別處說明。

命令列選項

以下是命令列參數的定義:

param( [Parameter(ValueFromRemainingArguments=$True, position=0)] [alias("path")]$pathes, # all unnames Parameter [switch]$c=$false, # bytes count [Parameter(ValueFromPipeline=$true)][String[]]$txt, [switch]$m=$false, # chars count [switch]$l=$false, # lines count [switch]$w=$false # words count ) 
Enter fullscreen mode Exit fullscreen mode

個別變數對應到上述表格中的選項, 並同樣可從管線接收資料。

變數初值與預設選項

首先設定變數的初值, 由於個別計數欄位的寬度要由總計數來決定, 因此必須先算出總計數後才能輸出結果, 所以我們建立了 4 個以 all 開頭命名的陣列來儲存每一個檔案的各項計數, 以便能在得到總計數後輸出個別檔案的計數值, 這 4 個陣列一開始都設為空的陣列。另外的 4 個變數則是用來記錄當前檔案的各項計數。我們也檢查沒有指定任何選項的時候, 啟用預設的選項顯示行數、單字數與位元組數:

begin{ $allLines = $allChars = $allBytes = $allWords = @() $lines = $words = $chars = $bytes = 0 if(-not ($m -or $c -or $l -or $w)) { # no switches on $c = $l = $w = $true # turn on default swtches } } 
Enter fullscreen mode Exit fullscreen mode

處理從管線輸入的資料

如果在命令列沒有指定檔案名稱, 而且管線有資料傳入, 就以管線資料為處理對象:

process{ if($pathes.count -eq 0) { # if no pathes specified if($txt.count -gt 0) { # check if there's any pipelined input $txt[0] += [Environment]::NewLine $lines += 1 $chars += $txt[0].length $all = $txt[0] | select-string -allmatches -pattern "[^\s]+" $words += $all.matches.length if($PSDefaultParameterValues['Out-File:Encoding']) { $enc = $PSDefaultParameterValues['Out-File:Encoding'] } else { $enc = [System.Text.Encoding]::UTF8 } $bytesCurrLine = $enc.getbytecount($txt[0]) $bytes += $bytesCurrLine } } } 
Enter fullscreen mode Exit fullscreen mode
  1. 每一行文字先透過 [Environment]::NewLine 添加行尾的換行字元再計算。
  2. 單字的計算是透過 select-string 搜尋規則表達式, 找出輸入的資料中有多少段非空白類字元串接的文字。
  3. 位元組數的計算是假設以 out-file 輸出到檔案的大小來計算, 所以我們先確認使用者是否有變更過預設採用的輸出編碼, 若是沒有則採用 out-file 預設的 UTF8 編碼, 並叫用編碼物件的 GetByteCount() 來計算位元組數。

計算陣列總和與整數總位數的工具函式

由於輸出時我們需要所有檔案的總計數, 因此特別寫了一個利用 measure-object-sum 參數計算陣列內各元素總和的工具函式。

另外, 因為輸出時各欄位數字佔的寬度是由總計數的位數來決定, 因此我們也寫了一個利用 [math]::floor() 搭配 [math]::log() 計算位數的工具程式。計算時要特別留意, 0 的 log 值會是負無限值, 所以我們會把 0 用 1 替換計算 log 值, 以免出錯:

end{ function sum { # sum of a array param([int[]]$ary) return ($ary | measure-object -sum).sum } function digits { # digits of a integer param([int]$num) return ([math]::floor([math]::Log([math]::max($num, 1), 10)) + 1) } 
Enter fullscreen mode Exit fullscreen mode

依照指定路徑統計

接著判斷如果沒有在命令列指定檔案, 就將統計完的管線數據加入個別的陣列中, 否則就依照指定的檔案一一處理。

 if($pathes.count -eq 0) { # if no pathes specified $allBytes += $bytes $allWords += $words $allLines += $lines $allChars += $chars } else { $allPathes = @() foreach($path in $pathes) { $allPathes += get-item $path } foreach($filename in $allPathes) { $lines = $chars = $words = $bytes = 0 if(test-path -pathtype leaf $filename) { $bytes = (get-item $filename).length $contents = get-content -path $filename $lines = $contents.count $all = $contents | select-string -allmatches -pattern "[^\s]+" $words = $all.matches.length $contents = get-content -raw -path $filename $chars = $contents.length } $allBytes += $bytes $allWords += $words $allLines += $lines $allChars += $chars } } 
Enter fullscreen mode Exit fullscreen mode
  1. 首先透過 get-item 幫我們處理檔名中的萬用字元, 並從取得的檔案清單中一一檢查檔案是否存在, 而且不是資料夾, 並統計檔案內的文字。
  2. 檔案的位元組數可以直接透過 get-item 傳回物件的 length 屬性取得。
  3. 每個檔案統計完後就將數據加入陣列中記錄下來。

計算總數以及個別欄位寬度

所有檔案都處理完成後, 就可以利用前面介紹過的工具函式計算總和, 並且推算欄位寬度, 其中行數的前面會多空 2 格、其餘欄位則是與前一個欄位相隔一個空格:

 $totalBytes = sum $allBytes $totalChars = sum $allChars $totalWords = sum $allWords $totalLines = sum $allLines $wLine = (digits $totalLines) + 2 $wWord = (digits $totalWords) + 1 $wChar = (digits $totalChars) + 1 $wByte = (digits $totalBytes) + 1 
Enter fullscreen mode Exit fullscreen mode

輸出結果

最後輸出結果, 如果該檔名是資料夾, 會顯示錯誤的訊息, 而不會遞迴進入該資料夾內統計數據。若是不存在的檔案, 也一樣顯示錯誤訊息。若非以上兩者, 就依照命令列選項以指定的格式輸出統計數據:

 for($i = 0; $i -lt $allPathes.count;$i++) { if(test-path -pathtype container $allPathes[$i]) { "wc: {0}: Is a directory" -F $allPathes[$i].name continue } if(-not (test-path $allPathes[$i])) { "wc: {0}: No such file or directory" -F $allPathes[$i].name continue } if($l) {$txt = "{0, $wLine}" -F $allLines[$i]} if($w) {$txt += "{0, $wWord}" -F $allWords[$i]} if($m) {$txt += "{0, $wChar}" -F $allChars[$i]} if($c) {$txt += "{0, $wByte}" -F $allBytes[$i]} $txt += " {0}" -F $allPathes[$i].name write-host $txt } if($l) {$txt = "{0, $wLine}" -F $totalLines} if($w) {$txt += "{0, $wWord}" -F $totalWords} if($m) {$txt += "{0, $wChar}" -F $totalChars} if($c) {$txt += "{0, $wByte}" -F $totalBytes} if($allPathes.count -gt 1) {$txt += " total"} if(($allPathes.count -gt 1) -or ($totalLines -gt 0)) { write-host $txt } } 
Enter fullscreen mode Exit fullscreen mode

要特別留意的是, 如果是從管線輸入, 就不會顯示檔名。另外, 如果只有單一檔案, 就不會顯示總計數。

完整程式

完整的程式如下:

param( [Parameter(ValueFromRemainingArguments=$True, position=0)] [alias("path")]$pathes, # all unnames Parameter [switch]$c=$false, # bytes count [Parameter(ValueFromPipeline=$true)][String[]]$txt, [switch]$m=$false, # chars count [switch]$l=$false, # lines count [switch]$w=$false # words count ) begin{ $allLines = $allChars = $allBytes = $allWords = @() $lines = $words = $chars = $bytes = 0 if(-not ($m -or $c -or $l -or $w)) { # no switches on $c = $l = $w = $true # turn on default swtches } } process{ if($pathes.count -eq 0) { # if no pathes specified if($txt.count -gt 0) { # check if there's any pipelined input $txt[0] += [Environment]::NewLine $lines += 1 $chars += $txt[0].length $all = $txt[0] | select-string -allmatches -pattern "[^\s]+" $words += $all.matches.length if($PSDefaultParameterValues['Out-File:Encoding']) { $enc = $PSDefaultParameterValues['Out-File:Encoding'] } else { $enc = [System.Text.Encoding]::UTF8 } $bytesCurrLine = $enc.getbytecount($txt[0]) $bytes += $bytesCurrLine } } } end{ function sum { # sum of a array param([int[]]$ary) return ($ary | measure-object -sum).sum } function digits { # digits of a integer param([int]$num) return ([math]::floor([math]::Log([math]::max($num, 1), 10)) + 1) } if($pathes.count -eq 0) { # if no pathes specified $allBytes += $bytes $allWords += $words $allLines += $lines $allChars += $chars } else { $allPathes = @() foreach($path in $pathes) { $allPathes += get-item $path } foreach($filename in $allPathes) { $lines = $chars = $words = $bytes = 0 if(test-path -pathtype leaf $filename) { $bytes = (get-item $filename).length $contents = get-content -path $filename $lines = $contents.count $all = $contents | select-string -allmatches -pattern "[^\s]+" $words = $all.matches.length $contents = get-content -raw -path $filename $chars = $contents.length } $allBytes += $bytes $allWords += $words $allLines += $lines $allChars += $chars } } $totalBytes = sum $allBytes $totalChars = sum $allChars $totalWords = sum $allWords $totalLines = sum $allLines $wLine = (digits $totalLines) + 2 $wWord = (digits $totalWords) + 1 $wChar = (digits $totalChars) + 1 $wByte = (digits $totalBytes) + 1 for($i = 0; $i -lt $allPathes.count;$i++) { if(test-path -pathtype container $allPathes[$i]) { "wc: {0}: Is a directory" -F $allPathes[$i].name continue } if(-not (test-path $allPathes[$i])) { "wc: {0}: No such file or directory" -F $allPathes[$i].name continue } if($l) {$txt = "{0, $wLine}" -F $allLines[$i]} if($w) {$txt += "{0, $wWord}" -F $allWords[$i]} if($m) {$txt += "{0, $wChar}" -F $allChars[$i]} if($c) {$txt += "{0, $wByte}" -F $allBytes[$i]} $txt += " {0}" -F $allPathes[$i].name write-host $txt } if($l) {$txt = "{0, $wLine}" -F $totalLines} if($w) {$txt += "{0, $wWord}" -F $totalWords} if($m) {$txt += "{0, $wChar}" -F $totalChars} if($c) {$txt += "{0, $wByte}" -F $totalBytes} if($allPathes.count -gt 1) {$txt += " total"} if(($allPathes.count -gt 1) -or ($totalLines -gt 0)) { write-host $txt } } 
Enter fullscreen mode Exit fullscreen mode

Top comments (0)