sh/cmd.exe両対応スクリプトの作成方法
概要
Windows 10にWSLが搭載されてから,Windows環境でbash (sh) を使うことが以前よりも増えてきた。
その他,Git for WindowsをインストールするとMSYS2も一緒にインストールされるので,WSLを有効にしていなくても,bashを使えるということは多い。
ただし,WSLはあくまで開発機能であり,標準では有効になっていない。非開発者など純粋なWindowsユーザーも数多いだろう。
Windowsでのスクリプトとしては,コマンドプロンプト (cmd.exe) によるコマンドファイル (.cmd) やバッチファイル (.bat) が昔から数多く使われている。最近はPowerShellも使われているが,こちらはWindows XPやVistaなどで使えなかったり,セキュリティ設定などいろいろ面倒くさいこともあり,cmd.exeが相変わらずシンプルだ。
MSYSやWSLを含むGNU/Linux環境ではbash (sh) が標準的なシェルスクリプトファイル (.sh) として使われている。
MSYSやWSLなどでWindowsにおいてcmd.exeやPowerShellとshを併用している場合,現在のシェルによってコマンドファイルとシェルスクリプトの実行可否が異なる。
そのため,インストーラーなどの共通処理などでは一般的には複数のシェルから実行できるように,同じ処理内容のコマンドファイルとシェルスクリプトが用意することが多い。
簡単な処理などでは,現在のシェルに問わず,どちらのシェルからでも使えるスクリプトファイルを用意できると,同じ内容のファイルを環境毎に用意する必要がなくなる。
実は,書き方を工夫することで,cmd.exeからもshからも実行できるスクリプトファイルを用意することができる。
そこで,今回はsh/cmd.exeの両対応スクリプトの作成方法について整理する。
アイデア自体は数年前からあったのだが,必要な場面が特になく,投稿にまとめるのを億劫にしていた。最近,このテクニックを使う場面があったので,重い腰を上げて整理した。
結論としては,以下のようなコード (ファイル拡張子.cmd
) で実現する。
: cmd.exe block
: <<-"EOC"
@echo off
echo cmd.exe
exit /b
EOC
: sh block
echo sh
サンプルコードを「example/CMD/cmdsh at master · senooken/example」で公開している。
方法
基本的な考え方として,cmd.exeとshとで,コメント文字,無害なコマンドの組み合わせにより実現する。
具体的には,shとcmd.exeで以下の要素を組み合わせる。
- sh:
:
コマンド,ヒアドキュメント - cmd.exe: gotoラベル (
:
)
shとcmd.exeの両方に登場している:
が重要なポイントとなる。
shにおける:
は「POSIX:2004」にあるとおり,何もしないコマンドだ。機能としては,true
コマンドと同じだ。構文エラーを防ぐため,空のif文を用意する際にこのコマンドを配置することが主な用途だ。その他,何もしないことを利用して,引数で変数展開などを行ったりする。
cmd.exeにおける:
はgoto
コマンドのジャンプ先のラベルの1文字目として使われる文字であり,:
で始まる行はgoto
のラベルとみなされ無視される。
このcmd.exeで:
で始まる行が無視されることと,shで何もしないコマンドであることを利用する。
具体的には以下のようにコーディングすることで,ファイル内でshとcmd.exeを混在できる。
: \file cmdsh.cmd
: cmd.exe block
: <<-"::EOC"
@echo off
echo cmd.exe
exit /b
::EOC
: sh block
echo sh
cmdファイル内で1行だけshを実行したい場合は以下のようにしてもいい。
: Only sh executed
:; echo sh
例えば,以下のように実行して動作を確認できる。
cmd.exe /c cmdsh.cmd
sh cmdsh.cmd
cmd.exe
sh
意味を説明する。
:
の後にヒアドキュメントを指定することで,ヒアドキュメントの開始はcmd.exeで無視されるものの,ヒアドキュメント本体はcmd.exeでだけ実行される。
shにとってはヒアドキュメントは何もしないコマンド:
の標準入力に指定しているだけなので,ヒアドキュメントのブロックは何もなく終わる。ヒアドキュメントの後からshが実行される。
なお,ヒアドキュメントに,先頭のタブを無視する<<-
を指定しているが,先頭のタブを無視しない<<
でも問題ない。
ヒアドキュメント内でコマンド置換の構文 ($()
と``
) が登場すると,shからの実行時に意図せずコマンドが実行される可能性があるため,ヒアドキュメントの文字列のEOC
を二重引用符 ("
)か一重引用符 ('
) で囲んで,コマンド置換が展開されないようにしている。
なお,cmd.exeとshをそれぞれ対応する処理毎に併記したい場合,以下のようにcmd.exeのヒアドキュメントのブロック間をgotoでジャンプすることで対応する。ただし,可読性が悪くなるので推奨はしない。
: Process 1: cmd.exe <<-"EOC"
@echo off
echo cmd.exe
goto Process
EOC
# Process 1: sh
echo sh 1
: Process 2: cmd.exe <<-"EOC"
@echo off
echo cmd.exe
goto Process
EOC
# Process 2: sh
echo sh 2
: Process 3: cmd.exe <<-"EOC"
@echo off
echo cmd.exe
exit /b
EOC
# Process 3: sh
echo sh 3
なお,gotoはラベル:
の直後のスペースは無視するようで,:2
でも: 2
でも同じだった。先頭からの部分一致でgotoの下側優先でジャンプするようで,同じラベルを複数回指定しても問題なく下に進んだ。
注意点
今回のsh/cmd.exeの両対応スクリプトの作成時にはいくつか注意点があるので解説する。
まず,ファイル名に.cmd
の拡張子を指定する。Windowsでは拡張子でプログラムを判断しており,.sh
の拡張子は標準では直接起動できない。.cmd
の拡張子であれば,ファイル名でそのまま実行できる。一方,sh側はファイルの拡張子は関係ない。POSIXでは,実行権限があり,ELFでなければshのスクリプトファイルと見なして実行される (参考: What is shebang (#!/bin/sh) in POSIX shell script – senooken.jp)。
そして,改行コードにはLFを採用する。これはsh側の都合だ。shでの改行コードはLFとなっており,CRは文字列とみなされる。改行コードがCRLFだとCRは余計な文字とみなされ,行末をコメントアウトしないとエラーが出る。
最後に,改行コードにLFを採用するのと似ているが,スクリプトファイル内では日本語などの多バイト文字を使わない。これは,cmd.exe側で他バイト文字を解釈するには言語にあった文字エンコーディングを採用し,改行コードもCRLFにする必要があるからだ。そうしなければ,cmd.exeが文字列を適切に解釈できずエラーとなる。
日本語を使う場合,スクリプトファイルにいくつか制限が発生する。まず,文字エンコーディングはShift_JISかCP932 (Windows 932) 限定で,改行コードをCRLFにする必要がある。
また,改行コードをCRLFにする場合,ヒアドキュメントのブロックを除いて,shで実行される箇所は全て行末に#
を配置して,行末の改行 (CR) をコメントアウトする必要がある。
特に,sh側で全行末への#
の配置が厳しい制限となる。日本語を使わないほうがいいだろう。
結論
sh/cmd.exeの両対応スクリプトの作成方法を記した。
基本的に,日本語を使わなければ,書き方を若干工夫するだけで実現できる。ただ,複雑で長いコードになる場合,可読性や保守性が悪くなるだろう。あくまで,両対応スクリプトは1画面に収まるような簡単な処理に留めるにしたい。
Windows 10やGitの普及が進み,Windows環境でもshを使うことが増えてきた。
以前はこのようなsh/cmd.exeの両対応スクリプトは使う場面がないだろうと思っていたのだが,実際にWindowsで開発を行うと,あると嬉しい場面に遭遇した。
今後,WindowsとUNIX系OSが混在する開発では,今回の両対応スクリプトで簡単な共通スクリプトを用意してみたい。