初出の記事ではファイル名に括弧 ()
含まれる場合にエラーになっていたため,これを修正しました(2024年8月31日)
0. はじめに
Windows 上でクロス開発を行うときに起こる問題の一つに,ヘッダファイルの大文字・小文字問題がある。Windows のファイルシステムはファイル名の大文字・小文字を区別しないので,コーディング中にも気にしない人が多いが,いざコンパイルしようとするとヘッダファイルが見つからないというエラーに驚く。
本来,コンパイラに大文字・小文字を区別しないオプションがあれば良いのだが,残念ながらそのような便利なオプションは見当たらない。我々の開発チームはソースコードを書き換えたくないということでファイル名のほうを変更することになった。
※とても最善の策とは思えないが,チームの方針には従うしかない。
1. お品書き(仕様案)
- 簡単に作れるバッチファイルでミスなく機械的に処理したい。
大文字に変えるほうは to Upper Case ということでUCASE.CMD
,小文字に変えるほうは to Lower Case ということでLCASE.CMD
とする。 - 複数のファイル名を指定できる。ファイル名にはワイルドカードを使用できる。
- オプション
/V
を指定すると変更対象となるファイル名を表示するだけで変更しない。
※これは事前確認のため - オプション
/Q
を指定すると変更したファイル名を表示しない。デフォルトは表示する。 - オプション
/D
を指定するとディレクトリ名(のみ)を変更する。デフォルトはファイル名(のみ)を変更する。
2. ファイル名(フォルダ名)の変更方法
SET
コマンドの文字列置換機能を使う。実は SET
コマンドでは文字列検索の際に大文字・小文字を区別しないので小文字を大文字に変換する場合は set STR=%STR:a=A%
とする必要はなく set STR=%STR:A=A%
でも良い。このため
set STR=%STR:a=A%
set STR=%STR:b=B%
set STR=%STR:c=C%
set STR=%STR:d=D%
~中略~
set STR=%STR:z=Z%
のようにずらずらとアルファベット26文字分の変換処理を並べる必要はなく
for %%C in ( A B C D E F G H I J K L M N O P Q R S T U V W X Y Z ) do (
set "STR=!STR:%%C=%%C!"
)
のようにループ処理で書ける。ここで注意すべきことは
- ループ処理になるので遅延環境変数にする(変数名をパーセント記号
%
ではなく感嘆符!
で囲む)必要があること。またsetlocal enabledelayedexpansion
の指定も必要。 - プログラムのファイル名に括弧記号
()
を使うような人は滅多にいないと思うが,もしも使っているとfor
コマンドの括弧記号と混同してエラーになるので,二重引用符"..."
で囲む必要がある。二重引用符で囲む範囲にも注意されたい。
ちなみに小文字に変換するときは下記のようになる。
for %%C in ( a b c d e f g h i j k l m n o p q r s t u v w x y z ) do (
set "STR=!STR:%%C=%%C!"
)
3. FOR コマンドの闇
複数のファイルをループ処理したい場合は for
コマンドを使うと便利である。コマンドラインで指定したファイル名(複数)を %*
とする。
for %%I in ( %* ) do ...
一方,フォルダを対象とする場合は /D
オプションを付ける。
for /D %%I in ( %* ) do ...
ここで引数 %*
を展開した文字列がワイルドカードを含む場合,実在するファイル(またはフォルダ)のみを列挙してくれるのだが,ワイルドカードを含まない場合,そのようなファイル(またはフォルダ)が実在しなくても引数の文字列をそのまま列挙してしまうので注意が必要だ。
4. ファイルとフォルダの区別
ファイル(またはフォルダ)の実在チェック自体は if exist %%I ...
で可能だが,ファイルとフォルダの区別がつかない。このためファイルの属性 %%~aI
を得て判断する。ファイルの場合は下記のようになる。
--a--------
一方,フォルダの場合は下記のようになる。
d----------
すなわち文字列の先頭が -
の場合はファイルであり,d
の場合はフォルダとなる。ちなみにファイル(またはフォルダ)が存在しない場合は空文字列になる。Windows10 22H2 の場合,属性文字列の長さは 11 文字であり,属性の割り当ては下記のようになっている。
※適当な資料が見当たらないため筆者の調査による。
drahscotl-x
d ... ディレクトリ
r ... 読み取り専用
a ... アーカイブ
h ... 隠しファイル
s ... システムファイル
c ... 圧縮
o ... オフライン
t ... テンポラリ
l ... 再解析ポイント
x ... スクラブファイルなし
ちなみに l
と x
の間にある属性は謎である。暗号化 e
やスパースファイル p
でもないし,インデクス化 i
でも無さそうだ。
5. 実装コード
実装コードを以下に示す。
@echo off
rem ------------------------------------------------------------------------
rem ファイル名(複数可)を大文字に変換する
rem ------------------------------------------------------------------------
setlocal enabledelayedexpansion
set DOPT=0
set QOPT=0
set VOPT=0
rem ------------------------------------------------------------------------
rem オプション解析
rem ------------------------------------------------------------------------
set ARGS=
if "%~1"=="" goto USAGE
:LOOP
set OPT=%~1
if /I "%OPT%"=="/D" set DOPT=1&&goto ENDIF
if /I "%OPT%"=="/Q" set QOPT=1&&goto ENDIF
if /I "%OPT%"=="/V" set VOPT=1&&goto ENDIF
if "%OPT:~0,1%"=="/" goto ERROR
if defined ARGS (
set ARGS=%ARGS% "%~1"
) else (
set ARGS="%~1"
)
:ENDIF
shift
if "%~1"=="" goto BREAK
goto LOOP
:ERROR
echo オプション %OPT% には対応していません!
exit /b
:BREAK
if not defined ARGS (
echo ファイル名を指定して下さい!
exit /b
)
rem ------------------------------------------------------------------------
rem ファイル名/フォルダ名を変更する
rem ------------------------------------------------------------------------
if "%DOPT%"=="1" (
for /D %%I in ( %ARGS% ) do (
set ATTR=%%~aI
if "!ATTR:~0,1!"=="d" call :CONVERT "%%~I"
)
) else (
for %%I in ( %ARGS% ) do (
set ATTR=%%~aI
if "!ATTR:~0,1!"=="-" call :CONVERT "%%~I"
)
)
exit /b
rem ------------------------------------------------------------------------
rem ファイル名/フォルダ名を変更する
rem ------------------------------------------------------------------------
:CONVERT
set OLDNAME=%~nx1
set NEWNAME=%OLDNAME%
for %%C in ( A B C D E F G H I J K L M N O P Q R S T U V W X Y Z ) do (
set "NEWNAME=!NEWNAME:%%C=%%C!"
)
if "%OLDNAME%"=="%NEWNAME%" exit /b
if "%QOPT%"=="0" echo %~1 %NEWNAME%
if "%VOPT%"=="0" ren "%~1" "%NEWNAME%"
exit /b
rem ------------------------------------------------------------------------
rem ヘルプメッセージ
rem ------------------------------------------------------------------------
:USAGE
echo ファイル名(複数可)を大文字に変更します。
echo.
echo UCASE(.CMD) [/D] [/Q] [/V] [ドライブ:][パス][ファイル名]
echo.
echo /D フォルダ名を変更します。
echo /Q 変更したファイル名を表示しません。
echo /V 変更対象となるファイル名を表示するだけで、変更は行いません。
exit /b
小文字に変換するほうも同様である。
@echo off
rem ------------------------------------------------------------------------
rem ファイル名(複数可)を小文字に変換する
rem ------------------------------------------------------------------------
setlocal enabledelayedexpansion
set DOPT=0
set QOPT=0
set VOPT=0
set ARGS=
rem ------------------------------------------------------------------------
rem オプション解析
rem ------------------------------------------------------------------------
if "%~1"=="" goto USAGE
:LOOP
set OPT=%~1
if /I "%OPT%"=="/D" set DOPT=1&&goto ENDIF
if /I "%OPT%"=="/Q" set QOPT=1&&goto ENDIF
if /I "%OPT%"=="/V" set VOPT=1&&goto ENDIF
if "%OPT:~0,1%"=="/" goto ERROR
if defined ARGS (
set ARGS=%ARGS% "%~1"
) else (
set ARGS="%~1"
)
:ENDIF
shift
if "%~1"=="" goto BREAK
goto LOOP
:ERROR
echo オプション %OPT% には対応していません!
exit /b
:BREAK
if not defined ARGS (
echo ファイル名を指定して下さい!
exit /b
)
rem ------------------------------------------------------------------------
rem ファイル名/フォルダ名を変更する
rem ------------------------------------------------------------------------
if "%DOPT%"=="1" (
for /D %%I in ( %ARGS% ) do (
set ATTR=%%~aI
if "!ATTR:~0,1!"=="d" call :CONVERT "%%~I"
)
) else (
for %%I in ( %ARGS% ) do (
set ATTR=%%~aI
if "!ATTR:~0,1!"=="-" call :CONVERT "%%~I"
)
)
exit /b
rem ------------------------------------------------------------------------
rem ファイル名/フォルダ名を変更する
rem ------------------------------------------------------------------------
:CONVERT
set OLDNAME=%~nx1
set NEWNAME=%OLDNAME%
for %%C in ( a b c d e f g h i j k l m n o p q r s t u v w x y z ) do (
set "NEWNAME=!NEWNAME:%%C=%%C!"
)
if "%OLDNAME%"=="%NEWNAME%" exit /b
if "%QOPT%"=="0" echo %~1 %NEWNAME%
if "%VOPT%"=="0" ren "%~1" "%NEWNAME%"
exit /b
rem ------------------------------------------------------------------------
rem ヘルプメッセージ
rem ------------------------------------------------------------------------
:USAGE
echo ファイル名(複数可)を小文字に変更します。
echo.
echo LCASE(.CMD) [/D] [/Q] [/V] [ドライブ:][パス][ファイル名]
echo.
echo /D フォルダ名を変更します。
echo /Q 変更したファイル名を表示しません。
echo /V 変更対象となるファイル名を表示するだけで変更しません。
exit /b
6. 実行例
引数なしで実行するとヘルプメッセージを表示する。
C:\>ucase
ファイル名(複数可)を大文字に変更します。
UCASE(.CMD) [/D] [/Q] [/V] [ドライブ:][パス][ファイル名]
/D フォルダ名を変更します。
/Q 変更したファイル名を表示しません。
/V 変更対象となるファイル名を表示するだけで変更しません。
ファイル名の指定にはワイルドカードを使用できる。変更前後のファイル名を表示する。区切り記号はタブである。
C:\Qiita>ucase *.h
sample.h SAMPLE.H
また,カレントフォルダのファイルに限らず,以下のようにサブディレクトリ以下のファイルも指定できる。
C:\Qiita>ucase include\*.h
include\sample.h SAMPLE.H
7. 今後の課題
フォルダの階層が深いと,サブディレクトリ以下を再帰的に検索する機能が欲しくなる。ただ,これをバッチファイルで実現しようとするといくつかの技術的課題をクリアしなくてはならない。
8. 参考文献
そもそものクロスコンパイルの問題は参考文献[1][2]の技術を使えば解決できそうであるが,敷居が高そうだ。ファイル名の大文字・小文字変換のテクニックは参考文献[5]による。文献[6]によればfor
コマンドのファイル属性文字列は Windows 2000 の頃は長さ 6 文字までしか無かったようだ。OS のバージョンアップに伴い,ファイル属性文字列は拡張されて長くなっているが,文字列の順序(上位互換性)は保たれているようである。
- clang cross compile では case-sensitive file system では大文字小文字を区別しないヘッダを読み込めない - Qiita
- clang-cl(clang) で case-insensitive でヘッダファイルを読み込む - Qiita
- 汝、コマンドプロンプトを愛せよ - Qiita
- ファイル名が強制的に大文字になるときの対処 - Qiita
- コマンドプロンプト(cmd.exe)私的メモ - Qiita
- Windows 2000活用講座 Windows 2000 コマンドライン徹底活用 第7回 forコマンド - itmedia
- Windowsのエクスプローラーで表示される属性情報文字の意味は? - itmedia