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
預設它會幫你統計個別檔案的行數、單字數、位元組數, 以及全部檔案的總計數。其中單字指的是以空白類字元區隔開的連續字元, 例如 "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 )
個別變數對應到上述表格中的選項, 並同樣可從管線接收資料。
變數初值與預設選項
首先設定變數的初值, 由於個別計數欄位的寬度要由總計數來決定, 因此必須先算出總計數後才能輸出結果, 所以我們建立了 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 } }
處理從管線輸入的資料
如果在命令列沒有指定檔案名稱, 而且管線有資料傳入, 就以管線資料為處理對象:
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 } } }
- 每一行文字先透過
[Environment]::NewLine
添加行尾的換行字元再計算。 - 單字的計算是透過
select-string
搜尋規則表達式, 找出輸入的資料中有多少段非空白類字元串接的文字。 - 位元組數的計算是假設以
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) }
依照指定路徑統計
接著判斷如果沒有在命令列指定檔案, 就將統計完的管線數據加入個別的陣列中, 否則就依照指定的檔案一一處理。
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 } }
- 首先透過
get-item
幫我們處理檔名中的萬用字元, 並從取得的檔案清單中一一檢查檔案是否存在, 而且不是資料夾, 並統計檔案內的文字。 - 檔案的位元組數可以直接透過
get-item
傳回物件的length
屬性取得。 - 每個檔案統計完後就將數據加入陣列中記錄下來。
計算總數以及個別欄位寬度
所有檔案都處理完成後, 就可以利用前面介紹過的工具函式計算總和, 並且推算欄位寬度, 其中行數的前面會多空 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
輸出結果
最後輸出結果, 如果該檔名是資料夾, 會顯示錯誤的訊息, 而不會遞迴進入該資料夾內統計數據。若是不存在的檔案, 也一樣顯示錯誤訊息。若非以上兩者, 就依照命令列選項以指定的格式輸出統計數據:
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 } }
要特別留意的是, 如果是從管線輸入, 就不會顯示檔名。另外, 如果只有單一檔案, 就不會顯示總計數。
完整程式
完整的程式如下:
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 } }
Top comments (0)