4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

あ〜!! 現在のフォルダ以下にある全ての PNG/GIF 画像のファイル名・拡張子・相対フォルダ名を再帰的に取得して好きな文字列と結合したソート済みテキストを cmd.​exe からワンライナーでクリップボードに直接コピーしたいなぁ〜!​!​!

Last updated at Posted at 2020-05-22

え!? 現在のフォルダ以下にある全ての PNG/GIF 画像のファイル名・拡張子・相対フォルダ名を再帰的に取得して好きな文字列と結合したソート済みテキストを cmd.​exe からワンライナーでクリップボードに直接コピー!​?​!​?

出来らあっ!

フォルダ構成

C:\current\  ← 現在の作業フォルダ
┣ sub\
┃ ┣ more\
┃ ┃ ┗ d.png
┃ ┗ a.png
┣ b.gif
┣ c.png
┣ not_image.txt
┗ z.gif
  • *.png および *.gif のファイルだけを対象とする。
    • フォルダそのものや他の拡張子のファイルは対象外。
  • ファイルのパスか何かから以下の情報を切り出して、いいかんじのテキストにしたい。
    • ファイル名(例:a
    • 拡張子(例:.png
    • 相対フォルダ名(例:sub\

ワンライナー

cmd /Q /V:ON /C "(for /R %I in (*.png *.gif) do ((set here=%~dpI) & (set rel=!here:%CD%=!) & (echo ファイル名「%~nI」、拡張子「%~xI」 … 相対フォルダ「!rel:~1!」)))" | sort | clip

できた。

結果(クリップボード)

ファイル名「a」、拡張子「.png」 … 相対フォルダ「sub\」
ファイル名「b」、拡張子「.gif」 … 相対フォルダ「」
ファイル名「c」、拡張子「.png」 … 相対フォルダ「」
ファイル名「d」、拡張子「.png」 … 相対フォルダ「sub\more\」
ファイル名「z」、拡張子「.gif」 … 相対フォルダ「」

全行の末尾に改行文字があるので、最後は空行。

ということで解説

せっかくなので徹底的に分解していきたい。
とりあえず複数行に戻してみよう。

書き下し文

cmd /Q /V:ON /C "
(
  for /R %I in (*.png *.gif) do (
    (
      set here=%~dpI
    ) & (
      set rel=!here:%CD%=!
    ) & (
      echo ファイル名「%~nI」、拡張子「%~xI」 … 相対フォルダ「!rel:~1!」
    )
  )
)
" | sort | clip

うーん、シンタックスハイライトが仕事できない…。
大部分が文字列として渡されてるから仕方無し。

では、個別の解説(という名の備忘録)へ。

以下、特にパスの取得については全て C:\current\sub\a.png を例として解説する。
例には必ずこの注釈1を付けておくのでわかると思う。
あと、変数は %var% みたいに斜体で示す。何となく。

cmd /Q /V:ON /C "なんやかんや"

なんやかんや の処理を cmd.exe の別インスタンスで実行する。

  • /Q実行されるコマンド行自身を出力しない。"Quiet" 。
    • バッチファイルを書く時には @echo off でお馴染みのやつ。大事。
    • これを指定しないと、出力テキストに C:\current>((set here=C:\current\sub\ ) & (set rel=!here:C:\current=! ) & (echo ファイル名「a」、拡張子「.png」 … 相対フォルダ「!rel:~1!」 ) ) ​1 みたいなコマンド行自身も含まれてしまってえらいことになるので、黙っててもらう。
      • なお、今回は do (〜) の箇所を do @(〜) としても同じ結果になる。
  • /V:ON遅延環境変数の展開を有効化する。"Variable" 。
    • バッチファイルでは setlocal enabledelayedexpansion でお馴染みのやつ。ちょー大事。
    • cmd.exe は、デフォルトでは変数の「展開」のタイミングが各処理のそれと並行しないことがある。特にワンライナーでは、コマンド同士を & 等で繋ぐ都合のせいか、その制限が更に厳しい。
      この仕様についてはググれば怨嗟2の声がいくらでも出てくるが、とにかくこの遅延なんちゃらを有効化すると、%var% の他に !var! という記法の変数展開が使えるようになる。
      • なお、%TMP% みたいな既に値を持っている環境変数とかは普通に使える。
    • 今回、処理全体を cmd でわざわざ囲んでいる大きな理由はこれ。
      **忌々しい。**←怨嗟の声
  • /C "なんやかんや"なんやかんや を実行して停止する。"Carry out" 。
    • 停止しない /K というのもあるが、今回は停止してくれないと次の sortclip にデータが渡らないので、大人しく死んでもらう。

書き下し文(cmd 内)

cmd に文字列として渡された実行内容を改めて書き下すと、この通り。

(
  for /R %I in (*.png *.gif) do (
    (
      set here=%~dpI
    ) & (
      set rel=!here:%CD%=!
    ) & (
      echo ファイル名「%~nI」、拡張子「%~xI」 … 相対フォルダ「!rel:~1!)
  )
)

ありがとうシンタックスハイライト。

for /R %I in (*.png *.gif) do なんか

in (*.png *.gif) を1件ずつ読み込んで %I に代入し、do なんか 内で好きなコマンドとして実行する。

  • /Rin () としてファイルセットを受け付け、条件に該当するファイルを再帰的に検索する。"Recursive" 。
  • %I:見つかったファイルのフルパス(たぶん)が代入される変数。
    • 変数名には1文字しか使えないが、別に %I じゃなくても良い。
      仕様が妙に柔軟で、%_ とか %1 とかでもいける。
    • for の変数参照は、上述した遅延なんちゃらを利用しなくても普通に使える。!I などとしてはいけない。
    • 変数 %I をコマンド直打ちでなくバッチファイルの中に記述する場合は、パーセント記号を重ねて %%I などと指定すること。
  • in (*.png *.gif):ファイルセット。要はワイルドカードで指定するファイル名。
    • 今回のような複数条件は、単に半角スペース区切りで並べれば良い。
  • do なんか:該当ファイルが見つかる度に なんか を実行する。
    • 実行内容では変数 %I を利用でき、更にバッチファイルにおける %~dp0 等でお馴染みの引数置換構文をこの変数に適用できる。使う場合は恐らく ~ が必須。
      今回も3種類ぐらい使うが、詳細は追い追い。
    • 変数 %I をコマンド直打ちでなくバッチファイルの中に記述する場合は、パーセント記号を重ねて %%I などと指定すること。ダブルチェックヨシ!

(set here=%~dpI)

各ファイルがあるフォルダのフルパスを here に代入する。

  • ():コマンドをそこで区切ってひとまとめにする
    • この重要性の話は余談にて。
  • set なまえ=あたい:変数 なまえ を宣言し、あたい を代入する。
  • here:宣言した変数。後で使う。
    • 宣言時は、変数名に % とか ! とかを付けてはいけない。
  • %~dpI:引数置換構文。ファイル %I が置かれているフォルダまでのフルパス C:\current\sub\1
    • ~:文字列がダブルクォーテーションで囲まれている場合にそれを外す。
    • d:ファイルがあるドライブ文字 C:1
    • p:ファイルがあるディレクトリのパス名 \current\sub\1
      • 末尾にフォルダ区切り文字 \ を含む。
    • 変数 %I をコマンド直打ちでなくバッチファイルの中に記述する場合は、パーセント記号を重ねて %%I などと指定すること。トリプルチェックヨシ!​!​!

& (set rel=!here:%CD%=!)

動的環境変数 %CD% を利用して !here! から相対パス名を取得し、変数 rel に代入する。

  • &:1行に複数のコマンドを記述する。ワンライナーの最高の友達
  • ()set なまえ=あたい:上述。
  • rel:宣言した変数。後で使う。
  • !here:%CD%=!:これ、書いてみたら通っちゃって**「え、いいの!?」**となったようなやつなので、どう説明すべきか…。
    • !here!:さっき宣言した変数。ファイルのあるフォルダまでのフルパス C:\current\sub\1 を持つ。
      • 使う時にはこのように !% で囲う。
    • %CD%作業フォルダのフルパス C:\current を勝手に持っている動的環境変数。
      • 末尾にフォルダ区切り文字 \含まない
    • %へんすう:ア=イ%:変数 %へんすう% が持つ文字列中の を全て に置換した文字列を返す記法。
      • 例えば echo %CD:c=Z% というコマンドは z:\zurrent を出力する。大文字・小文字は無視するっぽい。
        あるいは echo %CD:t=% とすれば、空文字への置換(=文字列の削除)が行われて、:\urrent を出力する。

ということで、まぁ最後のやつは、
!here! の中の %CD% と一致する箇所を空文字列に置換する」
=「C:\current\sub\1 から C:\current を削除する」
=「相対フォルダ名 \sub\1 を取得する」
という処理になる。

…なるが、cmd.exe においてそんな風に変数を変数で置換するのは、本来ならもっと困難を伴う。

\current\本来の困難.bat
@echo off
set var=c

echo   : %CD%
echo 失敗: %CD:%var%=z%
echo 失敗: %%CD:%var%=z%%
call echo 成功: %%CD:%var%=z%%
pause
C:\Windows\system32\cmd.exe
元  : C:\current
失敗: var
失敗: %CD:c=z%
成功: z:\zurrent
続行するには何かキーを押してください . . .

具体的には call と二重の % が要る。なんだそれ。

今回そういう困難とご挨拶せずに済んだのは、遅延なんちゃらである !here! とそうではない %CD% との性質の違い、もっと言えば変数としての展開タイミングの違いによるものだろう。たぶん。

& (echo ファイル名「%~nI」、拡張子「%~xI」 … 相対フォルダ「!rel:~1!」)

今まで準備してきた変数を用いて、望むテキストを標準出力する。

  • &():上述。
  • echo:メッセージを標準出力する。
    • 最終的に得られるテキストを直接生み出すのはこいつ。
      そういう意味では最重要コマンドだろう。
  • %~nI:引数置換構文。ファイル %I の拡張子抜きファイル名 a1
    • **※ 変数 %I をコマンド直打ちでなくバッチファイルの中に記述する場合は、パーセント記号を重ねて %%I などと指定すること。**クアドラプルチェックヨシ!​!​!​!
  • %~xI:引数置換構文。ファイル %I の拡張子 .png1
    • ※ 変 数  %I  を コ マ ン ド 直 打 ち で な く バ ッ チ フ ァ イ ル の 中 に 記 述 す る 場 合 は 、 パ ー セ ン ト 記 号 を 重 ね て  %%I  な ど と 指 定 す る こ と 。 ク イ ン タ プ ル チ ェ ッ ク で 今 日 も 一 日 ご 安 全 に ! ! ! ! ! ! ! ! ! !
  • !rel:~1!:これはまぁ、さっきの置換記法よりは難しくない。
    • !rel!:さっき宣言した変数。ファイルのある相対フォルダ名 \sub\1 を持つ。
    • %へんすう:~N%:先頭を0文字目として、変数 %へんすう% が持つ文字列の N 文字目以降を切り出して返す記法。
      • 例えば echo %CD:~1% というコマンドは :\current を出力する。

ということで、最後のやつは
「相対フォルダ名 \sub\1 の先頭1文字を削除した文字列 sub\1
を表す。

先頭をなぜ消すかって、これをやらないとファイルが作業フォルダ直下にある場合に相対フォルダ名が \ になるのがキモいので…。
そんなら !rel! の宣言代入時に消しとけよって感じになるが、実はそこには大いなる罠が待ち受けている。

空文字列の代入.bat
@echo off
echo 通常の変数の場合
set s=ABC
set s1=%s:~1%
set s99=%s:~99%
REM ↑たった3文字しか無い元文字列の、先頭99文字を削除。
REM こういうオーバーキルをした場合は空文字列が返る。
echo s%s%」、s1「%s1%」、s99「%s99%echo 遅延環境変数の場合
cmd /Q /V:ON /C "(set s=ABC) & (set s1=!s:~1!) & (set s99=!s:~99!) & (echo s「!s!」、s1「!s1!」、s99「!s99!」)"
pause
C:\Windows\system32\cmd.exe
通常の変数の場合
s「ABC」、s1「BC」、s99「」
遅延環境変数の場合
s「ABC」、s1「BC」、s99「!s99!」
続行するには何かキーを押してください . . .

遅延環境変数の展開においては、変数に空文字列が直接代入されているとなんか上手くいかんらしい。いかんならしゃーない。
これを防ぐために、変数 !rel! にはあえて無駄な1つの頭文字を持たせておいて、いざ表示という段においてのみそれを必ず1文字削る、という手順を経ている。

もし本当に要らないなら、宣言と代入の時点で set rel=!here:%CD%\=!3 とすれば容易に消しておけるのだ。

もう一息

cmd /Q /V:ON /C "なんやかんや" | sort | clip

( ) が生い茂り ! % が降り注ぐ、"なんやかんや" の縄張りを今抜けた。
ゴールは目と鼻の先だ。

あれやこれや | sort

あれやこれや の処理が一通り済んだら、その内容をソートして標準出力する。

  • あれやこれや:行う処理。
  • |:標準出力を次のコマンドにパイプする(渡す)。
  • sort:受けたテキストの行を辞書順に並べ替えて標準出力する。

どうってことはない。

てんやわんや | clip

てんやわんや の処理が一通り済んだら、その内容をクリップボードにコピーする。

  • てんやわんや:行う処理。
  • |:上述。
  • clip:受けたテキストをクリップボードにコピーする。

どうってことなさすぎる。


てなわけで完了!​!​!​!​!​!​!

まとめ

フォルダ構成
C:\current\  ← 現在の作業フォルダ
┣ sub\
┃ ┣ more\
┃ ┃ ┗ d.png
┃ ┗ a.png
┣ b.gif
┣ c.png
┣ not_image.txt
┗ z.gif
結果(クリップボード)
ファイル名「a」、拡張子「.png」 … 相対フォルダ「sub\」
ファイル名「b」、拡張子「.gif」 … 相対フォルダ「」
ファイル名「c」、拡張子「.png」 … 相対フォルダ「」
ファイル名「d」、拡張子「.png」 … 相対フォルダ「sub\more\」
ファイル名「z」、拡張子「.gif」 … 相対フォルダ「」

ワンライナー
cmd /Q /V:ON /C "(for /R %I in (*.png *.gif) do ((set here=%~dpI) & (set rel=!here:%CD%=!) & (echo ファイル名「%~nI」、拡張子「%~xI」 … 相対フォルダ「!rel:~1!」)))" | sort | clip
書き下し文
cmd /Q /V:ON /C "
(
  for /R %I in (*.png *.gif) do (
    (
      set here=%~dpI
    ) & (
      set rel=!here:%CD%=!
    ) & (
      echo ファイル名「%~nI」、拡張子「%~xI」 … 相対フォルダ「!rel:~1!」
    )
  )
)
" | sort | clip

めでたし。

余談

以下余談。

心の声(パスに半角スペースや %! が交ざってても大丈夫なの…?)

フォルダ構成
C:\cu !r!r%e% nt\  ← 現在の作業フォルダ
┣ %CD% !CD!\
┃ ┣ !rel! %rel%\
┃ ┃ ┗ dracula %Is !here!.png
┃ ┗ a縺薙s縺ォ縺。縺ッ.png
┣ b.gif                                   .exe
┣ c ,.'`+-=^~_()[]{};!#$%&@.png
┣ not_image.txt
┗ z%% o%m%b%%Ie.gif

いっけなーい🔪 殺意殺意💦
ある日突然、平和なディレクトリに悪意の塊が攻めてきてもう大変💦
一体私達、これからどうなっちゃうの〜😭😭!?

結果(クリップボード)
ファイル名「a縺薙s縺ォ縺。縺ッ」、拡張子「.png」 … 相対フォルダ「%CD% \」
ファイル名「c ,.'`+-=~_()[]{};!#$%&@」、拡張子「.png」 … 相対フォルダ「」
ファイル名「dracula %Is C:\cu !r!r%e% nt\%CD% C:\cu !r!r%e% nt\\%CD% \ %rel%\」、拡張子「.png」 … 相対フォルダ「%CD% \\%CD% \ %rel%\」
ファイル名「z%% o%m%b%%Ie」、拡張子「.gif」 … 相対フォルダ「」

答:ダメ。

大丈夫な例

  • a縺薙s縺ォ縺。縺ッ
    • aのファイル名
  • c ,.'``+-=~_()[]{};!#$%&@
    • cのファイル名
  • z%% o%m%b%%Ie
    • zのファイル名
  • .png
    • a、c、dの拡張子
  • .gif
    • zの拡張子

※ bは .exe 面に堕ちたのでヒットしない。

ダメな例

  • %CD% !CD!\%CD% \
    • aの相対フォルダ名
  • dracula %Is !here!.pngdracula %Is C:\cu !r!r%e% nt\%CD% C:\cu !r!r%e% nt\\%CD% \ %rel%\
    • dのファイル名
  • %CD% !CD!\!rel! %rel%\%CD% \\%CD% \ %rel%\
    • dの相対フォルダ名

半角スペースとか単なる変な記号とか % の変数名とかはやりすごせても、! の方の変数名(しかも実在するやつ)を突かれちゃうと無理な模様。
バッチファイルだったら setlocal の範囲を最低限にすることで多少はコントロールできるらしいんだけど、ワンライナーだと多分、さすがにどうしようもないかな……。

心の声(ルート直下にあるファイルについては … 相対フォルダ「」 なんて表示自体要らないのでは…?)

ワンライナー
cmd /Q /V:ON /C "(for /R %I in (*.png *.gif) do ((set here=%~dpI) & (if !here!==%CD%\ (set rel=\) else (set rel=\ … 相対フォルダ「!here:%CD%\=!」)) & (echo ファイル名「%~nI」、拡張子「%~xI!rel:~1!)))" | sort | clip
書き下し文
cmd /Q /V:ON /C "
(
  for /R %I in (*.png *.gif) do (
    (
      set here=%~dpI
    ) & (
      if !here!==%CD%\ (set rel=\) else (set rel=\ … 相対フォルダ「!here:%CD%\=!」)
    ) & (
      echo ファイル名「%~nI」、拡張子「%~xI!rel:~1!
    )
  )
)
" | sort | clip
結果
ファイル名「a」、拡張子「.png」 … 相対フォルダ「sub\」
ファイル名「b」、拡張子「.gif」
ファイル名「c」、拡張子「.png」
ファイル名「d」、拡張子「.png」 … 相対フォルダ「sub\more\」
ファイル名「z」、拡張子「.gif」

if 文最高!

心の声(cmd "なんやかんや" の中に更なる " があったら…?)

うるせえなこいつ…。

今回は関係無いけど、一応。
もし cmd の実行内容が入れ子のダブルクォーテーションを持っていても、かしこい cmd.exe は実行内容の最初の " と最後 の " の2つだけを削除してから実行する(少なくとも1段階の入れ子を許容する)ので問題にならない。

cmd /C "echo Call me "Taxi"."
REM > Call me "Taxi".

かしこい。

cmd "なんやかんや" で処理全体を囲むもう1つの理由

大きな理由は遅延なんちゃらのため、と既に述べた。
実はもう1つ小さな理由があり、それは クリップボードにコピーした時の各行末のスペース除去コード自体の見やすさを両立するため。

ここにコマンドがあるじゃろ。

echo val1 val2 | clip

これを実行すると、クリップボードにコピーされるテキストはこうじゃ。

※説明のために、半角スペースを[sp]で置換してお見せしています。
val1[sp]val2[sp]

…見ての通り、末尾に余計なスペースがくっつく。
目的によってはそれを許容できることもあるが、今回はなんかキモいからお断り申し上げたい。
なんでこうなるのかというと、多分 echo は末尾のスペースをも文字列の一部として貪欲に捕捉してしまうのだろう。
故に、こう書けば一応は解決する。

※説明のために、結果の半角スペースを[sp]で置換してお見せしています。
echo val1 val2| clip
REM > val1[sp]val2

…が、いや今度はコード側がキモくない?​?​?

ワンライナーはただでさえ呪文のようになりがち。
それを少しでも見やすくするために、コマンド同士をどうのこうのする | とか & とか (カッコ) とかの前後にはできればスペースを挟んで記述したい、というのが個人的な願い。
cmd.exe はほんとそういうとこが**忌々しい。**←怨嗟の声

ということで試行錯誤すると、こんな風になった。

※説明のために、結果の半角スペースを[sp]で置換してお見せしています。
REM *commands*                 *results*
echo 1 test | clip             REM > 1[sp]test[sp]
echo 2 test| clip              REM > 2[sp]test
(echo 3 test) | clip           REM > 3[sp]test[sp]
(echo 4 test)| clip            REM > 4[sp]test[sp]
cmd /C "echo 5 test" | clip    REM > 5[sp]test[sp]
cmd /C "echo 6 test"| clip     REM > 6[sp]test
cmd /C "(echo 7 test)" | clip  REM > 7[sp]test
cmd /C "(echo 8 test)"| clip   REM > 8[sp]test

見ての通り、結果のスペース除去とコードのスペース空けを両立できるのは7番目の記法のみとなる。
特に (カッコ) の発見の意義は大きく、このおかげで今回のワンライナーは (カッコ) まみれとなったのであった。

ちなみに

そんなに頑張らなくても、3番目の記法と適当な一時ファイルを経由すれば、末尾のスペースは勝手に消える

※説明のために、結果の半角スペースを[sp]で置換してお見せしています。
(echo val1 val2) > %TMP%\tmp.txt & clip < %TMP%\tmp.txt & del %TMP%\tmp.txt
REM > val1[sp]val2

うるせえ!​!​! クリップボードに直接渡せるのがロマンなんだよ!​!​!​!​!​!
…そういうことにしておいてください。
っていうかなんでファイルか否かで出力差が出るんだよ……何なんだよほんと……。

for /R 〜 の代替手段

今回は一番短い for /R 〜 を採用したが、他にもある。

  • for /F "delims=" %I in ('dir /A-D/B/S *.png *.gif') do なんか
    • 機能が多いので、やりたいことが変わっても柔軟に対応しやすい。
      • その分めんどい。
    • ファイルではなくフォルダだけを得ようとした場合、以下の通り、今回の手法とは少し違う結果になる。
C:\Windows\system32\cmd.exe
C:\current> for /R %I in (.) do @echo %I
C:\current\.
C:\current\sub\.
C:\current\sub\more\.

C:\current> for /F "delims=" %I in ('dir /A:D/B/S') do @echo %I
C:\current\sub
C:\current\sub\more
  • for /F "delims=" %I in ('where /R %CD% *.png *.gif') do なんか
    • 単にファイルだけを検索してそのフルパスだけを返すので、わかりやすい。
      • その分自由度が低い。
        少なくとも、フォルダ名を得ることはできない。

他には forfiles というのもあり、こいつは相対フォルダ名もデフォルトで取得できるので可能性は感じる。
しかし、触った感じでは以下のように自由以上の不自由に直面したのでやめた。

  • 複数条件が指定不能。
  • 出力が何もかもダブルクォーテーションで囲まれている。
  • echo の出力内容にスペースが挟まるだけで問題が起こる。

その他

  • cmd 等の各種コマンド、/Q 等の各種パラメーター、%~dp0 等の引数置換構文、そして変数の命名では、大文字・小文字を区別しない。
    ただし、for の1文字変数だけはそれを区別する。なんで…?
C:\Windows\system32\cmd.exe
C:\current>set a=12345

C:\current>eChO %A%
12345

C:\current>FOR /r %I in (*.gif) DO @ECHO %~NXI %~NXi
b.gif %~NXi
z.gif %~NXi
  • コマンドをバッチファイルで試す時は UTF-8 で保存しない。(2敗)
  • 筆者環境:Windows 7 Professional 64-bit

参考

† 圧倒的感謝 †

Qiita

Stack Overflow

個人サイト系、他

おわり

  1. 元は C:\current\sub\a.png 2 3 4 5 6 7 8 9 10 11 12 13

  2. これ「えんさ」って読むんだ……今までずっと「おんさ」だと………合唱部かよ…………。

  3. 元は set rel=!here:%CD%=!

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?