計算 UNC 路徑下分享資料夾的總用量
Tags: batch, cmd, dos
Published: 2023-01-22 14:23:24

有人問了我在 Windows 下如何得到某個機器下分享的資料夾總用量？由於單一機器分享的不只是資料夾, 並不允許直接用 [DIR /s](https://ss64.com/nt/dir.html) 指令列出所有資料夾, 因此就必須花點功夫寫個批次檔來處理。

## 取得 UNC 路徑下所有的分享資料夾清單

首先, 我們得知道 UNC 路徑下有哪些資源是資料夾, 這可以透過 [NET VIEW](https://ss64.com/nt/net-view.html) 指令取得, 例如：

```cmd
>net view \\192.168.0.217
共用資源在 \\192.168.0.217



共用名稱   類型   使用方式  註解

-------------------------------------------------------------------------------
books     Disk
CPP       Disk
命令已經成功完成。
```

其中, 類型為 'Disk' 的就是分享的資料夾, 可以透過 DIR /s 直接取得總用量, 例如：

```cmd
>dir /s \\192.168.0.217\cpp
 磁碟區 \\192.168.0.217\cpp 中的磁碟是 Data
 磁碟區序號:  9446-A1B0

 \\192.168.0.217\cpp 的目錄

2022/12/28  下午 02:15    <DIR>          .
2023/01/20  下午 06:16    <DIR>          ..
2022/04/08  下午 04:15    <DIR>          .ipynb_checkpoints
...
 \\192.168.0.217\cpp\test_ini\output 的目錄

2022/05/03  下午 01:42    <DIR>          .
2022/05/03  下午 01:42    <DIR>          ..
2022/05/03  下午 01:42               240 foo.obj
2022/05/03  下午 01:42               756 main.obj
               2 個檔案             996 位元組

     檔案數目總計:
             291 個檔案      82,151,599 位元組
             149 個目錄  30,557,925,376 位元組可用
```

DIR /s 最後會列出總計的檔案數量以及總用量, 所以只要針對清單中的個別資料夾一一取得用量, 加總後就可以知道所有分享資料夾的總用量了。

以下就是依據此概念撰寫的批次檔：

```cmd
@echo off
Setlocal EnableDelayedExpansion

set /a total=0
@rem net view 可以顯示 UNC 路徑下的所有分享資源
@rem 每一行都是 資源名稱 資源類型 的格式
for /f "tokens=1,2" %%l in ('net view %1') do (
  @REM -----------------------------------
  @REM    1(l)   2(m)
  @REM 共用名稱   類型  使用方式  註解
  @REM books     Disk
  @REM CPP       Disk  
  @REM -----------------------------------
  @rem 資源類型為 Disk 的就是資料夾
  if "%%m"=="Disk" (
    set full_path=%1\%%l
    set get_total=F
    @rem dir /s 可以遞迴列出子資料夾與檔案並統計總用量
    for /f "tokens=1,3" %%p in ('dir /s !full_path!') do (
      @rem -------------------------------------------------
      @rem         1(p)
      @REM  檔案數目總計:
      @rem         1(p)     2            3(q)
      @REM          291 個檔案      82,151,599 位元組
      @REM          149 個目錄  30,767,378,432 位元組可用
      @rem -------------------------------------------------
      @rem 已經到了倒數第二行, 也就是總計用量
      if "!get_total!"=="T" (
        set subtotal=%%q
        @rem 將顯示用量中的 ',' 去掉
        echo %%l 用量：!subtotal:,=!
        set /a total+=!subtotal:,=!
        set get_total=F
      )
      @rem dir /s 倒數第三行是『檔案數目總計:』
      if "%%p"=="檔案數目總計:" (
        set get_total=T
      )
    )
  )
)
echo 總用量：%total%
```

外層 for 迴圈會從 NET VIEW 的輸出結果中逐行取出以空格隔開的第一欄 (資源名稱) 與第二欄 (類型), 如果第二欄 (類型)是 'Disk', 就以內層的 for 迴圈透過 DIR /s 指令取得資料夾內容。由於我想要的是最後倒數第二行的總計, 所以先以 if 判斷前一行是否為 "檔案數目總計：", 即可取出位在以空格隔開的第三欄的總計位元組數了。

由於總用量的數字每三位會以逗號區隔, 但 [SET /a](https://ss64.com/nt/set.html#expressions) 不接受這樣的數值格式, 因此我們也會先用字串取代的方式, 將逗號去除。如果你覺得很麻煩, 也可以在 DIR 時加上 /-c 選項, 就可以用不加逗號的格式顯示數值。

實際執行會像是這樣：

```cmd
>list_shared_folders2 \\192.168.0.217
books 用量：71280427
CPP 用量：82151599
總用量：153432026
```

## 無法自動取得子資料夾的神奇分享資料夾

上述批次檔看起來似乎運作的很好, 不過實際上卻遇到一個奇怪的問題, 有些分享資料夾在使用 DIR /s 的時候, 就只會列出第一層, 不會進入子資料夾, 我不確定這是不是因為 NAS 的關係, 但是同一台機器上的其他分享資料夾又都正常。

在我無法動到遠端不歸我管的 NAS 或是伺服器的情況下, 只好改變策略, 針對分享資料夾內的個別資料夾取得用量後再加總, 這個版本的批次檔如下：

```cmd
@echo off
Setlocal EnableDelayedExpansion

set /a total=0
@rem net view 可以顯示 UNC 路徑下的所有分享資源
@rem 每一行都是 資源名稱 資源類型 的格式
for /f "tokens=1,2" %%l in ('net view %1') do (
  @rem 資源類型為 Disk 的就是資料夾
  if "%%m"=="Disk" (
    set full_path=%1\%%l
    @rem dir /s 預設為包含隱藏檔, 若沒有 /s 則不會
    for /f "tokens=2,3,4,5" %%f in ('dir /a/-c !full_path!') do (
      @rem --------------------------------------------------
      @rem                1  2(f)            3(g)  4(h)
      @rem               43 個檔案         4938370 位元組
      @rem --------------------------------------------------
      if "%%f"=="個檔案" (
        echo !full_path! 用量：%%g
        set /a total+=%%g
      )
      @rem -------------------------------------------------
      @rem          1  2(f)  3(g)     4(h)              5(i)
      @rem 2022/05/03  下午 01:42    <DIR>          test_ini
      @rem --------------------------------------------------
      if "%%h"=="<DIR>" (
        if not "%%i"=="." (
          if not "%%i"==".." (
            set sub_path=!full_path!\%%i
            @rem dir /s 可以遞迴列出子資料夾與檔案並統計總用量
            set get_total=F
            for /f "tokens=1,3" %%p in ('dir /s /-c !sub_path!') do (
              @rem -------------------------------------------------
              @rem         1(p)
              @REM  檔案數目總計:
              @rem         1(p)     2            3(q)
              @REM         3781 個檔案        71280427 位元組
              @REM         1501 個目錄     30558121984 位元組可用          
              @rem -------------------------------------------------
              @rem 已經到了倒數第二行, 也就是總計用量
              if "!get_total!"=="T" (
                echo !sub_path! 子資料夾用量：%%q
                set /a total+=%%q
                set get_total=F
              )
              @rem dir /s 倒數第三行是『檔案數目總計:』
              if "%%p"=="檔案數目總計:" (
                set get_total=T
              )
            )
          )
        )
      )
    )  
  )
)
echo 總用量：%total%
```

在第二層的 for 迴圈要特別注意, 必須以 [DIR /a](https://ss64.com/nt/dir.html) 才能顯示隱藏的資料夾, 否則會漏計用量。另外, 也要記得把最上層的分享資料夾自己的用量加進來, 不然就只會計算到分享的資料夾底下子資料夾的用量。

第三層的 for 迴圈就跟前一版的批次檔一樣, 使用 DIR /s 計算連同子資料夾的總用量, 不再贅述。

這一版也在 DIR 時加上 /-c 選項, 不用逗號區隔數字, 執行結果如下：

```cmd
>list_shared_folders \\192.168.0.217
\\192.168.0.217\books\.git 子資料夾用量：30185953
\\192.168.0.217\books\.vscode 子資料夾用量：0
\\192.168.0.217\books\old 子資料夾用量：13204
\\192.168.0.217\books\python 子資料夾用量：41051833
\\192.168.0.217\books 用量：29437
\\192.168.0.217\CPP\.ipynb_checkpoints 子資料夾用量：144
\\192.168.0.217\CPP\.vscode 子資料夾用量：46791
\\192.168.0.217\CPP\blink 子資料夾用量：21016766
\\192.168.0.217\CPP\esp32_times 子資料夾用量：350
\\192.168.0.217\CPP\fb_test 子資料夾用量：104207
\\192.168.0.217\CPP\intro 子資料夾用量：55832596
\\192.168.0.217\CPP\intro_define.cpp 子資料夾用量：802
\\192.168.0.217\CPP\mdp 子資料夾用量：19863
\\192.168.0.217\CPP\test 子資料夾用量：59625
\\192.168.0.217\CPP\test_ext 子資料夾用量：3889
\\192.168.0.217\CPP\test_extern 子資料夾用量：61501
\\192.168.0.217\CPP\test_ini 子資料夾用量：66695
\\192.168.0.217\CPP 用量：4938370
總用量：153432026
```

你可以看到它會先取得分享資料夾內個別資料夾的用量, 再加上分享資料夾本身的用量, 運作正常。

## SET /a 的限制：32 位元整數

剛剛的批次檔測試後可以正常運作, 但是有一個大問題, 就是 SET /a 只能處理 32 位元的整數, 設定時只能接受最大 2147483647, 單一資料夾的用量一大, 就爆掉了, 類似這樣：

```cmd
>set /a a=2147483647
2147483647
>set /a a=2147483648
不正確的數字。數字限制為 32 位元精確度。
```

這個問題有兩個解法, 一是改用 Powershell, 即可使用 .net 的 long 來計算；另一種是自己將 DIR 取得的數字分段, 個別計算並處理進位即可, 以下就是我新修改的程式：

```cmd
@echo off
Setlocal EnableDelayedExpansion

@rem net view 可以顯示 UNC 路徑下的所有分享資源
@rem 每一行都是 資源名稱 資源類型 的格式
for /f "tokens=1,2" %%l in ('net view %1') do (
  @rem 資源類型為 Disk 的就是資料夾
  if "%%m"=="Disk" (
    set full_path=%1\%%l
    @rem dir /s 預設為包含隱藏檔, 若沒有 /s 則不會
    for /f "tokens=2,3,4,5" %%f in ('dir /a/-c !full_path!') do (
      @rem --------------------------------------------------
      @rem                1  2(f)            3(g)  4(h)
      @rem               43 個檔案         4938370 位元組
      @rem --------------------------------------------------
      if "%%f"=="個檔案" (
        echo !full_path! 用量：%%g
        call :uni_add %%g
      )
      @rem -------------------------------------------------
      @rem          1  2(f)  3(g)     4(h)              5(i)
      @rem 2022/05/03  下午 01:42    <DIR>          test_ini
      @rem --------------------------------------------------
      if "%%h"=="<DIR>" (
        if not "%%i"=="." (
          if not "%%i"==".." (
            set sub_path=!full_path!\%%i
            @rem dir /s 可以遞迴列出子資料夾與檔案並統計總用量
            set get_total=F
            for /f "tokens=1,3" %%p in ('dir /s /-c !sub_path!') do (
              @rem -------------------------------------------------
              @rem         1(p)
              @REM  檔案數目總計:
              @rem         1(p)     2            3(q)
              @REM         3781 個檔案        71280427 位元組
              @REM         1501 個目錄     30558121984 位元組可用          
              @rem -------------------------------------------------
              @rem 已經到了倒數第二行, 也就是總計用量
              if "!get_total!"=="T" (
                echo !sub_path! 子資料夾用量：%%q
                call :uni_add %%q
                set get_total=F
              )
              @rem dir /s 倒數第三行是『檔案數目總計:』
              if "%%p"=="檔案數目總計:" (
                set get_total=T
              )
            )
          )
        )
      )
    )  
  )
)
echo 總用量：%tot_s%
goto:eof

:uni_add
set num=%~1
@rem 取得位數
for /l %%d in (0, 1, 98) do (
  if not "!num:~%%d,1!" == "" (
    set /a digits=%%d + 1
  )
)
@rem 每三位數一個單位會剩下幾位數
set /a remain_digits=digits%%3
@rem 三位數一個單位共佔幾位數
set /a r_digit=digits - remain_digits
@rem 進位數值
set /a carry=0
@rem 依序取得每三位的數字
for /l %%d in (-3, -3, -!r_digit!) do (
  set sub_sum[%%d]=!num:~%%d,3!
)
set /a r_digit+=3
set /a r_digit=-r_digit
@rem 取得不足三位的最左邊剩餘數字
for %%d in (!remain_digits!) do (
  @REM echo !num:~0,%%d!
  set sub_sum[!r_digit!]=!num:~0,%%d!
)

@rem 循序三位一組將小計與目前總計相加
for /l %%d in (-3, -3, !r_digit!) do (
  if "!totals[%%d]!"=="" (
    set totals[%%d]=!sub_sum[%%d]!
    set /a carry=0
  ) else (
    set /a temp=1000!sub_sum[%%d]!%%1000+1000!totals[%%d]!%%1000+carry
    set /a carry=temp/1000
    set /a temp=temp%%1000
    set totals[%%d]=!temp!
  )
)
@rem 處理進位
if !carry! gtr 0 (
  set /a r_digit-=3
  for /l %%d in (!r_digit!, -3, !max_digit!) do (
    @rem 由於 0 開頭的數字會被當成 8 進位
    @rem 這裡採取加 1000000 再取除以 1000 的餘數來避免
    set /a temp=1000!totals[%%d]!%%1000+carry
    set /a carry=temp/1000
    set /a temp=temp%%1000
    set totals[%%d]=!temp!
  )
)
@rem 更新目前最高位數
if !max_digit! gtr !r_digit! set /a max_digit=r_digit
@rem 進位到新的一組三位數
if !carry! gtr 0 (
  set /a max_digit-=3
  for %%d in (!max_digit!) do (
    set totals[%%d]=carry
  )
)
@rem 用三位一組的格式顯示總計
set tot_s=
for /l %%d in (!max_digit!, 3, -3) do (
  set temp_s=1000!totals[%%d]!
  @rem 由於計算結果可能不到三位數
  @rem 這裡用特別的技巧補上開頭的 0
  set tot_s=!tot_s!,!temp_s:~-3,3!
)
set tot_s=!tot_s:~1!
exit /b
```

在程式最後新增了 uni_add 副程式, 它會把數字每三個一位分段, 個別當成數字進行加法, 並且處理進位。要特別留意的是, 因為我們是直接把文字每三位切開, 以 120051 為例, 會切開成 120 和 051, 但是因為批次檔的解譯器會把 0 開頭的數字以 8 進位來解釋, 如果不特別處理, 就會得到錯誤的結果, 例如：

```cmd
>set a=051

>set /a a=a+10
51
```

由於 051 被解釋成 8 進位, 所以變成 41 + 10, 結果就是奇怪的 41。為了避免這個問題, 我們採用[簡單的方式](https://www.dostips.com/forum/viewtopic.php?p=2619&sid=9cfa9ff47f3ae0504c0d0032985bd22a#p2619), 先在數字前面串接上 "1000", 然後將串接後的結果當成數值運算取除以 1000 的餘數, 就可以確保得到正確的 10 進位數值, 像是這樣：

```cmd
>set a=051

>set /a a=1000%a%%1000+10
61
```

這裡要特別留意, 前面串接的字串在 1 之後至少要有 3 個 0, 因為我們每個分段是三位數。另外, 也要記得在互動環境下取餘數只要寫一個 % 符號, 但是在批次檔中則是要寫兩個 % 符號。

當我們要把個別分段重組起來顯示的時候, 會遇到數字不足三位數的問題, 例如 26, 除非是在整個數字最左側, 否則就應該要顯示成 026, 不然會少掉一位數。這裡我們也採取類似的方式, 在前面先串接上 "1000", 然後從倒數第三個數字開始取三位數, 就可以自動補 0, 像是這樣：

```cmd
>set a=26

>set a=1000%a%

>echo %a:~-3%
026
```

## 結語

實際上如果遠端是 NAS, 我想應該都是有使用者介面可以查看, 但是在無法進入 NAS 管理系統的前提下, 還是得靠本文的苦力方式取得資訊。