wine $(find . -name "*exe" | fzf) が空白入りパスで失敗した話
Ubuntu を使っているが、 Wine で Windows 向けの実行ファイルを起動するとき、カレントディレクトリ以下から .exe を探して、fzf で選択して実行する簡単なハックを使っていたところ面白いエラーが出たのでまとめた記事。
はじめに
wine $(find ./ -name "*exe" | fzf)
これは直下の *.exe ファイルをあいまい検索して起動できる便利なワンライナーだ。
あるとき次のようなエラーが出た。
wine: failed to open "./path/to/some file": c0000135
最初はWine側のDLL不足や環境破損を疑ったが、調べてみると原因はもっと単純だった。
選択されたファイルパスに空白文字が含まれており、シェルによってパスが途中で分割されていた。
この記事では、原因と対策を整理する。
問題のコマンド
上述のコマンドがやりたいことは単純で、
-
findで実行ファイルっぽいファイルを探す -
fzfで候補をあいまい検索する - 選択したファイルを
wineに渡す
というもの。
ファイル名やディレクトリ名に空白が含まれていなければ、うまく動く。
例えば、fzf で次のようなパスを選んだ場合は問題ない。
./app/game.exe
実行されるコマンドは実質的にこうなる。
wine ./app/game.exe
しかし、パスに空白が入ると壊れる。
例えば、選択結果が次のようなものだった場合。
./example dir/game.exe
このとき、元のコマンドではシェルが空白で分割してしまう。
wine ./example dir/game.exe
つまり、wine には次のような複数の引数が渡る。
./example
dir/game.exe
本来は1つのパスとして渡したかったものが、空白で分割されてしまっている。
原因
原因は、コマンド置換 $(...) をクォートしていなかったこと。
上のコードのように書くと、$(...) の結果に対してシェルの単語分割が行われる。
そのため、結果に空白やタブなどが含まれていると、1つのパスではなく複数の引数として扱われる。
最低限の対策
まず、最小限の修正はこれ。
wine "$(find ./ -name "*exe" | fzf)"
$(...) 全体をダブルクォートする。
これで、fzf の選択結果が次のような空白入りパスでも、
./example dir/game.exe
wine には1つの引数として渡される。
wine "./example dir/game.exe"
これだけでも、今回の空白文字によるパス分割問題は解決できる。
もう少し安全な書き方
ただし、上記の対策だけだと、fzf をエスケープキーで離脱した場合などの扱いが微妙になる。単純に起動に失敗する。
そこで、選択結果をいったん変数に入れて、空でないことを確認してから実行することで解決する。
exe="$(find . -type f -iname "*.exe" | fzf)" && [ -n "$exe" ] && wine "$exe"
ただ、タイプ数が増えるので個人的にはやらないかな。
まとめ
今回のエラーは、見た目だけだとWine側の問題に見えた。
wine: failed to open "./path/to/some file": c0000135
しかし、実際にはWine以前に、シェルがパスを空白で分割していた。
つまり、問題の本体はWineではなくシェルの引数展開だった。
次のような書き方は危ない。
command $(some_command)
some_command の出力がファイルパスであり、そのパスに空白が含まれる可能性があるなら、クォートする。
command "$(some_command)"
変数に入れる場合も同じ。
file="$(some_command)"
command "$file"
ファイルパスを扱うシェルスクリプトでは、変数展開やコマンド置換をクォートすることを意識するのが重要。