概要・対象の読者
タイトルが大仰ですが、業務上で必要になった、Windows10上で、システムを導入するほどじゃないけど面倒な作業、特にファイル、フォルダの管理を行うバッチファイルを作成したときの調査内容の包括的なまとめです。
他言語でプログラミングの基本はわかっているけれど、バッチファイルは作ったことがない、という人がこれからバッチを作るときに調査の手間が減るといいなぁと思います。
とても長くなったのでお時間がある方はどうぞ。
例に挙げているコードはコマンドプロンプトではなくバッチファイル用です。プロンプトで試すときは%%xは%xに置き換えてください。
パスを指定できるコマンドは基本的にカレントディレクトリを対象として実行しています。必要に応じて置き換えてください。
筆者の環境
windows10
cmd:Version 10.0.19045.3086
CMDEXTVERSION:2
日本語を含むバッチファイルの文字コードはSJIS(MS932)が無難だよ
日本語を含んだバッチファイルを作成するときも、テキストが編集できれば何のソフトでも構わないんだけど、保存するときの文字コードはSJIS(MS932)じゃない文字化けして実行できないことがあるよ。
最近のテキストエディタだとデフォルトでUTF-8だったりするので気を付けてね。
厳密にはOS、コマンドプロンプトに設定されている言語によって使用出来る文字コードが変わってくるんだけど、日本語だと普通はSJISだね。特にファイル名を扱うときはUTF-8と日本語のWindows標準のSJIS(MS932)の文字コードでは互換性に問題があって手間が増えるよ。
バッチ内で文字コードを指定する方法もあるんだけど、大変残念なことにバッチから使えるコマンドにUTF-8に対応してないコマンドがいくつかあるから、ここではSJIS(MS932)で作ることを前提にするよ。大変残念だけど。英数字(アスキー文字)しか使わないならUTF-8でも行けるよ。
ちなみに改行はご存じのとおりCR+LFだね。これもOS固有だから従うしかないよ。
バッチファイルの基本は変数を使った文字列処理だよ
バッチファイルはコマンドプロンプト(cmd.exe)からOSやcmd自身に組み込まれたコマンドを実行することで動いてるよ。
でも、本当に一番重要な機能は文字列処理だよ。
ファイルへのパス、IPアドレス、URLなどなど、コマンドに渡す必要のある、あらゆる情報を文字列として扱うからだね。
バッチファイルの中では環境変数という領域に情報を格納してその文字列を足したり削ったり置き換えたりしながらコマンドを実行していくよ。だからバッチから実行できるコマンド、構文も文字列を操作したり検索するようなコマンドが多くあるし、それを使いこなすことがバッチを作成するうえで大事なことになってくるよ。
変数を使うにはsetコマンドを使うよ
変数に文字列を格納するにはsetコマンドを使うよ。まずは書き方だけ覚えて、バッチで実行はまだしちゃだめだよ。
set VARIABLE=変数
こうするとVARIABLEという名前の変数に「変数」という文字列が入るよ。変数に値を格納することを代入と言うよ。
変数名は大文字小文字を区別していないよ。使い分けできると思い込んでいると上書きしちゃうよ。実は日本語の変数名も使えるけど、打鍵が多くなるし変な挙動が起きるかもしれないからお勧めしないよ。
他のプログラミング言語に慣れていると変数に文字列を入れるのにダブルクォーテーション(")はいらないの? って思うけど、バッチファイルは根本的に文字列を扱うものだから、ダブルクォーテーションは不要だよ。
もしもsetするときに文字をダブルクォーテーションで括った場合は、 ダブルクォーテーションも文字列として変数に代入されるよ。ダブルクォーテーションの有無はバッチの実行時に意味が大きく変わってくるから要注意だよ。繰り返すけど、変数への文字列の代入にダブルクォーテーションはいらない。覚えておいてね。
代入の式で変数名と=の間は空白を入れちゃだめだよ
試したら自分が変数名だと思ってる文字+空白で変数が出来てたよ。
REM 間違えたとき
set variable = 変数
これ、「variable 」という変数に「 変数」って文字列が入るよ。これはめちゃくちゃ怖いよ。間違えて空白を入れたら変数を参照できなくてすごく焦ると思うよ。
=と代入する文字の間も空白は入れちゃだめだよ。
イコールの後の空白と、行末(改行)までの空白も文字列として変数に入るよ。文字の前後に空白が必要な時は良いけど、書き間違えないように注意してね。
変数の中身を使うには%で括って参照(または展開)するよ
echo variableには「%variable%」が入ってたよ!
とりあえず今は、echoはcmdにメッセージを表示するコマンドだと思っておいて。上のように書くとcmdに「variableには「変数」が入ってたよ!」と表示されるよ。%で括ったvariableが「変数」に展開されるんだね。
echoにはいろんな使い道があるからバッチファイルではとてもよく使うコマンドだよ。覚えてね。
変数の削除
そして、setで変数に空文字を代入すると、その変数は削除されるよ。
set variable=
変数が削除されたか確認する方法はいくつかあるけど、コマンドプロンプトで操作してるならset 変数名を実行する、バッチファイルの中では後で説明するけどdefinedを使うのが確実だと思うよ。
環境変数を汚染しないように必ずsetlocalするよ
とりあえず変数の使い方を説明したけど、バッチ上で何もせずにsetで変数を設定すると、コマンドプロンプトから起動したならそのコマンドプロンプトを終了するまで、別のバッチにも影響するよ。
変数がバッチファイルの外に出ていかないようにするために、setlocalというコマンドがあるよ。バッチファイルの先頭で下のように必ずsetlocalしてバッチ内に変数を閉じ込めるよ。
@echo off
setlocal
REM 実際の処理
set VAR=変数
echo %VAR%
REM eof ここに`endlocal`があるものとして終了するよ
構文としてはsetlocalしたらendlocalして閉じこんだ変数を解放して元の値に戻すのが正しいけど、書いてなくてもバッチの終了時にはendlocalが存在するものとして処理されるよ。
ついでに、最初の行の@echo offはバッチの実行時に出力される、コマンドの解析を非表示にするよ。
これは実験中やデバッグ中はremでコメントアウトしておいて、実行されるコマンドを表示させておいた方が何が起きているのか理解しやすいよ。
難しいことが起きないように変数名はバッチ内で一意にするよ
さっきのsetlocal、endlocalは本来、変数の「スコープ」、有効期限や範囲みたいなものを設定するコマンドだよ。
バッチの中で繰り返し使用できるし、この括りの内側と外側では同じ名前の変数を違う値として使うことが出来るよ。でも、これを使いこなすには工夫や注意力が必要だよ。
同じ変数に、バッチの場所ごとに別の意味合いの値が入っている、というのは、特殊な事情がない限りはやらない方が良いよ。できるだけ難しいことはせずに、違う意味、目的の変数には違う名前を付ける。ユーザー環境変数、システム環境変数に値を代入しない。
これが一番だよ。
変数の参照の書式は%variable%か%%xの2種しかないよ
変数の使い方について調べると色々な記事が出てくるけど、名前のある変数の値を参照するときは%variable%。後で詳しく説明すると思うけど、forで使う単文字変数は%%xで参照するよ。
まずはこれが鉄則で、これ以外の%の数を調整するような書き方は応用というか、コマンドラインに機能が少なかったころのハックに近い技法のなごりだよ。これから作るバッチでは、バグが見つけにくくなるだけだからやらない方がいいよ。
変数からは加工した文字列を取り出せるよ
それとは別の話で、変数に格納された文字列を加工して取り出す機能が%には最初からついてるよ。
%変数名:置換前の文字=置換後の文字%という書き方で変数の中の文字列を置換した状態で取り出せるよ。よく使うのは例えばこう。
echo %DATE:/=%
これはDATEという変数の内容の/を、イコールのすぐ後ろに%があるから「無」に置換、つまり削除して取り出すよ。
試すとわかるけど、実行したときの年月日がYYYYMMDD形式の数字の列として取り出されるよ。これは、DATEという変数があらかじめ環境変数に設定されていて、言語が日本語だと現在の日付がYYYY/MM/DDで格納されているってことだよ。文字コードもそうなんだけど、OS、cmdの設定で挙動が変わっちゃうから気を付けてね。
もう一つの機能が変数から部分的な文字列を取り出す機能。
%変数名:~開始位置,文字数%で指定した範囲、あるいは%変数名:~開始位置%で開始位置以降全部、が抜き出せるよ。
echo %TIME:~0,-3%
これは現在時刻がHH:mm:ssに近い書式で表示されるよ。近いというのは0~9時が「00」や「09」じゃなく「 0」や「 9」になっちゃうんだよね。ちなみにTIME変数には現在時刻がHH:mm:ss.SSっぽく格納されてるよ。
ちなみに位置はマイナスにすると最後の文字から数えて指定したことになるよ。変数に入ってる文字数がわからないときに「後ろから」を指定するとき有効だよ。
そして、このあたりの変数から文字列をを加工して取り出す書式の説明は、Microsoftのsetコマンドのドキュメントにも存在しなかったりしていて、すごく探しにくいよ。
一番わかりやすいのはコマンドプロンプトでset /?で表示されるsetコマンドのヘルプだよ。
バッチをできるだけ使わないでほしいというMicrosoftさんの気持ちが見えるよね。
このふたつ機能で大事なところは、取り出すときに加工しているから元の変数は変更されていないことだね。これが意外に便利だったりするよ。
引数は別枠で%数字しかないよ
cmdからバッチを起動するときに、コマンドに渡すオプションのように指定した文字列を渡すことが出来るよ。この実行時に指定された文字列が、引数だよ。
バッチに渡された引数を参照するときは%数字だよ。渡された引数の順番に1~9の数字に割り当てられるよ。それ以上の数の引数も渡せるけど…たぶんあまりに大量の引数が必要になるバッチは設計が間違ってるよ。処理が大変になるから考え直した方がいいよ。
引数の展開にはバッチパラメーターが使えるよ。
引数はファイルをバッチにドロップしてそのファイルを処理するとき、とかに便利だよ。引数を使うバッチファイルを作り始めると、いろんなことが出来るようになるよ。その分、コーディングの難易度が跳ね上がることになるんだけどね……。ちなみにパッチパラメーターで展開できるパス系統の情報は、実際にファイルシステムから情報を得ているんじゃなくて変数に収まっている文字列を加工しているだけだよ。dirなどのコマンドで得られた情報なら信用できるけど、 文字列を組み合わせたものの場合、そのパスにファイルやフォルダが実在するかを確認する必要があるよ。
バッチの実行の方法を理解するよ
バッチファイルには実行の方法がいくつかあるよ。起動の方法と向いてるバッチの機能を簡単に書くね。
1. ダブルクリック
バッチファイルをエクスプローラーの画面でダブルクリックするか選択してエンターキーで起動できるよ。
自分のタイミングで決まった動作をするバッチはこの起動方法がいいね。
2. ファイルやディレクトリをドロップ
エクスプローラーの画面上でバッチファイルにファイルやディレクトリをドロップすることでも起動できるよ。
起動させるのに使ったファイルやフォルダに対して何かの処理をするバッチはこの起動方法がいいね。
3. コマンドプロンプトで実行する
コマンドプロンプト(cmd.exe)を起動させて、手動でバッチファイルを指定することで起動させることが出来るよ。
この方法は実行する度に対象のファイルやフォルダが変わったり、オプションを指定して動作を切り替えるみたいなちょっと高度な機能を実現したいバッチファイルの起動に向いているよ。
4. タスクスケジューラーを設定して実行する
Windowsの管理機能の一つであるタスクスケジューラーに起動の時間や間隔を指定して、定期的にバッチを実行させることが出来るよ。
これは一定の処理を定期的に行うとき、例えば特定のファイルを特定のフォルダに振り分けるとか、バックアップを作成するとかの機能を持ったバッチに向いてるね。いわゆる「自動化」に一番近い選択肢だよ。
バッチファイルは「現在のフォルダ」という特別なフォルダを認識しているよ
なんで起動の仕方を説明したかと言うと、バッチファイルは起動の仕方でドキュメントなんかには「現在のフォルダ」「作業フォルダ」なんて書かれている、コマンドを実行したときにデフォルトで処理対象になるフォルダ、俗に言うカレントディレクトリの認識が変わってくるんだ。
バッチファイルがなにも指定していないと、起動方法によってコマンドの対象になるフォルダが変わっちゃうかもしれない、ということだよ。なにも指定していない場合のカレントディレクトリの認識を表にするね。
| 起動方法 | カレントディレクトリ |
|---|---|
| ダブルクリック | バッチファイルが存在するフォルダ |
| ドロップ | バッチファイルが存在するフォルダ |
| cmd | cmdを起動したユーザーのフォルダ cdコマンドで指定したフォルダ |
| タスクスケジューラー | タスクを起動したユーザーのフォルダ 「作業フォルダ」に指定したフォルダ |
これを統一するにはどうすればよいか、という話になるけど、シンプルな解決方法があるよ。バッチファイルの頭にこう書く。
@echo off
setlocal
cd %~dp0
cd %~dp0というコマンドを実行する。
cdはカレントディレクトリを切り替えるコマンドで、それを%~dp0でバッチファイルが置かれているフォルダまでのパスに指定してるんだね。%~はさっきちょろっと書いたバッチパラメーターだね。なぜかドキュメントにも説明がないけれど、%0にはバッチファイル自身の情報が入っているよ。そこからdpでドライブ(例えば「c:」)とパス(例えば\batches)を抜き出しているというわけだね。
これでどの方法で起動しても、そのバッチファイルの置かれている場所が作業フォルダになるよ。
ファイルやフォルダの場所は常にフルパスで指定しよう
カレントディレクトリは、 一部のコマンドがファイルやディレクトリの指定を省略されたときに対象として使う場所だよ。
でも、さっきの方法でカレントディレクトリを指定しても、バッチファイルの場所を移動されたらカレントディレクトリの場所も変わってしまう。それでは困ることになりそうなのは想像がつくよね。
だから、決まった動作を行って欲しいコマンドにファイルやフォルダの場所を指定するときは、ドライブからのフルパスを文字列として変数に格納して、それをコマンドに渡す。これを徹底しよう。
逆に変えたい部分は、バッチへの引数か、ファイルから読み取った文字列を変数に格納して使う。この時も、ファイルやフォルダの場所を指定するならフルパスに統一する。
そうしておけば、バッチの動作も安定するし、エラーの原因も特定しやすくなるよ。
括弧(...)の中で変数をsetしたら遅延展開を使うよ
バッチファイルは実行開始前の構文解析の時点で変数を展開するよ。
ifやfor~doなんかの括弧の中は構文解析の時点で一つの文として解析されて、その中の変数も開始前に展開されるよ。
括弧の中でsetして、括弧の中で参照していても、括弧の中に処理が移る前に既に変数の展開は終わっているから、開始前の解析時点でわかっている値にしかならないよ。
どういうことかわからないなら、下のバッチを実行してみるといいよ。
setlocal
set TES=あ
echo %TES%
set TES=い
echo %TES%
(
set TES=う
echo inner:%TES%
)
echo outer:%TES%
括弧の外に出てきたときには反映されてるのが納得できない動きだね。
ともあれ、括弧の中で「現在の」変数の値を知りたいときは遅延展開をオンにして!variable!で展開するよ。
遅延展開をオンにするコマンドはsetlocal enabledelayedexpansion、オフはsetlocal disabledelayedexpantionだよ。それと、setlocal enabledelayedexpansionと対になってるendlocalでも自動的にオフになるね。
たぶん、endlocalを使った方がわかりやすいとは思うんだけど、そうも言ってられないときがあるのがバッチファイルなんだよね…。
ともあれ、遅延展開を実験するなら下のバッチだよ。
@echo off
setlocal enabledelayedexpansion
set TES=あ
echo %TES%
set TES=い
echo %TES%
(
set TES=う
echo inner:%TES%
echo inner delay:!TES!
)
echo outer:%TES%
setlocal、endlocalの「スコープ」の話を理解するにはさらに下のバッチだよ。
@echo off
setlocal
set TES=あ
echo %TES%
set TES=い
echo %TES%
(
setlocal enabledelayedexpansion
set TES=う
echo inner:%TES%
echo inner delay:!TES!
endlocal
)
echo outer:%TES%
echo outer delay:!TES!
こうなると他言語に慣れてる人には何となく納得できるかな? 括弧の外ではTESが「い」に戻ったね。
でも、前にも言ったように変数のスコープを自分で操作して管理するのは簡単ではないよ。特別な理由がない限りは一つの変数は一つの目的で使って、上書きしたけど戻すようなことはしない方がいいよ。括弧の中でしか使わない変数なら、別の名前にしてしまった方が大抵の場合でわかりやすい。
それと、最後の遅延参照は括弧内のendlocalで既に無効になってるから、!TES!がただの文字列として表示されてるね。これもバッチを作るときに認識してないと、エラーの原因になるよ。
括弧の中で参照していても、括弧のなかでsetされていない変数なら、遅延展開はいらないよ。括弧で括った内側をインデントして書く癖があれば、区別しやすくなるとは思うよ。
set /aの計算式で変数を参照するとき
ものすごく例外なのが変数をset /aの計算式で使用するときだよ。
計算式では参照するときに、常に現在の値をもとに計算するし、%すらいらないよ。
実験するならこのバッチだよ。
@echo off
setlocal enabledelayedexpansion
echo まだないよ:%TES%
(
set /a TES+=1
echo fast:%TES%
echo delay:!TES!
set /a TES2=TES + 1
echo delay:!TES2!
set /a TES3=TES + TES2
echo delay:!TES3!
set /a TES4= TES + TES2 + TES3
echo delay:!TES4!
REM まだ存在してない変数も足したらどうなる?
set /a add_blank=TES4+TES5
echo add_blank:!add_blank!
)
echo %TES2%
echo %TES4%
echo %ADD_BLANK%
TESは括弧の中で初めてsetされるので、「まだないよ」の時点や括弧内の「fast」では空になってるね。
set /a TES+=1は「TESに1を足した値をTESに代入」という式だけど、TESはまだ存在していないのにエラーにならずに1になっている。存在していない変数を計算式に使うと値は0として扱われるということだね。
それに、set /a TES2=TES+1の結果は2で、まるで遅延展開したときのように「現在の値」で計算されているね。そして、計算式は空白を入れてもエラーになったり、変な数値になったりしない。/aが指定されていると通常のsetとは全く別の基準で解釈されるようになっているんだね。
試してみたとおり、代入されている側の変数の参照には遅延展開が必要だよ。
あくまでも計算式に使われている分だけ、%もいらないし遅延展開の扱いになる。
あと、計算式で扱うことが出来る数値は整数で10進数、8進数、16進数が使えるよ。
たまたま8進数や16進数に変換できる文字列を計算式に渡してしまうと予測しない結果になるよ。詳しくはset /?でsetのヘルプを見よう。setについてはヘルプが一番詳しいよ。
このあたりも理解していないと計算結果がおかしい!ってなっちゃうね。注意してね。
ファイル・フォルダの一括処理はforを定型で覚えるよ
さて、ファイルやフォルダを一括で処理するときの話だよ。
for~doというコマンドというか構文を使うと、指定された条件でループ処理するよ。
forを使うときはforのオプションを設定するよりも、融通の利くdirみたいなコマンドと組み合わせて使うよ。findも合わせるともっと細かくできるよ。
REM 一括処理するよ!
for /f "usebackq delims=" %%i in (`dir /b "*" ^| find /v ""`) do (
REM 各ファイル・フォルダに施す処理
)
基本はこの形で覚えるよ。forから開き括弧の間でループの条件を指定して、doの括弧の中の処理を行うよ。
forでコマンドの実行結果を対象にするときは/f "usebackq"が定型だよ。inの後ろの括弧に指定したコマンドを実行して、その結果の文字列を処理対象にするよ。この文字列はデフォルトではアスキー文字の空白かタブ文字で区切られている、として処理されるから覚えておいてね。
dirと組み合わせて使うと、dirの結果としてファイル・フォルダ名の空白を含む文字列が返ってくるから、delims=で区切り文字を無効化して一行すべてを変数に入れるよ。この例では%%iに入るよ。この変数はわかりやすければアルファベットは何のアルファベットでもいいよ。
このforで生成される変数にはバッチパラメーターと同じ展開が利用できるよ。dirと組み合わせるととても使い勝手が良くなるよ。
inは繰り返し処理の処理対象を指定するよ。書式はちょっとずつ違うけど、文字列、ファイルの読み込み、コマンドの結果を利用する、が指定できるよ。括弧は必須だよ。
縦棒|はパイプと言って、前のコマンドの実行結果を後ろのコマンドへ渡すよ。バッククォートの中でパイプを書くには^でエスケープする必要があるよ。
この基本形ではdirの結果をfindに渡しているんだね。findは渡されたファイルや文字列から指定された文字を検索して該当するものをリストにするコマンドだよ。
dirのオプションで重要なのは/aで、/a:dならフォルダだけ、/a:-dならフォルダを除いてファイルだけを対象にできるよ。
あと、/bはdirの結果をファイル・ディレクトリの名前だけで返すよ。コピーや削除なんかの操作に必要になるのは名前だけだからこれを使用してるよ。
dirはワイルドカードが使えるし、dirだけでで処理対象を絞り込めるならfindはいらないよ。findを通すと候補のリストを2重に処理していることになるから、実行速度を気にするならdirだけで絞り込めた方がいいよ。
この例ではfind /v ""にして空行を避けてるけど、実際にはこのdirからは空の行は返ってこないはずだから、あくまでも構文の覚え方だよ。/vは指定した文字列(この場合は空文字)を含まないものを対象にするオプションだよ。
findのオプションで便利なのは/cオプションだよ。条件に一致した項目の数をカウントした数字だけが戻るよ。
forの構文のdoの中でカウントアップしてもいいけど、遅延展開のこともあるし、実行速度は自力でカウントしてもほぼ変わらないから、項目数だけを知りたいならfind /cでいいと思うよ。find /cをforの中で使うなら下のようになるよ。
for /f "usebackq delims=" %%i in (`dir /b "*" ^| find /c /v ""`) do (set /a COUNT=%%i)
REM このCOUNTは括弧の中でsetされてるけど括弧の外で参照だから普通に参照できるよ
if %COUNT% gtr 10 (echo 貯めすぎだよ!) else (echo 許す!)
この例だとバッチを実行したフォルダを探して項目数が10より多いと怒られるよ。
ただ、パイプでfind /cにdirの結果を渡すと、dirで発生したエラーは判別できないよ。エラーがあってもfind /cで0として集計されるんだ。
パイプで繋いだコマンドが失敗したのか、そもそもパイプに何も渡らなかったのかは、工夫しないと確認できないよ。コマンドのエラー対策も考えておこう。
for /f "usebackq"で実行するコマンドのエラー対策
この場合はdirがエラーになった時を識別するのが大事だね。
ちょうどいい記法がバッチにはあるよ。
||でコマンドを繋ぐと、前のコマンドが失敗したときだけ後ろのコマンドが実行されるよ。
「失敗」というのは厳密には、実行したコマンドやバッチファイルが1以上の終了コードを返したときなので、そもそも常に0を返すコマンドや、成功時でも0以外の特定のコードを返すコマンドにはこの書式は適用できないよ。コマンドのリファレンスにも終了コードは明記されてないケースが多いから、実験してみないとわからなかったりするよ。
とりあえず、例としてはこう。
for /f "usebackq delims=" %%i in (`dir /b "*" ^|^| echo *`) do (
if %%i==* (echo error!) else (echo %%iある!)
)
こうするとdirが失敗したときはechoからアスタリスクが出力されて%%iに入るよ。アスタリスクにしたのはWindowsのファイルやフォルダの名前に使えない文字だからで、dir以外のコマンドの時は別の文字がいいかもしれないね。
後はif~elseで処理を分岐させれば対応できるね。エラーがないときはループのたびに比較と条件分岐があるから、膨大な量の対象があるときは実行速度に影響するよ。
上の例は判別できてることだけを確認するだけのコードだけど、実際には「ファイルがない」こととそもそもコマンドの構文やオプションが間違ってた、みたいな場合を分けたいかもしれない。
それもなんとか対応する方法はあるよ。
エラー出力もforの対象にする
そもそもforが単文字変数に代入しているのはコマンドが標準出力に渡した文字で、いわゆる「実行結果」だね。
コマンドがエラーになると、標準出力ではなくエラー出力にだけメッセージを出力して、標準出力が空になる。パイプでつながれたコマンドが空の標準出力を受け取ったとき、コマンドによっては構文が成り立たなくなって致命的なエラーでバッチが落ちるんだね。
それなら、エラー出力もパイプに渡す、つまり標準出力にまとめちゃえばいいね。こうするとできるよ。
for /f "usebackq delims=" %%i in (`dir /b "*" 2^>^&1`) do (
echo %%i
)
2^>^&1の部分がエラー出力を標準出力にまとめてるよ。^はバッククォートの中で>や&は直接書けないからエスケープしてるよ。だから本来は2>&1だね。
勘が良い人はわかるだろうけど1が標準出力で2がエラー出力だよ。この>は「出力先を変更する」リダイレクトを表す記号だよ。dirのエラー出力を標準出力にリダイレクトして結び付けているんだね。
これでディレクトリで見つかったファイル・フォルダ名か、エラーメッセージが%%iに入るよ。
後はdoの中であれこれするんだけど、実行したコマンドのエラー出力を全部知っていないとエラーをifの条件に引っ掛けることも難しい。だけど、%%iに入ってるのはdirの結果なんだし、%%iという名前のファイル・フォルダが存在するかを確かめれば、存在しないものがエラーメッセージのはずだね。
for /f "usebackq delims=" %%i in (`dir /b "*" 2^>^&1`) do (
if exist %%i (
echo %%iあったよ!
) else (
echo %%i>>log.txt
)
)
これでどうだろう。
existで%%iに完全に一致する名前のファイル・フォルダがあるかどうかを確かめてるよ。存在していれば条件の後の括弧のなかに入って「あったよ!」と出力、存在しなければelseの後の括弧に入ってとりあえずecho %%i>>log.txtを実行するよ。
これはechoの出力をlog.txtファイルにリダイレクトしているよ。これは結構便利だよ。ここではパスを指定していないから、カレントディレクトリ内にlog.txtがないなら作成したうえで、%%iに入ったエラーメッセージを出力するよ。名前の通りログとして使えるように>を二つ重ねて>>にして、追記モードにしているよ。単に>にすると上書きされて、ログの役割には使えないよ。
この例は%%iに入ったエラーメッセージと全く同じ名前がカレントディレクトリに存在しない限りは正しく動作するよ。
「なんとか」と言ったのはこの例外があるからだね。でも、これは通常ならほとんど考えなくてもいいリスクだと思う。
バッチファイルでエラーに対応するのはとても難しいから、無視できるエラーは無視してしまうのも必要だと思う。もちろん予想されるエラーが致命的ならあらかじめ対処しておく必要はあるね。
エラーログからメッセージを拾う
くどいかもしれないけど、一応このケースで使える特殊な処理法も書いておくよ。
@echo off
setlocal enabledelayedexpansion
set err=0
for /f "usebackq delims=" %%i in (`dir "*" /b 2^>error.tmp ^| find /v "" ^|^| echo *`) do (
if %%i==* (
set /p err=<error.tmp
echo error:!err!
) else (echo %%i)
)
dirのエラーをファイルへリダイレクトして、エラーがあるときはechoを使って標準出力にアスタリスクを出力して%%iに入れる。
doでは%%iがアスタリスクだったらエラーログ?からset /pでerrへメッセージを代入して、エラーメッセージを表示する。という流れだね。
set /pは標準入力から変数に代入するコマンドなんだけど、バッチファイルでは大体ファイルから1行だけ読み取る形で使用されるね。<がファイルから標準入力に読み込みを行っているよ。
仮にファイルに複数行あっても、先頭の行だけ読み取られる。もちろんfor /fでファイルを読み込んでも良いんだけど、この処理では繰り返し処理ではないことがはっきりしてるからこの書き方にしたよ。
実際にはバッチファイルからエラーをログファイルに記録してさえあれば、さらにそれを読み込んで何かをする、なんて処理は必要ないと思うけど、こういうこともできるよ、という例だね。
条件分岐 if
ここまででさらっと使っていたけど、条件分岐ifも整理しよう。基本の構文としてはこうだよ。
if 条件 (条件が成立したとき) else (条件が不成立の時)
複数のコマンドを実行したいときは
if 条件 (
コマンド1
コマンド2
...
) else (
コマンドa
コマンドb
)
こうするよ。この場合はif 条件 (が一行、) else (が一行になっていないと構文エラーになるよ。
条件は主に比較演算子で変数同士か変数と文字、変数と数値を比較する。
比較するときにそれが文字なのか、数値なのかは比較演算子によって厳密に区別されるよ。
また、条件の方が複数の時はifを入れ子にするとかif~else if~という書き方もあるよ。
REM 入れ子
if defined user (
if %role%=="admin" (echo えらい人!) else (echo 普通の人)
) else (
echo 誰もいねぇ…
)
REM 複数のどれかに一致する
if %month% leq 3 (
echo 4Q
) else if %month% leq 6 (
echo 1Q
) else if %month% leq 9 (
echo 2Q
) else if %month% leq 12 (
echo 3Q
) else (echo error!)
入れ子の方は当然、重ねるほど分かり辛くなっていくから多用しない方がいいね。
条件の方だけど、ifのドキュメントにある通り、バッチファイルでよく使いそうなものだけが存在している。よく使うものをいくつか見ていこう。
文字列比較==
基本中の基本だね。左辺と右辺が同じ文字列なら真(true)になるよ。もし変数に数字のみが入っていても文字列として扱うよ。
普通は「変数の値がxxxだったら」という使い方をするので例えばこうだね。
if %TEXT%==テキスト (echo テキストだよ!) else (echo テキストじゃないよ!)
先にも書いたけどバッチファイルで文字列を扱うときは基本的にダブルクォーテーションは必要ないよ。コマンドに渡すときに空白やタブ文字を含めてひとつの文字列として扱いたいときだけ、ダブルクォーテーションで括る。
ifの文字列比較にもファイルやフォルダの名前から空白が入ってくることがあるから、変数が展開されたときにその中に含まれる空白が構文の区切り文字として扱われてエラーになったりするよ。予測不能なエラーの原因になったりするから、変数の中に何が入っているのかは常に気を配る必要があるよ。変数に空白が含まれていることが予想されるときの書き方はこうだよ。
if "%TEXT%"=="空白も ある文字列" (echo 一致したよ!) else (echo 一致しないよ!)
実際のところ、空白を含んだ文字列を比較する場面は滅多ににないと思う。それに、この書き方は変数に格納されている文字列によっては大変面倒なことになる。比較条件に使用されるような変数は、中に何が入っているかよりも、中に何を入れるべきなのかを考えたほうがいい。勘がいい人は、再三、文字列の代入にダブルクォーテーションはいらないと言ってきた理由がわかるんじゃないかな?
数値の比較
変数の中が数値である場合は大小なども比較できるよ。これは3文字の、実際のところコマンドみたいなもので比較するよ。
- EQU:左辺が右辺と等しい→EQUal
- NEQ:左辺が右辺と等しくない→Not EQual
- LSS:左辺が右辺の値未満→LeSS than
- LEQ:左辺が右辺の値以下→Less than or EQual
- GTR:左辺が右辺の値より大きい→GreaTeR than
- GEQ:左辺が右辺の値以上→Greater than or EQual
記号の方がわかりやすいとは思うんだけど、バッチファイルの中では記号は大体が特殊な機能を割り振られているからコマンドっぽいもので比較してるんだね。英語の略なのでそれを理解してると少し覚えやすい、かも。
両辺のどちらかに文字列、というか数字以外を含んでいる場合は必ず偽、falseになるよ。あと、ちょっとだけ触れたように8進数、16進数に解釈できる文字列が変数に代入されていて数値として比較された場合、予想しない結果になるから注意してね。
ファイル、フォルダの存在を確認するexist
バッチらしい演算子がexistだね。文字列で与えられた場所にファイル、フォルダが存在しているかを確認して、存在していたら真になるよ。
if exist "c:\program files" (echo あるよ!) else (echo ないだと…!?)
別々な文字列を組み合わせてファイルやフォルダの場所を示す文字列を作った時はこれで確認するのが安全だね。新規作成やコピーを行うときにも、上書き防止のために確認しよう。この例のようにダブルクォーテーションのことも意識しないとエラーのもとになるよ。
エラーの原因という意味では、存在しているファイルでも別のプロセスが書き込みをブロックしていたり、アクセス権がなくて開けないときはエラーになるから、注意が必要だよ。
変数の存在を確認するdefined
これもバッチらしい比較?演算子だね。指定された変数が存在するとき真になるよ。
if defined time (echo あるよ!) else (echo ないだと…!?)
これも使い方次第だろうけど、たぶん一番の使い道はバッチで使うことが確定している引数がきちんと渡されているかのチェックだと思う。
例えばコピー元とコピー先を文字列で受け取って処理するバッチなら引数は2つ必要になる。%1と%2が存在しているときだけ処理をする。そんな使い方だね。
forやifの括弧の中でsetしてその外側で変数を使うとき…なんて考えも浮かぶけど、その場合は外側で初期値を決めておいて、括弧の中でsetで上書きした方が良いね。そうしておけば、definedではなく単純な比較演算子を使えるからね。
エラーの有無を検出するerrorlevel 数値
直前にシステムが認識したコマンドやバッチの終了コードを調べて、指定の数値以上だったら成立する条件だね。
robocopy x y /s
if errorlevel 8 (echo error! see logfile.)
この演算子は比較的最近追加されたのかな? ネットで検索すると
if %errorlevel%==0 (echo success!)
みたいな利用法が多く引っ掛かるけど、演算子があるならそれを使う方が面倒なことにならなそうだよね。遅延展開のこともあるし。変数の方から取得するときは大体の場合、遅延展開しないと正しい値にならないよ。
errorlevelの数値はいくつにするべきかは、意外と難しいよ。
例えば、robocopyは利用者としては正常に動作してるように感じる「すべてのファイルが正常にコピーされました」もerrorlevelを1に設定してくるよ。
そういう点で、使う場面は多いんだけど、慎重さが必要な演算子だね。
あと、先にちょっと話した||があったよね。前のコマンドが失敗したらという条件だから、あれはerrorlevelを対象にした条件分岐と考えることが出来るよ。
条件付き実行&& ||とコマンドの連結&
if errerlevel 数値は終了コードを基準に条件分岐を行うけど、コマンドを条件付きで連結する&&や||もerrorlevelが0かそれ以外かで処理を切り替えてるよ。
コマンドを&&で繋ぐと前のコマンドが成功(終了コード0)の時だけ後ろのコマンドが実行されるよ。
commandA && commandB
REM ifに変換
commandA
if not erroerlevel 1 ( commandB )
errorlevel 数値は「errorlevelが指定の数値以上のとき成立」だから1以上の時をnotで反転させたifが同じ流れになるよ。
一方で||は前のコマンドが失敗したときに後ろのコマンドが実行されるんだったね。
commandA || commandB
REM ifに変換
commandA
if errorlevel 1 (commandB)
これを使うとif~elseも再現できそうだけど、厳密にはできない。
REM 連結で書くとしたらこうなるけど
commandA && commandB || commandC
REM ifに変換
commandA
if not errorlevel 1 commandB
if errorlevel 1 commandC
ifとして書き換えると2つの条件分岐を並べているだけなので、このif errorlevel 1 commandCは、「commandAが成功してcommandBが失敗したとき」もcommandCが実行される。
じゃぁ逆順に書いたらというと、
commandA || commandC && commandB
commandA
if errorlevel 1 commandC
if not errorlevel 1 commandB
となるわけで、「Aが失敗してCが成功したとき」もcommandBが実行される。なので、厳密にはif~elseをコマンドの連結で再現することはできないよ。
ただ、「複数のコマンドをすべて成功させていなかったらエラー処理をおこなう」ようなケースではこの書き方は有効だね。たとえば、こう。
del 社会性フィルタ.txt
( dir *.txt *.bat /b >list.txt ) && (
findstr /n /f:list.txt にゃーん>>社会性フィルタ.txt && echo フィルタ抽出!
) || (echo エラーが出たわよ)
pause
こうすると「カレントディレクトリからtxt、datのファイルを検索してlist.txtに書き出して、findstrがそのリストをもとに各ファイルを検索して、社会性フィルタが発動している行を抽出して社会性フィルタ.txtにファイル名と行番号、その行を出力する」という一連の流れのどこかでエラーが発生したら「エラーが出たわよ」と報告することが出来るよ。迂遠な手順だけど「こういうこともできるよ」の例として大目に見てほしい。
「txtとbatを検索してlist.txtに書き出す」に成功しないと「list.txtからファイル名を読み出してうんぬん」を実行しないし、ちゃんと抽出に成功したのかエラーが出たのかも検出できる。
成否の対象になっているコマンドがわかりやすい点がif errorlevel 数値で条件分岐するよりも優秀かもしれないね。ファイルでの入出力が多いときもエラーチェックがしやすいのもいいね。
ただ、この例で試して分かったけどfindstrの正規表現風の検索は日本語に弱いので十分実験してから使わないとだめだね。この例で".*にゃーん.*"ってすると結果がおかしくなるよ。
あと、コマンドの成否にかかわらず、単に1行で複数コマンドを実行させたいときは&でコマンドを連結できるよ。でもこれはバッチではあまり使わないかな。下みたいなことだからね。
commandA & commandB
REM バッチの標準的な記法だと…
commandA
commandB
コマンドプロンプトからコマンドを実行するときは使い道があるかも。
サブルーチンとバッチの呼び出し
条件で分岐させて別々の処理を行うことが出来ることを話してきたけど、例えば条件のうち二つに共通の処理があるときはどうしたらよいだろう。例えばこんな時。
if 予算が0以下 (
エラー処理
) else if 予算が1万未満 (
申請書を作成
小口出金伝票を作成
) else if 予算が1万以上 (
申請書を作成
稟議書を作成
)
それぞれの書類はフォーマットが決まっていてテキストや数値を埋め込むような処理をしている結構長いコードが必要だとして。
こうなると、同じコードをコピーするのは当然やりたくないよね。そういう時に使うのがcallだよ。先に例を書こうか。
if 予算が0以下 (
エラー処理
) else if 予算が1万未満 (
call :application
小口出金伝票を作成
) else if 予算が1万以上 (
call :application
稟議書を作成
)
REM バッチ本体の終了をきちんと書かないとapplicationも実行されるよ
exit /b 0
REM ラベルはコロンの後に名前を書くよ
:application
申請書を作るコマンド色々
exit /b 0
callはかなり特殊なコマンドで、実行中のバッチファイルから別のバッチファイル、またはラベルで指定されたブロックを実行するよ。実行した後は、callの次の文から実行を再開する。もともとが別のバッチを起動させるコマンドだから、引数を渡すこともできるよ。
この例の場合、2か所に存在していた申請書を作成する手順を:applicationというラベルで切り出して、コピペを避けるようにしたよ。引数は使用していないね。
この申請書を作るためにいろんな変数が使われるだろうけど、callで実行されるバッチ、ラベルにはその呼び出し前に宣言されていた名前付き変数がすべて引き継がれるよ。これはsetlocalの説明でちょっとだけ触れた、「同じcmdから実行されるバッチに環境変数が共有される」特性を利用したものだね。
この「変数の引継ぎ」は呼び出されるバッチの構文解析の前に行われるから、呼び出される側のバッチ内では呼び出し元の変数を遅延展開せずに参照できるよ。これはちょっとしたテクニックだから覚えておくといいよ。
一方で、変数は共有されているわけだから、呼び出される側で変数を書き換えると呼び出し側にも影響するよ。とはいえ、呼び出された側から何らかの値を呼び出し側に渡すには実質的に呼び出し元と共通の変数を使ってやり取りするしかないから、そこは注意して使う以外にないね。
そうなると「せっかく指定できる引数の使いどころは?」ってなるけど、親のバッチの引数(%0~%10)や、forが生成した変数(%%x)は引数で指定しないと引き継げない。
それと、バッチの引数にはバッチパラメーターが使用出来るね。つまり、ファイルやフォルダのパスを格納した変数を引数で渡すと、面倒な文字列操作を自分で書かずに、いろいろと加工して取り出せる。単なる環境変数の受け渡しではこれが出来ないから、巧く利用しよう。
終了コードを指定して終了するexit /b
呼び出されたバッチやサブルーチンは、終了するときにexit /bで終了コードを指定できるよ。これは当然errorlevelにも反映されるので、正常に終了したときは0、何かしらのエラーで終了したときはそれ以外の数値を返すようにしよう。
exit /bで指定できるのは数値だけだよ。さっき「実質的に共通の変数を使うしかない」と書いたけど、一応、数値であればexit /bで渡せるよ。でも、あくまでも終了コードだから、それ以外の目的に使うことはお勧めしないよ。errorlevelや&&などが正しく動作しなくなってしまうからね。
あ、もちろん呼び出し元のバッチも終了時にexit /bを使えば終了コードを返せるよ。 これが一番役に立つのはバッチをタスクスケジューラーから起動しているときだね。タスクスケジューラーは前回の実行結果を保存してるけど、「この操作を正しく終了しました」みたいな感じで反映されるよ。
だけど、タスクスケジューラーでこの終了コードをを拾ってくれないのはとても悲しいよ。再実行が必要な時はバッチ内でリトライするとかの細工が必要だよ。
別アプリケーションを起動させるstart
callと似た目的で利用できるのがstartだよ。callと違うところはバッチに限らず実行可能ファイルを起動できること。これを使うとコマンドラインから起動できるアプリを活用できるから、できることがぐっと増えるよ。ImageMagickで画像処理をするとか、VLCで動画や音源のエンコードを変換するとか、7zipで圧縮、解凍するとか。
他のアプリケーションを呼び出す分、オプションが複雑だけど、とりあえず最初は/b、/waitを知っていればいいかな。
/bはたぶんbindなのかな。batchのbかもしれない。これは起動するアプリやコマンドを呼び出し元のバッチ(cmd)と同じcmdのウィンドウ、プロセス上で実行するよ。これで呼び出すのはコマンド(コマンドライン対応のアプリケーション)かバッチじゃないとだめだよ。GUIのアプリを/bで起動するとよくわからないことになるよ。
/waitは起動させたアプリケーションの終了を待って次のコマンドを実行するよ。/bと/waitを指定してバッチを実行すると、callでバッチを起動したのとよく似た状態になるよ。ただし、アプリケーションから何かの変数を受け取ったりするのは普通はできないよ。アプリ側が決まった変数や終了コードを返すように設計されているか次第だね。
この二つの動作がオプションになっているということは、startは本来、バッチとは別にアプリケーションを起動させるまでが基本動作だということだね。だから規定では呼び出したアプリの終了を待たない。
で、最初に言ったけど、startの一番便利な使い方はコマンドラインに対応したアプリケーションに指示を出して、並列で処理させることだよ。画像処理や動画のエンコードみたいに時間のかかる複数の処理を、アプリケーションに任せてバッチは別の作業をしてもいいし、指示だけ出して終了させてしまってもいい。GUIで同じ作業を繰り返すのは大変だけど、バッチから指示を出せばとても楽だよ。
callとstartの違いはいろんな説明がされてるけど、callはバッチ、サブルーチンの呼び出し用、startはアプリケーション呼び出し用と理解しておくのが良いよ。そしてこの二つを使いこなすのは、バッチで魔術を行うための基本になるよ。
コマンドと引数とダブルクォーテーションの話
ここまで当たり前のようにコマンドを実行させる話をしてきたけど、コマンドと引数について踏み込んで考えてみよう。
抽象的になるけど、バッチ、コマンドプロンプトから使用出来るコマンドは下のように書くよね。
コマンド名 引数1 引数2…
実際はコマンドごとにオプションの順番が決まってたり、/c:textみたいにオプションにコロンで引数に似た値を指定出来たりと色々あるんだけど、重要なことは、構文としてコマンド名と引数やオプションは空白で区切る、ということだね。この時、引数に空白を含む文字列を指定するにはどうしたらよいだろう。
そうだね、この時に必要になるのがダブルクォーテーション"だ。
先にも触れたけど、ダブルクォーテーションで括られた文字列はダブルクォーテーションも含めてひとまとめで、特別な変数である引数に格納される。これはバッチに引数を渡すときも同じだよ。
コマンドは内部では引数のダブルクォーテーションを外して使用しているはずだね。そうしないと、ファイルやフォルダを検索できないだろうし。cmdの仕様上、空白が区切り文字になっているから「空白を含む文字列」を引数にするときは仕方なしにダブルクォーテーションで括っているだけで、元来、バッチやコマンドにとってダブルクォーテーションは邪魔でしかないんだ。使う必要があるのはコマンドやバッチの引数に空白を含む文字列を渡す必要があるときだけ。これはもっとも重要なルールだよ。
しかしながら、ファイル名やフォルダ名に空白が含まれることはよくあるから、バッチでコマンドを実行するときもダブルクォーテーションを使う場面が多々あるね。forでファイルやフォルダを繰り返し処理するときなんかが、その典型だね。そのとき、変数には空白を含んだ文字列が格納されているかもしれない。だから、ファイル名やディレクトリ名を受け取るコマンドに変数を使用するときは下のように書く。
コマンド名 "%variable%"
for /f "usebackq delims=" %%i in (`dir *.txt /b`) do (type "%%i")
変数に何かするんじゃなくて変数自体をダブルクォーテーションで括ってしまう。バッチでコマンドに変数を渡すときは鉄則だね。
そして、バッチが引数を受け取った時は%~で括りのダブルクォーテーションを除去してから使う。引数がダブルクォーテーションで括られている可能性を忘れると、コマンドの構文が破壊されて致命的なエラーになるよ。
ダブルクォーテーションの構文破壊力
外部からダブルクォーテーションが変数に紛れ込むことで、コマンドの構文が破壊される例を見てみよう。
わかりやすく致命傷になるのがifの文字列比較だよ。
if "%1"=="/f" set flag=1
この文は%1が単にダブルクォーテーションで括った何かしらの文字列であるだけで破壊されるよ。下のように解釈されるからだね。
if ""any text""=="/f" set flag=1
このパターンはさっきの例のtypeのようなシンプルなコマンドでも起こる。空白を含む文字列を考慮してダブルクォーテーションで変数を括ると、変数にダブルクォーテーションが含まれていたら確実に構文が破壊されるよ。
だから変数にダブルクォーテーションを格納してはいけないし、引数はダブルクォーテーションを外してから使用しなくちゃいけない。そして、ifで比較する文字列は何が入っているべきなのかを先に検討して、致命的なエラーを起こすような文字は変数から除去しなくちゃいけないよ。
実は引数の区切りは空白だけじゃないよ
これは意外に知られていないけれど、バッチやコマンドの引数の区切り文字は空白だけじゃないんだよね。
実際には,、;、=、タブ文字も区切り文字として認識される。だから引数にこれらの文字が含まれる可能性がある場合はやっぱりダブルクォーテーションで括らないといけないよ。
これらの文字がcmdの区切り文字に設定されている理由はわからない。たぶん過去からの遺産で、システム環境変数のPATHなんかにその気配を感じるよね。
ともかく、ファイル名、フォルダ名を格納する変数はコマンドに渡すときに変数自体をダブルクォーテーションで括ってから展開する癖をつければ、変なエラーに悩まされることは確実に減るよ。
そして、ファイル、フォルダ以外の変数にこれらの文字が含まれる場合は、それが本当に必要かを考えてみたほうがいいね。
dirかforfilesか
ここまではフォルダ、ファイルの検索のコマンドとしてdirを使ってきたね。でも、ファイルを日付範囲で検索したいとか、ファイルサイズでフィルターしたいとかも、バッチの目的によってはあるよね。
その処理をdirでやろうとすると大変だね。forの使い方の理解を深める意味でも、ここであえてそれに挑戦してみようか。
dirの出力のファイル情報をfor /fで整理する
dirの結果表示の最初は固定で、ドライブ情報、ボリューム情報、空行、検索対象のディレクトリ、空行、そしてこれ以降がファイル情報になる。最初の5行は読み飛ばしたいね。/fのキーワードに読み飛ばす行数を指定するskip=があるよ。いままでの定型に組み合わせるとこう。
for /f "usebackq skip=5 delims=" %%i in (`dir * /a:-d`) echo %%i
これでフォルダにあるファイルの情報が表示できたはず。まだ末尾にファイル数・バイト数とフォルダ数・空き領域のバイト数の2行があるね。これを除去しよう。定型ではfindを使ってたけど、findだとワイルドカードみたいな検索が出来ない。ここはfindstrの出番だね。
findstrは正規表現風の検索ができるよ。オプションもたくさんあるんだけど、ここでは条件にヒットしたら除去する/vを使うよ。そして、除去したい2行に共通するワードは「x個の」と「バイト」だね。それを組み込むとこう。
for /f "usebackq skip=5 delims=" %%i in (`dir * /a:-d ^| findstr /v 個の.*バイト`) echo %%i
dirの結果をパイプ|でfindstrに渡すという基本の構造は変わらないよ。バッククォートの中では|が直接書けないから^でエスケープも同じ。
findstrで使える表現はPOSIX的な正規表現とは異なるので、意図した結果になるかは複数回試してみたほうがいいね。この場合はdirの書式が定まっていて、必ず「個の」の後に「バイト」という単位が含まれているから、シンプルに任意の文字の繰り返し.*を挟むことでヒットさせてるよ。
これで「最終更新日 更新時間 バイト数 ファイル名」のリストが取得できたね。後は定型では無効にしていた区切り文字を有効にして、各項目をそれぞれ変数に入れるよ。
for /f "usebackq skip=5 tokens1-3*" %%i in (`dir * /a:-d ^| findstr /v 個の.*バイト`) echo %%i,%%j,%%k,%%l
キーワードのdelims=を削除してデフォルトの空白文字とタブ文字を区切りに文字に使う。tokens=で区切られた部分の1~3番目、に加えて残り全部を変数に入れる指定をしているよ。これについては公式ドキュメントからtokensの説明を引用しよう。太字は私が付けたものだよ。
各反復処理の for ループに渡す各行のトークンを指定します。 結果として、追加の変数名が割り当てられます。 m-n は、m 番目から n 番目までのトークンの範囲を指定します。 tokens= 文字列内の最後の文字がアスタリスク (*) の場合は、追加の変数が割り当てられ、解析された最後のトークンの後ろの行にある残りのテキストを受け取ります。
この例では欲しいのは全部で4項目なんだけど、4項目めがファイル名だから、空白文字を含む可能性があるね。だから1から3項目めに加えて、残りの全部という指定方法になるよ。tokens=1-4としてしまうと空白を含むファイル名で空白の前までしか取得できないよ。
結果としては、dirの表示項目をカンマで繋いだものがechoされたはずだよ。ここからさらにdoのブロックでechoするんじゃなく、日付やファイルサイズを比較したりすれば、一応の目標は達成できるね。
ただ、バッチファイルで日付の計算は結構大変だよ。うるう年もあるし自力ではやりたくない作業の筆頭だよ。だから、それをやるならforfilesを使おう。
forfilesで日付を条件に検索する
forfilesのオプションに/dという日付または日数で最終更新日を指定する機能があるよ。
符号で以前、以後を指定して、該当するファイルが対象になるよ。ただ、日数はプラスに指定しても意味がない。これはバグの気がするね。本当は+10にしたら「10日前以降に更新されたファイル」にしたかったはずだよ。
この動作を実現するためにはStackOverFlowのこの記事みたいな仕掛けが必要になるよ。
外のforfilesでフォルダ内を普通に検索して、2個目のforfilesでそのファイルが指定日数よりも更新日が古いかを条件として検索して、存在していなければ更新日が新しいものだから出力するというロジックだね。このコマンドの組み方はバッチの組み方としてとてもよい例なので解読してみるといいよ。
forfilesでコマンドを実行する
さておき、日付でフィルタはオプションでできるから、サイズはどうするかの話をしようか。
ここまでの説明でもforfilesが風変わりなコマンドなのはわかると思うけど、forfilesの本来の機能は検索結果になったファイルの情報を使って指定されたコマンドを実行することだよ。forfilesがforで使えるのはコマンドの実行を指定する/cに規定で"cmd /c echo @file"が指定されているからだね。
コマンドを指定するときに、コマンドはダブルクォーテーションで囲う必要がある。あと、ドキュメントにcmd内臓のコマンドを実行するためにはcmd /cを指定する必要がある、と書かれているけど、探した範囲ではcmdに内臓のコマンドが明記されてないんだよね。とりあえず、全部につければ問題ないよ!
forfilesの肝はファイルやディレクトリのプロパティを専用の変数に入れられることだね。これを自分の必要な形式でechoしたり、ifにかけたりするんだね。
これでもうわかった人もいると思うけど、ファイルサイズでフィルタするなら、コマンドにifでサイズを指定指定してあげればいいね。
forfiles /m * /c "cmd /c if @isdir==FALSE ( if @fsize gtr 1000 (echo @file @fsize))"
これも例としてわざと迂遠に書いてるよ。
@fsizeはバイト単位なことに注意が必要だね。この単位をK,M,Gで調整できればもっと良いんだけど、Windowsのコマンドにそこまで期待しちゃだめだね。この例では1000バイトより大きいファイルをリストアップするはず。
実行してみるとわかるけど、@fileはファイル名をダブルクォーテーションで括って返すね。これが親切なのかどうかは微妙なところ。他のコマンドは括らないのに何でforfilesだけ括ったんだろうね……。目的によってはこのダブルクォーテーションが邪魔になることもあるだろうから注意してね。
使い分け
dirとforfilesの使い分けはファイル名だけ必要ならdir、それ以上のプロパティが必要ならforfilesだね。
処理が1コマンド(か、パイプや連結で処理できる範囲)だけならforfiles、フィルタリング以外にも条件分岐や複数コマンドの処理が必要ならforと組み合わせる、という感じになるんじゃないかな。
あと、forfilesの処理は複雑なことが出来る分、dirと比べると目に見えて実行速度が遅いよ。処理速度が気になるならdirとforで自力で何とかする方が良い場面も出てくると思う。
そのほか、robocopy /lとほかのオプションの組み合わせで検索するという方法もあるにはある。取得できる項目がdirとforfilesの中間くらいで、2つのフォルダ間のファイルの新旧や新規作成や削除を検出できるから、そこが重要なら使い道もあると思う。
forのループ処理と高度?な変数
forの使い方をさらに突っ込もう。
当然ながらcmdのforも、他言語と同様に回数を指定したループ処理ができるよ。オプションに/lを指定してinに開始値、増分、終了値を指定するよ。
for /l %%i in (1,1,5) do (echo %%i)
この例は1から5をechoするよ。in (5,-1,1)なら5から1になるね。
他の言語のこの手のループで必ず出てくるのが番号でアクセスする変数、配列だね。
Windowsのコマンドラインそのものには他の言語の配列に相当する仕組みはないから、変数名に変数を埋め込んで展開するという方法が使われるよ。わかりにくいから例を出そう。
setlocal
for /l %%i in (1,1,5) do (
set val[%%i]=%%i
)
for /l %%i in (1,1,5) do (
setlocal enabledelayedexpansion
echo !val[%%i]!
endlocal
)
1回目のループでval[番号]という名前で変数をsetして、2回目のループで内容を表示させてるよ。
問題は2回目のループだね。普通なら%val[%%i]%と書いて展開したくなるところだけど、この書き方が出来ないよ。
変数の展開は%で括るんだから、この書き方をするとcmdは%val[%と%i]%と解釈してしまう。だから遅延展開で実行時に展開させるんだね。この方法がシンプルで確実だよ。変数名を書くときに変数を組み合わせて展開するというのが、バッチの黒魔術の基本だね。
ここでは配列の再現をしたけど、ここでいう%%iに文字列が入っても問題がないから、いわゆる連想配列も再現できる。変数名に規約を作れば、JavaScriptのオブジェクトみたいにforを回すこともできるよ。
setlocal enabledelayedexpansion
set html_PROPS[1]=head
set html_PROPS[2]=body
set html_PROPS[3]=footer
for /f "usebackq tokens=2 delims==" %%i in (`set html_PROPS`) do set html.%%i=%%i
for /f "usebackq tokens=2 delims==" %%i in (`set html_PROPS`) do echo !html.%%i!
for /f "usebackq delims=" %%i in (`set html. ^| find /v /c ""`) do echo html.lengs:%%i
if %html.head%==head (echo !!) else ??
「html」という名前のオブジェクトに「PROP」という内部プロパティがある、というルールを決めて、それに要素名を保存して、いろんなアプローチができることを示してみたよ。
setで変数名を検索するのが意外と便利だよ。このforはそれぞれ順に、要素に初期値を設定、要素名でforを回してhtmlの内容を表示、htmlの要素数を取得して表示しているよ。
ただまぁ、ここまでして他言語のデータ構造を再現しても、活用方法がすぐには思い浮かばないんだけどね。
初期値の設定というか、要素の定義はsetを羅列するのも見苦しいから、設定ファイルを外部に用意して、それを読み込んで設定するようにするとより本格的かも。安全性に問題があるかな。
遅延展開の沼
さっきの例、実行してみたら最後に「ECHOは<ON>です」って表示されなかったかな。これは、echoを引数なしで実行したときに表示されるechoの現在のモードだね。つまり、このコードの(echo !!)の!!はcmdから見えていない。どうしてこうなるかわかるかな。
遅延展開が悪さをしている。というか、正しくない使い方をしちゃってるからだね。次も試してみてほしい。
setlocal enabledelayedexpansion
for /f "tokens=*" %%i in ("aa b;c d!e") do echo %%i
echo 30% 50%
echo 30%% 50%%
文字列をすべて読み込んでechoしてるだけなんだけど、d!eじゃなくdeと表示されるはず。
%が変数の展開と解釈されて消えてしまうのと同じで、遅延展開が有効なら!も変数として展開されてしまう。これは変数内に!が含まれていないかをきちんと検証しないと遅延展開が使えないということだよ。
また、バッチでは単に%を表示したい場合も%が変数展開で消えるのを防ぐために%%にしないといけないよ。
そんなわけで、コマンドがechoだから大事にはならないけど、下のような利用は安全じゃない。
setlocal
set /a count=0
for /f "usebackq tokens=*" %%i in (`dir /b`) do (
setlocal enabledelayedexpansion
set /a count+=1
REM %%iに`!`が含まれていると意図通りに動作しない!
echo %%i,!count!
endlocal
)
これでも最初の例よりはdoブロックの中でだけ遅延展開を有効にさせてる分ましなんだけど、根本的な解決にはなっていない。ではどうするかというと、先にも説明した通り、サブルーチンを利用するよ。
サブルーチンは外側で宣言された名前付き変数を引き継ぐから、この例でいえばcountの遅延展開が必要なくなる。その代わりに%%iを引数として渡さないといけないね。
setlocal
set /a count=0
for /f "usebackq tokens=*" %%i in (`dir /b`) do (
set /a count+=1
REM サブルーチンならいける!
call :view %%i
)
exit /b 0
:view
echo %1,%count%
exit /b 0
こうすると遅延展開もなくなって、変数内の!が悪さをすることもなくなる。
作るバッチによってはいろんな箇所で同じ作業を行うケースもあるだろうから、doなどのブロックをサブルーチンに切り出すことは積極的に検討した方が良いね。そして、繰り返しになるけど、変数は中に何が入っているのか、もしも比較に使うのならそれが信頼できる値かどうかはチェックしないといけないよ。
初期設定ファイルがあるの、かっこいいよね
よくある変数名=値の形で初期設定を記入したファイルを読み込んで… うん。ここでは表示するだけ。設定ファイルはこんな内容にするよ。拡張子はiniだけどtxtの拡張子を変更するだけだよ。
;;;初期設定っぽいファイル
;;;本体設定
core.timeout=2000
core.retry=0
;;;クライアント設定
client.scheme=https
client.domain=qiita.com
client.user=talesleaves
内容はそれっぽい雰囲気を出してるだけだよ。
これを読み込んで、設定どおりの変数を設定するバッチを組むよ。
手順としては下のとおり。
-
forでファイルを読み取って、行ごとに変数名と値に分割 - 変数名がバッチの認識できる文字列かをチェック
- 値が有効な値かをチェック
- バッチの変数として格納
- echoする
チェックは変数名ごとにサブルーチンで行うのが1番よさそうかな。共通するルールはこう。
- 変数名は「a-z(大文字は含まない)、0-9、[].」で構成する。空白は厳禁。
- 値は変数名でサブルーチンを呼んでチェックする。
本当に設定ファイルのようなものなら、複数の値を設定できるパターンや空白を含む値もあり得るだろうけど、ここでは割愛。
forでファイルを読み込む
for /fでin()にファイル名を指定すると指定されたファイルを読み込んで1行ずつ処理できるよ。さっきのdemo.iniを指定するよ。
setlocal
for /f "tokens=1-2 eol=; delims==" %%i in (demo.ini) do (
echo %%i : %%j
)
eol=は空行として扱う文字をひと文字だけ指定できるよ。行頭にeol=で指定した文字が存在する場合、その行は空行として扱われる。ここではコメント行に使用してる;だね。
区切り文字のdelims=は=を指定して、1行をイコールで変数名と値として分割する。使用する部分は2個でtokens=1-2となるね。読み込んで表示するだけならこれでOKだね。安全性の保障がないけど。
変数名のチェック
これがいきなり難しいよ。条件の通りに使える文字を制限していこう。findstrの出番だね。
findstrの正規表現には文字の集合を指定する「クラス」と指定以外の文字を表す「逆クラス」があるので、それを利用しよう。[^文字集合]で逆クラスが指定できるんだけど、バッチ上では^をエスケープして[^^文字集合]にしないと正しく認識されないよ。この辺りが本当に分かり辛い。
クラスの中では始点文字-終点文字で文字の範囲が指定できる。POSIXならa-zで小文字だけになるけれど、cmdでは異なる。
結論から言うとアルファベットの指定はすべて羅列させないと機能しない。どうやらcmd内部で文字コードではなくソート順を参照していることに原因があるみたいだよ。バグでしょそれは。仕様ならドキュメントに範囲指定したらどうなるか書こうよ。
あとはこのクラス内に]を含めようとするとクラスの閉じ括弧として認識されてしまうので、]を含めたいなら\]と指定しなくちゃいけないよ。
以上を踏まえてfindstrに指定する検索文字列は[^^abcdefghijklmnopqrstuvwxyz0-9[\].]となるね。長い。
setlocal
for /f "tokens=1-2 eol=; delims==" %%i in (demo.ini) do (
echo %%i| findstr [^^abcdefghijklmnopqrstuvwxyz0-9[\].] ^
&& echo 変数名"%%i"に使用できない文字が含まれています。&& exit /b 1
echo %%i : %%j
)
長いから コードの改行をエスケープして行を折り返す小技を使ったよ。
変数の値をfindやfindstrに渡すときは値をechoしてパイプで渡すよ。この時、|とechoの最後の文字の間に空白を入れちゃダメ だよ。echoの特有の仕様で、|の直前までをすべて文字列として出力するから、余計な空白のせいでfindやfindstrがヒットしなくなるよ。
ひとまずこれで、変数名にサポートしない文字があったらバッチが終了するね。
変数名がバッチで認識できるものかチェックする
これは変数名を指定する変数を作ってチェックするのが一番良いと思う。さっきやったJavaScriptっぽくforを回せる方法で行こう。まずは変数のプロパティを設定する。
setlocal
REM 設定の種別=セクション
set SECT[1]=core
set SECT[2]=client
REM coreセクションのプロパティ名
set %SECT[1]%_PROP[1]=timeout
set %SECT[1]%_PROP[2]=retry
REM clientセクションのプロパティ名
set %SECT[2]%_PROP[1]=scheme
set %SECT[2]%_PROP[2]=domain
set %SECT[2]%_PROP[3]=user
REM 変数のチェック
for /f "tokens=1-2 eol=; delims==" %%i in (demo.ini) do (
echo %%i| findstr [^^abcdefghijklmnopqrstuvwxyz0-9[\].] ^
&& echo 変数名"%%i"に使用できない文字が含まれています。&& exit /b 1
echo %%i : %%j
)
exit /b 0
さっそく変数名を変数を展開して決めてるよ。こうすることでセクションやプロパティが増えても対応しやすいと思う。forで回す前提ならコードを書き換える必要もないしね。
読み込んだ変数名がバッチが認識できるものかどうかの確認は、setの検索結果と読み込んだ変数名を突き合せればOKだね。これは結構シンプルに作れる。ただ、forのループの入れ子が深くなるからサブルーチンに切り出そう。
このサブルーチンは引数として、読み込んだ行の左辺、上のコードでいうところの%%iを受け取る。それをさらにピリオド.で分割すれば、セクション名とプロパティ名にできるね。後はsetで検索してバッチが持っているセクションとプロパティ名に一致すればよいね。
REM 変数名がバッチで使用されるかチェックするサブルーチン
:check_name
REM errorlevel保存用
set /a status=0
for /f "usebackq tokens=1-2 delims=." %%i in ('%1') do (
(set SECT | findstr "\<SECT\[.\]=%%i\>")^
&& (set %%i_PROP | findstr "\<%%i_PROP\[.\]=%%j\>")^
&& goto :end_name_check
echo 無効な設定"%1"は無視されます。
set /a status=1
)
:end_name_check
exit /b %status%
forにdelims=.と指示して、引数で受け取った変数名をピリオドで分割する。分割した部分は両方使用するのでtokens=1-2だね。前半が設定ファイル上のセクション名、後半がセクション内のプロパティ名という扱いだよ。これが%%iと%%jにセットされる。
doブロックではまず、セクション名をset SECTで検索して、findstrで"SECT[x]=セクション名"という結果が存在しているかをチェックしているよ。
それが成功したら続けて、set %%i_PROPでセクション内のプロパティを検索して、findstrでプロパティ名と%%jが一致するかをチェック。
それも成功したらgotoでdoブロックを抜けるよ。
gotoは指定したラベルへ飛んで実行再開するコマンドだよ。黒魔術に多用され過ぎて使用禁止を提唱されるほどのコマンドだけど、次の行のecho以下は変数名が一致しなかったときの処理だから、それを飛ばすために使用しているよ。if errorlevelでいちコマンドごとにチェックすればelseで処理できるんだけど、こっちのほうがまだわかりやすいかなぁ。
このgotoでforのループを抜ける方法も、割と最近まで使用できなかったみたいだよ。
リンク先の記事と変わったは、Windows10のcmdでは「forループの中でgotoで移動すると%%iがリセットされる」ことだよ。罠としか思えない仕様だけど、抜け出せるようになっただけマシということのようだね。
一連のチェックに失敗したときは「無効な設定"%1"は無視されます」と表示して、エラーレベルを1に設定する。
そしてdoを抜けてエラーレベルを返してサブルーチンは終了だね。
バッチの本体部部にこのサブルーチンを呼ぶ部分を追加しよう。
setlocal
REM 設定の種別=セクション
set SECT[1]=core
set SECT[2]=client
REM coreセクションのプロパティ名
set %SECT[1]%_PROP[1]=timeout
set %SECT[1]%_PROP[2]=retry
REM clientセクションのプロパティ名
set %SECT[2]%_PROP[1]=scheme
set %SECT[2]%_PROP[2]=domain
set %SECT[2]%_PROP[3]=user
REM 変数のチェック
for /f "tokens=1-2 eol=; delims==" %%i in (demo.ini) do (
echo %%i| findstr [^^abcdefghijklmnopqrstuvwxyz0-9[\].] ^
&& echo 変数名"%%i"に使用できない文字が含まれています。&& exit /b 1
echo %%i : %%j
call :check_name %%i
if not errorlevel 1 (echo 値をチェックします。)
)
exit /b 0
REM 変数名がバッチで使用されるかチェックするサブルーチン(以下略)
値をチェックしますと表示している部分で、今度は値をチェックすることになるね。先にも書いたとおり、各値のチェック用のサブルーチンを呼ぶことにするよ。
サブルーチン名を変数で決める
forループの中で今チェックしている変数の値をチェックするためのサブルーチンを呼びだすよ。
サブルーチンを呼びだすためのラベルも、変数を展開して決定できる。サブルーチン名も規約を作って一定の法則で呼び出せるようにすれば、コードもシンプルに書けるよ。ここでは、変数名.checkValueという名前でサブルーチンを呼びだすと決め込もう。
REM 変数のチェック
for /f "tokens=1-2 eol=; delims==" %%i in (demo.ini) do (
echo %%i : %%j
REM 変数名の規約チェック
echo %%i| findstr [^^abcdefghijklmnopqrstuvwxyz0-9[\].] ^
&& echo 変数名"%%i"に使用できない文字が含まれています。&& exit /b 1
call :check_name %%i
REM 変数の値のチェック
if not errorlevel 1 (
echo 値をチェックします。
echo %%j| find """" && echo %%j:値にダブルクォーテーションは使用できません。&& exit /b 1
call :%%i.checkValue %%j
if not errorlevel 1 (set %%i=%%j) else (echo 無効な値"%%j"は無視されます。)
)
)
まずはifでcheck_nameがエラーを返さなかったときだけ処理を行うようにするよ。
そして、値は"は禁止してしまおう。かろうじてuserが使用できてもよさそうだけど、今回は禁止。
ここで注意が必要なのが "で括られた文字列の中に"自体を含めるためには"でエスケープする必要があるというルールだね。これもバッチ作成を分かり辛くさせる原因だと思う。実はこの辺りはcmdの「解説」に書かれてるよ。正直、分かり辛くて理解できないけど。
そういうわけで、findで"を指定するためにfind """"と記述しないといけないよ。
このバッチは値に"を見つけたらエラーメッセージを表示して終了するよ。これは"を含んでいるとそのあとの動作が保証できなくなるから正当だと思うよ。
callのラベルは変数名.checkValueに決めたのでチェックしたものをそのまま使う。そして引数に値を渡せばOKだね。そしてその結果をif errorlevelで検知して、エラーがなければ変数名と値をそのまま利用してsetする。
これで本体の流れはOKだね。
各変数ごとの値のチェックをする
それじゃ実際に値をチェックするサブルーチンを作ろう。
coreの各要素は0以上の数値のみ有効。
domainはまじめに検証すると大変だね。今は理論上は日本語文字も使用出来るからね…。でもここでは日本語ドメインは無視して、アルファベットと数字とハイフン、ピリオド。
userはアルファベットと数字のみにしておきましょうか。
これを踏まえて各要素の値にfindstrを掛けるルーチンを組めばいいね。先にコードを貼ってしまおう。
REM 値チェック
:core.timeout.checkValue
:core.retry.checkValue
echo %1| findstr "[^^0-9]" && exit /b 1
exit /b 0
:client.scheme.checkValue
for /f "tokens=1-2" %%i in ("%SCHEMES%") do (
echo %%i%%j| findstr "\<%1\>" && exit /b 0
echo %%i| findstr "\<%1\>" && exit /b 0
)
exit /b 1
:client.domain.checkValue
echo %1| findstr "[^^%LOWER_A_Z%%UPPER_A_Z%0-9\-.]" && exit /b 1
exit /b 0
:client.user.checkValue
echo %1| findstr "[^^%LOWER_A_Z%%UPPER_A_Z%0-9]" && exit /b 1
exit /b 0
timeoutとretryについては全く同じ処理になるのでラベルを列挙して処理はまとめているよ。ラベルはあくまでも移動先を表す文字列で機能があるわけではないから、timeoutのサブルーチンに入ってきたときにはretryのラベルは無視されるよ。
schemeについてはSCHEMSという変数にhttp sという文字列を設定しているよ。これをforで分割して変数に入れることで、引数の値がhttpかhttpsに一致することを確認しているよ。他のサブルーチンはは使用できない文字含んでいないかをチェックしているから、このルーチンだけ返すエラーコードが反転して見えるのがあまりよくない…。
domainとuserは長くなるアルファベットの羅列を、小文字をLOWER_A_Z、大文字をUPPER_A_Zという変数に入れて使用しているよ。それ以外は新しいことはないね。
これで、初期設定値をファイルから読み取るバッチが出来たね。全体をまとめるので試してみてほしい。
@echo off
setlocal
set SECT[1]=core
set SECT[2]=client
set %SECT[1]%_PROP[1]=timeout
set %SECT[1]%_PROP[2]=retry
set %SECT[2]%_PROP[1]=scheme
set %SECT[2]%_PROP[2]=domain
set %SECT[2]%_PROP[3]=user
set SCHEMES=http s
set LOWER_A_Z=abcdefghijklmnopqrstuvwxyz
set UPPER_A_Z=ABCDEFGHIJKLMNOPQRSTUVWXYZ
REM 変数のチェック
for /f "tokens=1-2 eol=; delims==" %%i in (demo.ini) do (
echo %%i : %%j
REM 変数名の規約チェック
echo %%i| findstr [^^abcdefghijklmnopqrstuvwxyz0-9[\].] ^
&& echo 変数名"%%i"に使用できない文字が含まれています。&& exit /b 1
REM 変数名の実在チェック
call :check_name %%i
REM 値のチェック
if not errorlevel 1 (
echo 値をチェックします。
echo %%j| find """" && echo %%j:値にダブルクォーテーションは使用できません。&& exit /b 1
call :%%i.checkValue %%j
if not errorlevel 1 (set %%i=%%j) else (echo 無効な値"%%j"は無視されます。)
)
)
REM 設定された変数の表示
set core.
set client.
exit /b 0
REM 変数名がバッチで使用されるかチェックするサブルーチン
:check_name
set /a status=0
for /f "tokens=1-2 delims=." %%i in ("%1") do (
(set SECT | findstr "\<SECT\[.\]=%%i\>">nul)^
&& (set %%i_PROP | findstr "\<%%i_PROP\[.\]=%%j\>">nul)^
&& goto :end_name_check
echo 無効な設定"%1"は無視されます。
set /a status=1
)
:end_name_check
exit /b %status%
REM 値チェック
:core.timeout.checkValue
:core.retry.checkValue
echo %1| findstr "[^^0-9]" && exit /b 1
exit /b 0
:client.scheme.checkValue
for /f "tokens=1-2" %%i in ("%SCHEMES%") do (
echo %%i%%j| findstr "\<%1\>" && exit /b 0
echo %%i| findstr "\<%1\>" && exit /b 0
)
exit /b 1
:client.domain.checkValue
echo %1| findstr "[^^%LOWER_A_Z%%UPPER_A_Z%0-9\-.]" && exit /b 1
exit /b 0
:client.user.checkValue
echo %1| findstr "[^^%LOWER_A_Z%%UPPER_A_Z%0-9]" && exit /b 1
exit /b 0
本体は最後にsetで変数名と値を表示してるだけだけど、curlに渡せばページの中身を取ってくるくらいはできるよ。そこからさらに何かの処理をするとなると相当に難しいけど…。
でもこれで色々できそうだ。あとは、何のコマンド、アプリケーションと組み合わせるかだけだね。
コマンドの紹介
ここまででバッチを書くときに必要なことはほとんど網羅できたと思うよ。
後はコマンドを知る必要があるね。バッチやcmdで実行できるコマンドはたくさんあるけど、ここではファイル、ディレクトリ周りの初歩のコマンドだけ紹介するね。
コマンドの使い方は解説しているサイトも多数あるからそれらも参考にしてね。
ネットワークドライブ
- net use
指定したネットワーク上の共有フォルダをドライブに割り当てる。
netコマンド自体は多数のサブコマンドを持った管理用コマンド群だが網羅的なドキュメントがない…
ディレクトリ操作
- md/mkdir
ディレクトリを作成する。 - rd/rmdir
ディレクトリを削除する。
ファイル・フォルダ共通
- ren/rename
ファイル・フォルダ名を変更する。 - del/erase
ファイル・フォルダを削除する。 - attrb
ファイル・フォルダの属性(読み取り専用、隠しなど)を操作する。属性を参照するコマンドは多いので利用価値あり。
ファイル操作
- move
ファイルを移動する。 - type
テキストファイルの内容を表示する。
type nul>>ファイル名で空のファイルを作成できる≒touch(空データを指定のファイル名にリダイレクトして出力する。更新時間の変更はできない) - comp/fc
ファイル内容の比較を行う。
コピー
- robocopy
高度なコピー機能を持つ。多量のファイルを扱うときは速度や安全性の面からこのコマンド1択。 - copy/xcopy
簡易的なコピーコマンド。
入出力関連
- clip
出力をクリップボードにリダイレクトする。クリップボードから読み込みはPowerShellなら可能。 - choice
cmd画面に選択肢を表示して入力を受け付ける。 - echo
文字列を出力する。リダイレクトやパイプと組み合わせて使用する。 - sort
入力を並べ替えして再度出力する。 - set /p
標準入力から変数に値をセットする。
プロセス、タスク関連
- tasklist
実行中のプロセスのリストを表示する。 - taskkill
実行中のプロセスを終了する。 - at
タスクスケジューラーにスケジュールを登録する。例外処理的にバッチ内から別のバッチを1回だけ実行するとか。 - start
指定した実行可能ファイル、またはコマンドを起動する。
最後に
結局のところ、バッチで何かの処理を行うときは試行錯誤は避けられません。cmdは怪しい挙動が多いのでなおさらです。
私が必要だったバッチは2週間くらいで作れましたが、この記事はさらに調査やテストを行って約1か月かかりました。
もしもバッチ制作で行き詰まった人がこの記事にたどり着けば幸いです。
参考サイト
公式ドキュメント-Windows のコマンド
&&と||で流れ制御1 - || (論理和) - DOS/コマンドプロンプト コマンド一覧
&&と||で流れ制御2(英語) - Conditional execution
FINDSTR
[findstr]正規表現で長音記号"ー"を使うとエラーとなる
FINDSTR - Escapes and Length limits
cmdでエスケープ処理のまとめ-How-to: Escape Characters, Delimiters and Quotes at the Windows command line.
ss64.comはおそらくバッチ関連最強のまとめサイトだと思います。
ifの括弧の中で)が展開されるとエラーになる-(解決?)バッチで良くわからないエラーに悩まされてます。
@plcherrimさんの一連の記事-qiita屈指のバッチマニア。バッチでゲームを作ってたりします。
記事作成中に発見したバグと思われる挙動
for /f "usebackq delims="とdirを組み合わて使用したとき、cmd区切り文字で開始されるファイル、フォルダ名を正しく扱えない
for /f "usebackq delims=" %%i in (`dir * /b`) do (echo %%i)
dirのみなら認識されている「;,=」のcmd区切り文字から始まるファイル、フォルダが、forに"delims="を指定しているにもかかわらず、%%iに入らない。
おそらくdirの結果をforへ渡すタイミングでcmdが文字列の先頭にある区切り文字を行末として認識し、空行として処理されている。forfilesなら大丈夫。