仿 Linux 的 Powershell 小工具--wc
codemee
Posted on October 21, 2022
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
}
}
Posted on October 21, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 22, 2023