読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

bat中でforループをネストし,サブルーチンを呼び出して,条件付きファイル検索の結果を一斉コピーしよう (ファイル名の重複防止機能付き)

コマンドプロンプト プログラミング


下記のような要望がある。

  • 特定のフォルダツリーの中から,batファイルで,Excelファイルを抽出したい。
  • サブディレクトリのフォルダ名は,スペースを含む場合がある。
  • 抽出対象のExcelファイルは,ファイル名の先頭に特定の「先頭ID」が付与されている。その先頭IDはリストにしてある。(=条件付きファイル検索)
  • 抽出後に,ファイル名がダブる場合もあり得るので,上書きせず別ファイルとなるように保存したい。
  • フォルダツリーはネットワーク上の共有フォルダである。(未確認)


この要望に応えるようなbatファイルをコーディングすると,
下記のようなプログラミング・テクニックを一気に使う事になる。

  • コマンドの実行結果を行ごとに引数に取るようなforループ。(bashでよくあるパターン)
  • forループのネスト制御構造。
  • 引数を渡したサブルーチンの呼び出し。
  • 変数のローカル化。
  • ファイルの存在判定のif文。
  • ループ制御構造にカウンタを導入し,条件を満たすまでの間,整数を加算する。


その結果,下記のようなバッチファイルが出来上がる。


移動.bat

@echo off

rem ファイルを一斉コピーするバッチ
rem 
rem ・コピー元のフォルダ内から,特定の拡張子のファイルを再帰的に検索。
rem ・コピー対象のファイル名の先頭に付与されているIDは,リストとして存在。
rem ・コピー先のファイル名が重複した場合,ファイル名の末尾に数値を付与して別名保存。
rem 
rem coded for the answer of: http://q.hatena.ne.jp/1319868767


rem 変数定義
set IDLIST=移動対象の先頭IDのリスト.txt
set FROM_DIR="コピー元"
set TO_DIR="コピー先"
set EXCEL_FILE_EXT="xls"

rem このバッチが存在するフォルダをカレントに
pushd %0\..

rem カレントを変数に保持
for /F "usebackq" %%i in (`cd`) do (
  set BAT_DIR="%%i"
)

rem 開始
echo 「%FROM_DIR%」内のファイルを全チェックします。
pushd "%FROM_DIR%"

rem 先頭IDごとに
for /f "usebackq" %%n in (`type %BAT_DIR%\%IDLIST%`) do (
  echo --------- 先頭IDが「%%n」であるようなファイルを移動します。 --------- 
  
  rem 発見したファイルごとに
  for /f "usebackq delims=" %%m in (`dir /s /b %%n*.%EXCEL_FILE_EXT%`) do (
    echo 移動対象ファイル「%%m」を発見しました。コピーします。
    
    rem ※「%%~nxm」は,拡張子込みのファイル名を表す。
    rem   http://fpcu.on.coocan.jp/dosvcmd/batch.htm#param
    
    rem コピー実行用のサブルーチンにファイル名と拡張子などを渡す
    call :ROUTINE_COPY_ONE_FILE  "%%m"  "%%~nm"  "%%~xm"  %BAT_DIR%  %TO_DIR%
    
      rem ※ネストしたforループの内側でgotoするとエラーになる。要サブルーチン
      rem   http://fpcu.on.coocan.jp/dosvcmd/bbs/log/cat3/for_in_do/4-1324.html
  )
)

rem 終了
echo --------- 全コピーが完了しました。コピー結果: --------- 
dir /b %BAT_DIR%\%TO_DIR%
echo -------------------------------------------------------- 

pause
exit



rem --------------------- サブルーチン ---------------------


rem コピー実行用のルーチン
:ROUTINE_COPY_ONE_FILE

  rem ルーチンの引数を取得
  setlocal
  set FILE_FULLPATH=%1
  set FILE_BASENAME=%2
  set FILE_EXT=%3
  set BAT_DIR=%4
  set TO_DIR=%5
    rem ※サブルーチン中でsetlocalすれば,
    rem   呼び出し元に影響なくローカル変数を使える
    rem   http://d.hatena.ne.jp/iroiro123/20110331/1301567381

  echo ---「%FILE_FULLPATH%」のコピー処理開始:

  rem 通常のコピー実行
  if exist %BAT_DIR%\%TO_DIR%\%FILE_BASENAME%%FILE_EXT% (
    echo 既にコピー済みです。コピー先ファイル名を変えて再試行します・・・
    goto SETUP_COPY_BY_NEW_NAME
  ) else (
    echo 未コピーです。
    copy %FILE_FULLPATH% %BAT_DIR%\%TO_DIR%\
    goto END_OF_ONE_COPY
  )
    rem ※複数行に渡るIF文の書き方
    rem   http://fpcu.on.coocan.jp/dosvcmd/bbs/log/cat3/if/4-0906.html


rem 重複するファイル名が存在する場合の初期化
:SETUP_COPY_BY_NEW_NAME

  rem 重複するファイルの末尾につける数値
  set /a DUP_FILE_COUNT=2
    rem ※算術変数は遅延展開され加算が可能
    rem   http://fpcu.on.coocan.jp/dosvcmd/bbs/log/cat3/4-0873.html
  
  goto BEGIN_COPY_BY_NEW_NAME


rem 重複を避けて新規ファイル名を生成する
:CREATE_NEW_NAME_BY_INCREMENT
    
  rem echo 数値をインクリメントします。
  set /A DUP_FILE_COUNT=%DUP_FILE_COUNT%+1


rem 新規ファイル名でコピーを試みる
:BEGIN_COPY_BY_NEW_NAME
  
  if exist %BAT_DIR%\%TO_DIR%\%FILE_BASENAME%%DUP_FILE_COUNT%%FILE_EXT% (
    echo %DUP_FILE_COUNT%回目:失敗。ファイルが存在するため,再試行します・・・
    
    rem 名前をつけ直し
    goto CREATE_NEW_NAME_BY_INCREMENT
  ) else (
    echo %DUP_FILE_COUNT%個目は未コピーです。
    
    rem コピー実行
    rem echo 新規ファイル名:%BAT_DIR%\%TO_DIR%\%FILE_BASENAME%%DUP_FILE_COUNT%%FILE_EXT%
    copy %FILE_FULLPATH% %BAT_DIR%\%TO_DIR%\%FILE_BASENAME%%DUP_FILE_COUNT%%FILE_EXT%
    goto END_OF_ONE_COPY
  )


rem ファイル名がどうなったにせよ,コピーは終了した
:END_OF_ONE_COPY
  echo ---「%FILE_FULLPATH%」のコピー処理終了。

exit /b
  rem サブルーチンからの脱出時には値も返せる
  rem http://logicalerror.seesaa.net/article/125905911.html

動作確認

上記プログラムはEXCEL_FILE_EXT変数の値でExcelファイルの拡張子を変更できる。

テスト時にはxlsではなくxlsxとして動作確認している。


事前の状態:

D:\temp\battest>tree /f
D:.
│  移動.bat
│  移動対象の先頭IDのリスト.txt
│
├─コピー元
│  │  hoge.xlsx
│  │  移動対象1_fuga.xlsx
│  │
│  ├─a
│  │      fuga.xlsx
│  │      移動対象1_fuga.xlsx
│  │      移動対象2_hoge.xlsx
│  │
│  └─スペースの テスト
│          fuga.xlsx
│          移動対象1_fuga.xlsx
│          移動対象2_hoge2.xlsx
│
└─コピー先


D:\temp\battest>type 移動対象の先頭IDのリスト.txt
移動対象1
移動対象2


実行ログ:

D:\temp\battest>移動.bat
「"コピー元"」内のファイルを全チェックします。
--------- 先頭IDが「移動対象1」であるようなファイルを移動します。 ---------
移動対象ファイル「D:\temp\battest\コピー元\移動対象1_fuga.xlsx」を発見しました。
コピーします。
---「"D:\temp\battest\コピー元\移動対象1_fuga.xlsx"」のコピー処理開始:
未コピーです。
        1 個のファイルをコピーしました。
---「"D:\temp\battest\コピー元\移動対象1_fuga.xlsx"」のコピー処理終了。
移動対象ファイル「D:\temp\battest\コピー元\a\移動対象1_fuga.xlsx」を発見しました
。コピーします。
---「"D:\temp\battest\コピー元\a\移動対象1_fuga.xlsx"」のコピー処理開始:
既にコピー済みです。コピー先ファイル名を変えて再試行します・・・
2個目は未コピーです。
        1 個のファイルをコピーしました。
---「"D:\temp\battest\コピー元\a\移動対象1_fuga.xlsx"」のコピー処理終了。
移動対象ファイル「D:\temp\battest\コピー元\スペースの テスト\移動対象1_fuga.xlsx
」を発見しました。コピーします。
---「"D:\temp\battest\コピー元\スペースの テスト\移動対象1_fuga.xlsx"」のコピー
処理開始:
既にコピー済みです。コピー先ファイル名を変えて再試行します・・・
2回目:失敗。ファイルが存在するため,再試行します・・・
3個目は未コピーです。
        1 個のファイルをコピーしました。
---「"D:\temp\battest\コピー元\スペースの テスト\移動対象1_fuga.xlsx"」のコピー
処理終了。
--------- 先頭IDが「移動対象2」であるようなファイルを移動します。 ---------
移動対象ファイル「D:\temp\battest\コピー元\a\移動対象2_hoge.xlsx」を発見しました
。コピーします。
---「"D:\temp\battest\コピー元\a\移動対象2_hoge.xlsx"」のコピー処理開始:
未コピーです。
        1 個のファイルをコピーしました。
---「"D:\temp\battest\コピー元\a\移動対象2_hoge.xlsx"」のコピー処理終了。
移動対象ファイル「D:\temp\battest\コピー元\スペースの テスト\移動対象2_hoge2.xls
x」を発見しました。コピーします。
---「"D:\temp\battest\コピー元\スペースの テスト\移動対象2_hoge2.xlsx"」のコピー
処理開始:
未コピーです。
        1 個のファイルをコピーしました。
---「"D:\temp\battest\コピー元\スペースの テスト\移動対象2_hoge2.xlsx"」のコピー
処理終了。
--------- 全コピーが完了しました。コピー結果: ---------
移動対象1_fuga.xlsx
移動対象1_fuga2.xlsx
移動対象1_fuga3.xlsx
移動対象2_hoge.xlsx
移動対象2_hoge2.xlsx
--------------------------------------------------------
続行するには何かキーを押してください . . .

もし,もう一度実行すると,番号がかぶらないように
別ファイルとしてもう一度まとめてコピーし直される。

「"コピー元"」内のファイルを全チェックします。
--------- 先頭IDが「移動対象1」であるようなファイルを移動します。 ---------
移動対象ファイル「D:\temp\battest\コピー元\移動対象1_fuga.xlsx」を発見しました。
コピーします。
---「"D:\temp\battest\コピー元\移動対象1_fuga.xlsx"」のコピー処理開始:
既にコピー済みです。コピー先ファイル名を変えて再試行します・・・
2回目:失敗。ファイルが存在するため,再試行します・・・
3回目:失敗。ファイルが存在するため,再試行します・・・
4個目は未コピーです。
        1 個のファイルをコピーしました。
---「"D:\temp\battest\コピー元\移動対象1_fuga.xlsx"」のコピー処理終了。
移動対象ファイル「D:\temp\battest\コピー元\a\移動対象1_fuga.xlsx」を発見しました
。コピーします。
---「"D:\temp\battest\コピー元\a\移動対象1_fuga.xlsx"」のコピー処理開始:
既にコピー済みです。コピー先ファイル名を変えて再試行します・・・
2回目:失敗。ファイルが存在するため,再試行します・・・
3回目:失敗。ファイルが存在するため,再試行します・・・
4回目:失敗。ファイルが存在するため,再試行します・・・
5個目は未コピーです。
        1 個のファイルをコピーしました。
---「"D:\temp\battest\コピー元\a\移動対象1_fuga.xlsx"」のコピー処理終了。
移動対象ファイル「D:\temp\battest\コピー元\スペースの テスト\移動対象1_fuga.xlsx
」を発見しました。コピーします。
---「"D:\temp\battest\コピー元\スペースの テスト\移動対象1_fuga.xlsx"」のコピー
処理開始:
既にコピー済みです。コピー先ファイル名を変えて再試行します・・・
2回目:失敗。ファイルが存在するため,再試行します・・・
3回目:失敗。ファイルが存在するため,再試行します・・・
4回目:失敗。ファイルが存在するため,再試行します・・・
5回目:失敗。ファイルが存在するため,再試行します・・・
6個目は未コピーです。
        1 個のファイルをコピーしました。
---「"D:\temp\battest\コピー元\スペースの テスト\移動対象1_fuga.xlsx"」のコピー
処理終了。
--------- 先頭IDが「移動対象2」であるようなファイルを移動します。 ---------
移動対象ファイル「D:\temp\battest\コピー元\a\移動対象2_hoge.xlsx」を発見しました
。コピーします。
---「"D:\temp\battest\コピー元\a\移動対象2_hoge.xlsx"」のコピー処理開始:
既にコピー済みです。コピー先ファイル名を変えて再試行します・・・
2回目:失敗。ファイルが存在するため,再試行します・・・
3個目は未コピーです。
        1 個のファイルをコピーしました。
---「"D:\temp\battest\コピー元\a\移動対象2_hoge.xlsx"」のコピー処理終了。
移動対象ファイル「D:\temp\battest\コピー元\スペースの テスト\移動対象2_hoge2.xls
x」を発見しました。コピーします。
---「"D:\temp\battest\コピー元\スペースの テスト\移動対象2_hoge2.xlsx"」のコピー
処理開始:
既にコピー済みです。コピー先ファイル名を変えて再試行します・・・
2個目は未コピーです。
        1 個のファイルをコピーしました。
---「"D:\temp\battest\コピー元\スペースの テスト\移動対象2_hoge2.xlsx"」のコピー
処理終了。
--------- 全コピーが完了しました。コピー結果: ---------
移動対象1_fuga.xlsx
移動対象1_fuga2.xlsx
移動対象1_fuga3.xlsx
移動対象1_fuga4.xlsx
移動対象1_fuga5.xlsx
移動対象1_fuga6.xlsx
移動対象2_hoge.xlsx
移動対象2_hoge2.xlsx
移動対象2_hoge22.xlsx
移動対象2_hoge3.xlsx
--------------------------------------------------------
続行するには何かキーを押してください . . .

補足

下記の質問への回答として執筆した。

Batファイルについて教えてください。
http://q.hatena.ne.jp/1319868767
複数階層にまたがっているエクセルファイルをBatファイルでコピー、又は、移動させたいのです。コピー、移動させたいファイルはファイル名の頭(重複しないIDになっている)を拾ってリストにしてあります。エクセルファイルが複数のフォルダに入っているので、最上階層のフォルダでbatファイルを実行できればと思っていますが。

感想

「非構造化プログラミング」がこれほど骨の折れるものだとは・・・。
for文をネストした際にgotoが使えないという制約に直面し,スパゲッティコードの予感がしたが,そうならないように極力,コードのエントロピーを下げた。


要件はbatだったが,こういうのはWSH/JScriptのバッチでコーディングすればかなり楽だったろう。

しかし,MSDOSの勉強にはなる。