###### tags: `PowerShell` `Linux`
# 仿 Linux 的 Powershell 小工具--wc
[`wc`](https://linux.die.net/man/1/wc) 也是另一個 \*nix 世界常用的小工具, 也很適合當成模擬實作的對象。
## 統計文字的工具--wc
最簡單的用法類似這樣:
```shell
$ 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](/mr-VkQ-zTaih9Su45lAZ4A) 一文類似, 重複的部分不再詳述, 僅針對特別處說明。
### 命令列選項
以下是命令列參數的定義:
```
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 個變數則是用來記錄當前檔案的各項計數。我們也檢查沒有指定任何選項的時候, 啟用預設的選項顯示行數、單字數與位元組數:
```shell
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
}
}
```
### 處理從管線輸入的資料
如果在命令列沒有指定檔案名稱, 而且管線有資料傳入, 就以管線資料為處理對象:
```shell
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
}
}
}
```
1. 每一行文字先透過 [`[Environment]::NewLine`](https://learn.microsoft.com/zh-tw/dotnet/api/system.environment.newline?view=net-6.0) 添加行尾的換行字元再計算。
1. 單字的計算是透過 [`select-string`](https://ss64.com/ps/select-string.html) 搜尋規則表達式, 找出輸入的資料中有多少段非空白類字元串接的文字。
2. 位元組數的計算是假設以 [`out-file`](https://ss64.com/ps/out-file.html) 輸出到檔案的大小來計算, 所以我們先確認使用者是否有變更過預設採用的輸出編碼, 若是沒有則採用 `out-file` 預設的 UTF8 編碼, 並叫用編碼物件的 [`GetByteCount()`](https://learn.microsoft.com/en-us/dotnet/api/system.text.encoding.getbytecount?view=netframework-4.8) 來計算位元組數。
### 計算陣列總和與整數總位數的工具函式
由於輸出時我們需要所有檔案的總計數, 因此特別寫了一個利用 [`measure-object`](https://ss64.com/ps/measure-object.html) 的 `-sum` 參數計算陣列內各元素總和的工具函式。
另外, 因為輸出時各欄位數字佔的寬度是由總計數的位數來決定, 因此我們也寫了一個利用 [`[math]::floor()`](https://learn.microsoft.com/zh-tw/dotnet/api/system.math.floor?view=net-6.0) 搭配 [`[math]::log()`](https://learn.microsoft.com/zh-tw/dotnet/api/system.math.log?view=net-6.0) 計算位數的工具程式。計算時要特別留意, 0 的 log 值會是負無限值, 所以我們會把 0 用 1 替換計算 log 值, 以免出錯:
```shell
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)
}
```
### 依照指定路徑統計
接著判斷如果沒有在命令列指定檔案, 就將統計完的管線數據加入個別的陣列中, 否則就依照指定的檔案一一處理。
```shell
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
}
}
```
1. 首先透過 `get-item` 幫我們處理檔名中的萬用字元, 並從取得的檔案清單中一一檢查檔案是否存在, 而且不是資料夾, 並統計檔案內的文字。
2. 檔案的位元組數可以直接透過 `get-item` 傳回物件的 `length` 屬性取得。
3. 每個檔案統計完後就將數據加入陣列中記錄下來。
### 計算總數以及個別欄位寬度
所有檔案都處理完成後, 就可以利用前面介紹過的工具函式計算總和, 並且推算欄位寬度, 其中行數的前面會多空 2 格、其餘欄位則是與前一個欄位相隔一個空格:
```shell
$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
```
### 輸出結果
最後輸出結果, 如果該檔名是資料夾, 會顯示錯誤的訊息, 而不會遞迴進入該資料夾內統計數據。若是不存在的檔案, 也一樣顯示錯誤訊息。若非以上兩者, 就依照命令列選項以指定的格式輸出統計數據:
```shell
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
}
}
```
要特別留意的是, 如果是從管線輸入, 就不會顯示檔名。另外, 如果只有單一檔案, 就不會顯示總計數。
## 完整程式
完整的程式如下:
```shell
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
}
}
```