「Windowsでエクスプローラーからフォルダを各種ターミナルで開く」の版間の差分
(Notion-MW) |
(Notion-MW) |
||
51行目: | 51行目: | ||
* powershell.exe | * powershell.exe | ||
** -Commandがなくても付けたのと同じ扱いになる? | ** -Commandがなくても付けたのと同じ扱いになる? | ||
*** 逆に、- | *** 逆に、-Commandを付けなければいけないという点を除いてはpwshがpowershellの上位互換という感覚である(新しいのでそれはそう)。 | ||
** <code>`</code>や<code>[]</code>を含むフォルダの扱いにバグがあり、cmdなどでこれらのフォルダをカレントディレクトリとした状態で5系のPowerShellを起動すると<code>C:\</code>や<code>C:\Windows\System32\WindowsPowerShell\v1.0></code>をカレントディレクトリとして起動される(たとえば<code>C:\somefolder][</code>なら前者、<code>C:\somefolder[]</code>なら後者)。 | ** <code>`</code>や<code>[]</code>を含むフォルダの扱いにバグがあり、cmdなどでこれらのフォルダをカレントディレクトリとした状態で5系のPowerShellを起動すると<code>C:\</code>や<code>C:\Windows\System32\WindowsPowerShell\v1.0></code>をカレントディレクトリとして起動される(たとえば<code>C:\somefolder][</code>なら前者、<code>C:\somefolder[]</code>なら後者)。 | ||
** -Fileを使用してスクリプトを実行するとき、ダブルクォーテーションで囲われていない <code>-</code> が引数に含まれているとエラーになるバグがある。 | ** -Fileを使用してスクリプトを実行するとき、ダブルクォーテーションで囲われていない <code>-</code> が引数に含まれているとエラーになるバグがある。 |
2024年10月9日 (水) 16:23時点における最新版
Explorerの右クリックメニューやアドレスバーからフォルダを各種ターミナルで開くための設定方法の紹介。cmd.exe, powershell(pwsh含む), Cygwin, Git Bashの4つに対応。
基本
- Windowsの右クリックメニューから複数ファイルをまとめて開く のように、レジストリでフォルダ・フォルダ背景の右クリックメニューを編集できる。
- Windows10までの右クリックメニューに戻そう。
command
キーの既定値に直接コマンドを書く方法と、DelegateExecute
値(など)を使ってCOMを通じて実行する方法(このためにhttps://github.com/ge9/ExecuteCommand-Pipe(使い方はレポジトリの説明を参照)を作った)がある。- HKLMにあるマシン全体の設定は、それ自体は管理者権限(あるいはTrustedInstallerなどさらに上の権限)がないと書き換えられないが、HKCUに値があればそちらが優先されるので、自分だけに関する設定変更ならユーザー権限だけで可能である。
Directory
やDirectory\Background
の直下にrunas
という名前でキーを作った場合は、自動的にcommand
の内容が管理者権限で実行される。ただ、runas
という固定のただ一つの名前しか使えないため、例えば管理者としてコマンドプロンプトを開くメニューとPowerShellを開くメニューを共存させることができない。そのためこの記事ではrunas
キーを使用するのではなくコマンドの中で明示的に管理者としての実行を行っている。
- Explorerのアドレスバー(address bar, location barなど)にcommandと入力してEnterを押すと、そのフォルダをカレントディレクトリとして
command
というコマンドが実行される。- このアドレスバーの仕様はおそらく「ファイル名を指定して実行」とほぼ同じ。
command
で(メインとして)指定するファイルはフルパスなら必ず動作し、Pathが通っているまたはApp Pathsに登録されているならファイル名だけでよく、さらにbatやexeの場合は拡張子も省略できる。さらに、ほとんど知られていないが実はApp Pathsの値には実行ファイル以外も書けるようである。 - つまり、パスの通っているフォルダにあるimage.pngはフルパスを打たなくても「image.png」で開けるし、さらにはApp Pathsの
openimage.exe
というキーにimage.pngのフルパスを書けばopenimage
と打つだけで(実際にはそのような実行ファイルは全く存在しないにもかかわらず)image.pngが開くようになる。当然、vbsなども拡張子無しで呼び出せることになる。
- このアドレスバーの仕様はおそらく「ファイル名を指定して実行」とほぼ同じ。
ポイント
パス名のルール
- Windowsではパスの長さが260文字以下という制限がある。詳しくはWindowsのパス長さ制限に関して 参照。
- 制限を超えるような長いパスは
\\?\
というプレフィックスを付けて(UNCパスの一種として)扱われる。- このようなフォルダをカレントディレクトリとしてコマンドを実行することはできないので、アドレスバーに打ち込む方式は使えない(System32をカレントディレクトリとして実行される)。wt -dなどにも指定できない。
- 右クリックメニューでは、commandの既定の値に直接書く方式の場合は、
NoWorkingDirectory
を設定しないと呼び出されなくなる。 - シンボリックリンク/ジャンクションでパスを短くしてやれば普通に扱えるようになる
- 制限を超えるような長いパスは
- 普通に入力できる文字でWindowsのファイル名に使えないのは、
\
/
:
*
?
"
<
>
|
の9個(エクスプローラーの画面で入れようとすると表示される)。- その他、ヌル文字やタブスペースのような0x20未満の制御文字も使えない(はず)。
- これらを含むファイルはGit Bash/Cygwinを使っても作れない。
- NTFS自体の制限はおそらく
/
だけで、それ以外に関してはLinux上で作ってからWindowsで見ると見られることもある。中身の閲覧などは不可。
- その他、ヌル文字やタブスペースのような0x20未満の制御文字も使えない(はず)。
- また、末尾が空白またはピリオドのファイルも禁止で、Explorerやcmdでは作れない(自動で除去される)。
- これらはGit Bash/Cygwinなら作れる。前者はエクスプローラーで閲覧など多少の操作ができるが後者はほとんど何もできず、DelegateExecuteの右クリックメニューでもピリオドが外れた名前しか取得できない。
- 先頭が空白のファイルはExplorerだと作れないがcmdなら作れる。禁止ではないはず。
Git Bash/Cygwin
- Git BashではWindowsの実行ファイルにスラッシュ付きオプションを渡すときはスラッシュを2つにする必要がある。
cmd //c
やbcdedit //v
など。 - printf関数の%qオプションを使うと、文字列をエスケープされた状態にして返してくれる。これはshからの呼び出し先の別のshで再びフォルダ名をcdなどの引数として利用したいときに使える。
- bash(POSIX互換ではない)の組み込みコマンドのprintfと、独立実行ファイル(GNU Coreutilsの一部)である/bin/printfがある。基本的な趣旨としては同一のコマンドだが、組み込みコマンドのほうは入力に
"
が含まれていない限り結果にも"
が使われることはないようで、このおかげで意図しない動作を回避できることがある。今回扱う例に関しては前者が後者の上位互換であるということになる。後者を使っているものはそれでも動いたものである。 - そもそも%qは組み込みコマンドのほうにしかないという話もある。環境によって違いそう。
- bash(POSIX互換ではない)の組み込みコマンドのprintfと、独立実行ファイル(GNU Coreutilsの一部)である/bin/printfがある。基本的な趣旨としては同一のコマンドだが、組み込みコマンドのほうは入力に
\\?\
が付くような長いパスをカレントディレクトリにしていても、Git Bash/Cygwinに付属の(というかMSYS/Cygwin向けにコンパイルされた?)exeなら実行できる。- Git Bashの(ba)shは
--login
をつけて起動してもカレントディレクトリが維持されるが、Cygwinの(ba)shは--login
をつけると強制的にホームディレクトリに移動する。- 2024/4頃、Cygwinの挙動が変化し、カレントディレクトリが維持されるようになっていることに気付く。変わっていた方のバージョンは3.5.3、変わっていない方は3.3.6であった。
- ところで、Start bash shell (cygwin) with correct path without changing directory - Super Userの通り、
CHERE_INVOKING
という変数を1に設定しておけばカレントディレクトリは維持される。
- Git Bashにおいては環境変数を正しく設定するため
--login
の時点で環境変数MSYSTEM
をMINGW64
にしておく必要がある。Windowsでのターミナル環境 参照。
powershell/pwsh
- バージョン5まで(Windows PowerShell)はpowershell.exe、6以降(PowerShell Core, PowerShell)はpwsh.exeと実行ファイル名が違う
- 以後、「powershell(.exe)」と「pwsh(.exe)」で呼び分ける。ただし手元ではpowershellの5.1とpwshの7.2~7.3くらいでしか試していないので、一部バージョンではまた違う可能性もある。
- pwshは必ずしもWindowsに標準では入っていないので、以下の例でもあまり依存しないように気を付ける。
- 基本、シングルクォートで囲えばほとんどの特殊文字が無効になり、シングルクォート自体を書くときは2つ並べる。ただし注意点として、驚くべきことにpowershell/pwshでは「全角のシングルクォート(U+2018-U+201B、
‘
’
‚
‛
の4つ)」も通常のシングルクォートと同じ効力があるので、同じようにエスケープする必要がある(参照: https://learn.microsoft.com/ja-jp/powershell/scripting/lang-spec/chapter-02?view=powershell-7.2)。掲載したスクリプトでは省略しているがやることは同じ。
- powershell.exe
- -Commandがなくても付けたのと同じ扱いになる?
- 逆に、-Commandを付けなければいけないという点を除いてはpwshがpowershellの上位互換という感覚である(新しいのでそれはそう)。
`
や[]
を含むフォルダの扱いにバグがあり、cmdなどでこれらのフォルダをカレントディレクトリとした状態で5系のPowerShellを起動するとC:\
やC:\Windows\System32\WindowsPowerShell\v1.0>
をカレントディレクトリとして起動される(たとえばC:\somefolder][
なら前者、C:\somefolder[]
なら後者)。- -Fileを使用してスクリプトを実行するとき、ダブルクォーテーションで囲われていない
-
が引数に含まれているとエラーになるバグがある。
- -Commandがなくても付けたのと同じ扱いになる?
- powershell.exe・pwsh.exe共通
- 連続した
`
があるフォルダに関する挙動にバグがある。コンソールプログラムを実行しても新規ウインドウで開かれる、そこをカレントディレクトリとしたときにファイルをmvできないなど。 `
や[]
を含むフォルダに関しても、絶対パス指定したりきちんとエスケープしたりすれば操作自体はできる(場合もある)。- エスケープはいずれも直前に
`
を付ける。 - これらを特殊文字として扱いたくないときは
-LiteralPath
を付けると良い場合もある。
- エスケープはいずれも直前に
- UNCパスをカレントディレクトリにすることができるが、
Microsoft.PowerShell.Core\FileSystem::\\
とかいう謎のプレフィックスが付く。 - 末尾が空白のフォルダについてはcdできず、Cygwinなどでそのようなフォルダをカレントディレクトリとして起動したとしても
C:\Windows\System32\WindowsPowerShell\v1.0
やC:\Program Files\PowerShell\7
に移動してしまう。 - 8.3形式のパスを強制的に長い名前に戻してcdしてしまう。その結果、長いフォルダをカレントディレクトリとして外部プログラムを実行できなくなる。
- 連続した
- PSModulePath環境変数の関係な気がするが、pwshの中でcmdを起動してその中でpowershellを起動したりするとPSReadLineモジュールが読み込めない(
PSReadline モジュールを読み込めません。コンソールは PSReadline なしで実行されています。
)とか言われたりする
環境変数への置換
cmdなどを使うと%
で囲われた環境変数(%PATH%
など)が置換される。%
はファイル名にも普通に使える文字なので、注意が必要となる
- 該当の名前の環境変数が定義されていたときのみ置換が行われ、定義されていなければそのままになる。
%
VAR
%
の置換は必ずしもcmdだけで行われるわけではない。「ファイル名を指定して実行」、VBSのWscript.Shellのrun、conhost、wtの引数などでも行われる。しかしCreateProcessやShellExecute(Ex)やレジストリ値では行われない(無理やりREG_EXPAND_SZにしたら展開されるが、後述の%V
との適用順などは不明)。- 一般には、環境変数には英数字とアンダースコアしか使えないとの説明もよくあるが、実際にはほとんどの記号が有効である。従って、
^%PATH^%
のようなキャレットによるエスケープはcmd向けにはある程度有効だが、PATH^
という環境変数が定義されているとうまく動かない。 - (そういう変数が定義されていることは現実にほとんどないだろうし、それが保証できるなら以下の部分を読む必要はない)
これを防ぐ方法はいくつかある。ちなみに、%%
でのエスケープというのはバッチファイルの中だけの話で、今回の場面では以下より簡単なエスケープ方法は多分ないと思う。
既に定義されているかもしれない環境変数をバックアップし、一時的にその中身を
%
に変えて使用することでリテラル文字としての%
を表現し、後で元に戻す。すなわち、
MY_PERCENT
のような変数の中身をバックアップし、MY_PERCENT
の中身を%
に変え、未知の文字列中の%
をすべて%MY_PERCENT%
にすることでこの部分が置換によって%
に変わるのを利用して%
をそのまま渡すということである(環境変数の置換は左から順に行われるため、たとえば%MY_PERCENT%PATH%MY_PERCENT%
の%PATH%
が置換されることはない)。受け渡しが終わったらMY_PERCENT
の中身を復元する。これは、変数のバックアップとリストアを同じプロセス・スクリプト内で行える(呼び出した後に元に戻ってくる)場面で有用である。呼び出し先プロセスでリストアすることも不可能ではないと思うが、環境変数には任意の文字が入る可能性があることを考えるとエスケープが大変困難になることが予想される。
VAR1, VAR2, VAR3, …のような無限個の変数を最初からチェックし、最初の未定義の変数を上記と同様に使用し、後で未設定にする。
ほぼ上記と同じだが、内容のリストアでなく未設定にするだけなので、呼び出し先プロセスでも容易に行える。ただし実装が面倒である。
%
を==%
へと置換して、置換のプロセスを抜けた段階で元に戻す。- これは、最初の文字以外に
=
が含まれる環境変数名は(実際に環境変数が設定されていたとしても絶対に)置換されないことがわかったためである。%=%
(あるいは、%=VAR%
など)は少なくともcmdの対話シェル上だと置換されてしまうので=%
へのエスケープだと不十分に見えるが、実際には=
一つでも問題なく動く場合もあった。- しかし、動かないときもあるので、多くの例では
==%
にエスケープしている。=%
になっているものは、試した限りはそれでも問題なく動いているものである。 - Environment Variables - Win32 apps | Microsoft Learnでは「
=
は環境変数に使えない」と書かれていて、ダイアログからでも設定はできないが、レジストリだと無理やり設定できる。- ユーザー環境変数に設定する分にはとりあえず問題なさそうだが、システム環境変数にレジストリから
=
(と、=
を名前に含むいくつかの変数。どれが原因かは正確にはわからない)を無理やり追加したら0xc000021のブルースクリーンでWindowsが起動しなくなった(該当の変数を全て削除したら治った)のでこれはやってはいけない。
- ユーザー環境変数に設定する分にはとりあえず問題なさそうだが、システム環境変数にレジストリから
- ちなみに、
=
や%
はUnix側では特殊文字ではないので、printfとの適用順はあまり気にする必要はない。
- この方法のメリットは、一度置換してしまえばその文字列を何度使いまわしても変化がないことが保証されることと、未定義の変数の探索や変数のリストアが必要ないことである。
- 要は
%(=を含まない文字列)%
を含まない文字列だけを値にとる単射を構成すればいいだけなので、base64エンコーディングのようにしても構わない。
- 要は
- ただ、
==%
を%
に置換するという操作はPowerShellやshにとっては容易でもcmdにとっては不可能に近いので、最終的な呼び出し先がcmdであるときには採用しづらい。
- これは、最初の文字以外に
また、今回の記事ではcmdやvbsのRunを経由してコマンドを実行しているものが多くあるが、このスクリプトの内容自体もエスケープが必要である。つまり、例えばcmd /c
の内側に$mypath.Replace('%','%MY_PERCENT%')
というPowershellコードが含まれるなら、','
やMY_PERCENT
という環境変数が定義されていた時にその部分が置換されてしまう。これを回避するのはそこまで難しくなく、この例であれば$mypath.Replace('%'.Trim('='),'%'.Trim('=')+'MY_PERCENT%')
のように=
を含む無意味なコードを挿入すればよい。また、%
をいったん==%
にしている部分に関しては、==%
ではなく==%==
にすることで問題を避けられるだろう。無駄に複雑になるのでスクリプト例ではそのような措置はしていない。
cmd
- UNCパス(
\\?\
だけでなく普通のUNCパスも含む)をカレントディレクトリにすることはできない(rdなど一部コマンドでの取り扱いは可能)。 - 末尾が空白のフォルダにcdすることはできないが、Cygwinなどでそのようなフォルダをカレントディレクトリとした状態でcmdを起動した場合は、そのフォルダをカレントディレクトリとして起動する(外部プログラムの起動はできないがcdなどの内部コマンドは有効で、一度でも出たら戻れない)。
- 各種のパース規則が本当に謎。特に引用符周りは地獄である。cmd.exe のコマンドラインの仕様を解析してみた - 永遠に未完成を読むとわかるが、例えば「ファイル名を指定して実行」で以下の挙動を確かめよう。
cmd /k echo "a b"c d"
→"a b"c d"
と出るcmd /k echo "a & b"c d"
→"a & b"c d"
cmd /k echo "a b"c & d"
→echo "a b"c
とd"
がそれぞれ別々に実行される- すなわち、
"
が一見特殊文字でないかのような振る舞いをする(そのまま出力される)割に、その前後で特殊文字の扱いが切り替わっている。
Windows Terminal
- 以下、wtと略す(実行ファイル名がwt.exeなため)。パスが通っているものと仮定する。
- wtでは
;
が特殊文字になるようで、\;
とエスケープが必要。https://github.com/microsoft/terminal/issues/13264 かな?"
も\"
にする必要がある。一方で、単一の\
は(うしろに;
や"
などがなければ)そのまま\
になるようである。大丈夫なのか…?
- 起動時にカレントディレクトリがユーザーフォルダ(%USERPROFILE%)になる
-d
オプションで明示的に指定することができる
連続空白
空白が連続するファイル名の対応は割と厄介である。Windows側では引数を""
で囲うことで、またsh側では適宜IFS=
と設定することで対処できる。
Unicode対応
- UTF8を使おう
chcp 65001
が一応使えるが、一旦シェル内部に入らないと使えないのでやや不便- 使わなくても大丈夫なときもある。あまりちゃんと理解できていないが、経験上、powershellで
$input
変数を用いて標準入力を受け取る際にはchcp 65001
が必要そう。
- 使わなくても大丈夫なときもある。あまりちゃんと理解できていないが、経験上、powershellで
- /bin/printfやprintfの実行時には
LANG
をen_US.UTF8
とかに設定する。- Cygwinなら不要?
デバッグ時
- 管理者権限のものを試すときでもまず
-Verb Runas
を外して動作確認するとよい。 - 後述のwaitrunも役に立つだろう。
- 特殊文字に関しては、最初は空白のないフォルダ、次に空白のあるフォルダや連続空白のあるフォルダ、次にシングルクォートや
%PATH%
を含むフォルダなどとだんだん難しくしていくとよい。 - それとは別に、Unicode対応、
C:\
(ドライブ直下)対応、259文字のフォルダ対応(後述)などをチェックする - 難しい例は、例えば
z 𠮷𠮷%PATH% ' '' ` `` $PATH & && %%PATH%%[] ] [ ^ ^^ ' ; ;; 𩸽𩸽!PATH!#$%&'()=~{`+}_,.][;@^- - ‘ ’ ‚ ‛
のような感じ(適当)。こんなファイル名を現実に見ることはないが、エクスプローラー上で普通に入力できる内容である。- 「𠮷」はBMP外かつJIS外の漢字としては最も変換で出しやすいのでテストに重宝する。(𩸽はBMP外だがJIS外ではない(第4水準))
その他
- デフォルトシェルは、通常ユーザーはwtでも管理者の場合はconhostに設定されているっぽい?
C:\
のようなドライブ直下のパスは末尾に\
を付けなければいけないことが多い。たとえばwt -dの引数にする場合など。またcmdでcdコマンドやCD
環境変数を見るときもドライブ直下のときだけは\
が付いて返ってくる。- ただ、wt -d に
C:
が渡されるように見えるのになぜか動いているものもある。未調査。
- ただ、wt -d に
- shやcmdなどの中に一旦入ってしまうと一部の環境変数などが書き換わってしまうかもしれないが、多少は許容することとする。
- コマンド例は網羅的でない可能性があり、色々な書き方を提示しようとあえて統一していないところもある。現実にはもっと簡単なやり方があるかもしれない。あくまで一例ということで。
- CygwinとGit Bashはほぼ同じなのでGit Bashを主に載せてCygwinは適宜差分のみ記述する。
- shは、特にベースのシェルとして使うにはエスケープの方式などでWindowsと相性が悪く使いづらい感じがする。逆にcmdは内部コマンドは地獄だがベースのシェルとしては意外と副作用が少なく、そう悪くはない。
- 以下の例ではCygwinやGit Bashの実行ファイルにはPATHを通していないことを想定している。
C:\cygwin64
あたりは適当に読み替えていただきたい。 - 2021年途中ごろまで、wtなどのストアアプリ系?の実行ファイル(Explorer上で0バイトになってるエイリアス)をGit Bashから実行できない(Permission deniedとなる)バグがあった。 https://github.com/git-for-windows/git/issues/2675
- ネットワークドライブは各ユーザー対象に割り当てられているせいなのか、管理者として実行すると利用できないらしい。割り当てられる前のネットワークパス自体は有効。
- wtを管理者権限で呼び出すときに(確率的に)ウインドウが最前面にならないことがある。chcpの有無で変わったりするなど詳細不明。conhostだとならない気がする。startrunなどを介して実行すると必ず最前面に出るようになるっぽい。
準備: win-console-delegator
cmdやPowerShellやCygwinのbashなどのシェルを使えば、スクリプトを起動して複雑なコマンドを呼び出せたり、パイプ実行が可能になったりと色々便利だが、それだけのためには大仰すぎて、特殊文字の扱いなどがかえって仇になることもある。
そこで、シェル関連の操作に汎用的に使えるコマンドをいくつか作成して公開した。https://github.com/ge9/win-console-tools
- 以前はC++で書いていた(https://github.com/ge9/win-console-delegator)が、C#のほうが文字列の扱いなどが簡明。
ただし、これはあくまで筆者が独自に作ったものであり、Windows標準のもので何とかしようとするのが面白いところでもあるので、それほど積極的には使わない。今後出てくる例はすべて、これらを一切使わなくても(余計な環境変数が設定されてしまうかもしれないという点をのぞけば)同等の機能が実現できる。
runother
与えられた引数を特定の文字列とつなげて実行してくれるプログラム。この実行ファイル自体の名前を適宜変更して使用し、「特定の文字列」は別のファイルから読ませる。使用例はWindowsでのターミナル環境 を参照。
runother-gui
runotherとほぼ同じだが、コンソールアプリケーションではなくWindowsアプリケーション(黒いウインドウが出ない、cmd上で実行したときに終了待ちが行われないなど)。
evalrun
与えられたコマンドラインを実行し、その出力をそのままコマンドラインとして実行するコンソールアプリケーション。(テキストファイルからのコマンドラインの読み込みなどに使える)
startrun
与えられたコマンドラインをWindowsのデフォルトシェルで起動するだけのWindowsアプリケーション。
waitrun
与えられたコマンドラインを実行したあとキー入力を待ってから終了するコンソールアプリケーション。一瞬でウインドウが消えてしまうときのデバッグに使いやすい。
hiderun
コンソールウインドウ非表示の状態で与えられたコマンドを実行するWindowsアプリケーション。(GUIアプリケーションなどを与えた場合は非表示にならないかも)
vbsのvbHideでも非表示にできるが、コンソールウインドウの表示位置をみると非表示のウインドウが一つ挟まっていることが分かるのに対して、こちらの場合はそうならない。
piperun
与えられたコマンドラインを最初の
|
とそれ以降の2つに区切ってそれぞれコマンドとしてパイプでつなげて実行するコンソールアプリケーション。ただし最初のコマンドのほうに|
自体を入力したいときは||
でエスケープする。また2つ目のコマンドの先頭の空白は除去される。piperunex
piperunと似ているが、2つだけでなく3つ以上の任意個のパイプ実行を一度に行う。一見便利だが、コマンドの後ろ側に未知の文字列が渡される場合、
|
をエスケープする必要が生じる。piperun piperunex command1 || command2 || ... || commandN | unknown_commandline
のように外側をpiperun
で囲うと安全。adminrun
与えられたコマンドラインを管理者として実行するWindowsアプリケーション。
ShellExecuteEx
では実行ファイルパスと引数は別々に指定しなければいけないので最低限の引数の解析を行っている。PowerShell経由で管理者権限で実行するときのような面倒なエスケープが全て不要になるため非常に使いやすいが、この記事ではこれに依存しないようにする。- ちなみにこういうのもある。mattn/sudo: sudo for windows これは、通常のUACのように別ウインドウで実行するのではなく、localhostのランダムなポートを使い、自分自身にポート番号を渡しつつ管理者権限で呼び出すことで標準入出力を転送し、元の端末でそれを読み書きできるという点で、linuxのsudoに近い。しかし標準入出力を介しているので、
cmd /c for /?
やcmd /c pause
の挙動が変わってしまうという問題がある。また、呼び出し元でchcp 65001をしていると文字化けする。また、コマンドライン引数を一旦配列に分割してから組み直しているので情報が一部落ちている。あと、セキュリティ的にも懸念があるかもしれないが、これはそもそもLinuxのsudo自体がまずどうなの?という気持ちになった。(未解決)(参照: https://twitter.com/e9g/status/1687385469921931264?s=20)
- ちなみにこういうのもある。mattn/sudo: sudo for windows これは、通常のUACのように別ウインドウで実行するのではなく、localhostのランダムなポートを使い、自分自身にポート番号を渡しつつ管理者権限で呼び出すことで標準入出力を転送し、元の端末でそれを読み書きできるという点で、linuxのsudoに近い。しかし標準入出力を介しているので、
uacrun
startrunと同じだが、コンパイル時のマニフェスト設定でこのプログラム自体の実行に管理者権限を要求するようにしたので、adminrunと同じように使える(管理者権限で実行する対象が渡されたコマンドではなくuacrun自体になるという違いはある)
pecho
与えられたコマンドライン引数をそのままコンソールに出力する。→WindowsでUnicode文字(特にU+10000以上)を正しく表示(&ファイルに書き込み)するには、「出力先がコンソールならWriteConsoleW、そうでなければWriteFileを使用する」といった非常に面倒な実装が必要であり(参照: https://twitter.com/mattn_jp/status/542581083242364928 など)、C++やC#では扱いづらい。そこでrustを用いて実装した。→pure-echo-win
printcd
カレントディレクトリをコンソールに出力する。(ちなみにpwdの由来はprint working directoryなのでそれに倣った)
注意: 当初は後述のNoWorkingDirectoryを知らなかったので1.の方法に頼っていたが、実際には2.の方法のほうが簡明なので、そちらを使うことを勧める。1.は残しておくが、読む必要はあまりない。
1.標準入力から受け取る場合
まず「標準入力からディレクトリのフルパスを受け取り、そのディレクトリで各種ターミナルを起動するコマンド」を紹介する。
そのようなコマンドを仮にopenterm
とすると、ExecuteCommand-Pipeを使用して、レジストリ(該当CLSIDのLocalServer32の既定の値)に以下のように書くことで、CLSIDが右クリックメニューのDelegateExecute値に使えるようになる。
C:\path\to\ExecuteCommand4000.exe h openterm
h
はコンソールウインドウを非表示にするExecuteCommand-Pipeのオプションである。
また、このopenterm
の部分は非常に長くなることがあり、その場合レジストリに書いて直接編集するのは手間がかかる(操作もしづらいし、更新を即座に反映させるために「LocalServer32
」キーの名前などを変えて戻す必要もあって面倒)。そこで、runotherを使ってtxtにopenterm
の内容をそのまま書くことで、openterm
の部分にそのexeの名前だけを書けばよくなる。
また、いくつかはCygwin/Git Bash向けに冒頭でcygpathによる変換を入れているが、これを取り除けば、Cygwin/Git Bash用のパスを受け取って動作するコマンドということになる。
では以下で、openterm
部分についてそれぞれ紹介する。
- 直接コマンド記入でも
NoWorkingDirectory
を指定すれば\\?\
に対応できるというのをこのセクションの大部分を書き終えてから知ったので、このセクションは内容の多さの割には実際の必要性はそこまで大きくないかもしれない。
Git Bash
git-bash.exe
で開く
この場合shの感覚でgit-bash.exe
に引数を渡せば勝手にminttyのウインドウで開いてくれるので最も楽である。
cmd /c ""C:\Program Files\Git\usr\bin\cygpath" -f - | "C:\Program Files\Git\usr\bin\sh.exe" -c "IFS=;/bin/env LANG=en_US.utf8 /bin/printf %q $(/bin/cat)" | "C:\Program Files\Git\usr\bin\xargs" -d '\n' -I {} "C:\Program Files\Git\git-bash.exe" -c "cd {};exec bash""
- Git BashのcygpathでGit Bash用のパスに変換
- printfでエスケープ、utf8を設定
- git-bash.exeに渡す
デフォルトのターミナルで開く: ①startrunを使う
cmd /c ""C:\Program Files\Git\usr\bin\cygpath" -f - | "C:\Program Files\Git\usr\bin\sh.exe" -c "IFS=;/bin/env LANG=en_US.UTF8 /bin/printf %q $(/bin/cat)" | "C:\Program Files\Git\usr\bin\xargs.exe" -d '\n' -I {} startrun "C:\Program Files\Git\usr\bin\env.exe" MSYSTEM=MINGW64 "C:\Program Files\Git\usr\bin\bash.exe" --login -i -c 'cd {};exec bash'"
git-bash.exe
やC:\Program Files\Git\bin
のbashではなく/usr/binのbash.exeを使う。以後同じ。
デフォルトのターミナルで開く: ②cmdのstartを使う
cmd /c ""C:\Program Files\Git\usr\bin\cygpath" -f - | "C:\Program Files\Git\usr\bin\sh.exe" -c "IFS=;/bin/env LANG=en_US.UTF8 /bin/printf %q $(/bin/sed 's/\\^/^^/g;s/%/=%/g;s/&/^&/g')" | "C:\Program Files\Git\usr\bin\xargs.exe" -d '\n' -I {} cmd //c start "" "C:\Program Files\Git\usr\bin\env.exe" MSYSTEM=MINGW64 "C:\Program Files\Git\usr\bin\bash.exe" --login -i -c 'cd "$(echo {} ^| sed s/=%/%/g)";exec bash'"
- この場合、cmdによる環境変数への置換を避ける必要があるほか、キャレットによるエスケープにも対応する必要がある。
- Git Bashからcmdを実行しているので
//c
のところのスラッシュは2つ。
conhostで開く
cmd /c ""C:\Program Files\Git\usr\bin\cygpath" -f - | "C:\Program Files\Git\usr\bin\sh.exe" -c "IFS=;/bin/env LANG=en_US.UTF8 /bin/printf %q $(/bin/sed 's/%/=%/g')" | "C:\Program Files\Git\usr\bin\xargs.exe" -d '\n' -I {} conhost "C:\Program Files\Git\usr\bin\env.exe" MSYSTEM=MINGW64 "C:\Program Files\Git\usr\bin\bash.exe" --login -i -c 'cd "$(echo {}|sed s/=%/%/g)";exec bash'"
conhostによる環境変数の置換を抑制。
Windows Terminalで開く
cmd /c ""C:\Program Files\Git\usr\bin\cygpath" -f - | "C:\Program Files\Git\usr\bin\sh.exe" -c "IFS=;LANG=en_US.UTF8 printf %q $(/bin/sed 's/%/=%/g')" | "C:\Program Files\Git\usr\bin\sed.exe" "s/;/\\\\;/g" | "C:\Program Files\Git\usr\bin\xargs.exe" -d '\n' -I {} wt "C:\Program Files\Git\usr\bin\env.exe" MSYSTEM=MINGW64 "C:\Program Files\Git\usr\bin\bash.exe" --login -i -c "IFS=\\;cd $(echo {}|sed s/=%/%/g) \\;exec bash""
上記に加えてセミコロンへの対応が必要なのと、こっちは/bin/printfじゃなくて組み込みのprintfでこうしないとだめだった。
Cygwin
付属のminttyを使うものだけはGit Bashと割と差があるので載せる。
cmd /c ""C:\cygwin64\bin\cygpath" -f - | "C:\cygwin64\bin\sh.exe" -c "IFS=;/bin/printf %q $(/bin/cat)" | "C:\Program Files\Git\usr\bin\xargs" -d '\n' -I {} "C:\cygwin64\bin\mintty.exe" -e "C:\cygwin64\bin\bash.exe" --login -i -c "cd {};exec bash""
PowerShell
デフォルトシェル, startrun
- パスのエスケープがおかしい気がするが、なぜか動いている
cmd /c ""C:\Program Files\Git\usr\bin\sed.exe" "s/'/''/g" | "C:\Program Files\Git\usr\bin\xargs.exe" -d '\n' -I {} startrun powershell -noexit -command Set-Location -literalPath "'{}'""
デフォルトシェル, cmdのstart
cmd /c ""C:\Program Files\Git\usr\bin\sed.exe" "s/'/''/g;s/%/=%/g" | "C:\Program Files\Git\usr\bin\xargs.exe" -d '\n' -I {} cmd //c start "" powershell -noexit -command Set-Location -literalPath '\'{}\'.Replace(\'=^%\',\'%\')'"
cmd
Windows Terminal
cmd /c chcp 65001 & powershell -Command "$mypath=($input | ForEach-Object { return $_ });$bak=$env:MY_PERCENT; $env:MY_PERCENT = '%';if ($mypath.Length -gt 258) { $mypath = cmd /c \"for %A in (`\"$($mypath.Replace('%', '%MY_PERCENT%'))`\") do @echo %~sA\" };$env:MY_PERCENT = $bak;wt -d ($mypath -replace ';', '\;') cmd"
Windowsのパス長さ制限に関して でも書いた通り、Windows側はちょうど259文字の長さのフォルダだけは(その必要があるにもかかわらず)8.3形式を使った短いパスにして渡してくれない。そこで、パス長さが258を超えていればcmdに渡して8.3形式の名前に変換する処理をPowerShell側で行う。この際はエスケープされていない正確なパス名をcmdに渡さなければならないので
MY_PERCENT
の値を一時的に%
に設定する方法をとっている。- 念のため再確認しておくが、この
'%MY_PERCENT%'
の部分は'%'.Trim('=')+'MY_PERCENT%')
などと変えておかないと、MY_PERCENT
が定義されていた場合に正しく動作しない。 - cmdではちょうど258文字のフォルダでもdirが失敗するなど挙動が不自然であるため、もう少し保守的に257を超えていれば短くするという仕様でもいいかもしれない。
- 念のため再確認しておくが、この
(追記)8.3形式のパスを取得するのは普通にPowerShell内でもできるらしい…。以下のようになる。
if ($mypath.Length -gt 258) { $mypath = (New-Object -ComObject Scripting.FileSystemObject).GetFolder($mypath).ShortPath}
259文字のフォルダを無視するなら以下のように簡潔に済む。
cmd /c chcp 65001 & powershell -Command wt '-d' $input.Replace(';','\;') cmd
デフォルトシェル
これは案外難しい。なぜなら、cmd内でcdさせるのは%のエスケープの関係で難しく(cmd内で=を含む文字列を置換することがどうやってもできなさそう(外部プログラムを呼び出すのも難しそうだった)なので、未定義の変数を探索するやり方しかない)、Start-Processの-WorkingDirectoryやCygwinのshは8.3形式に対応していないからである。要は「与えられたディレクトリ(8.3形式のパスが含まれるかもしれない)をカレントディレクトリとして与えられたコマンドを実行する」だけやってくれるプログラムがあればよく、これ自体はそう難しくないことのはずだが、現状、Windows標準環境でこれができるのは自前でプログラムをコンパイルする以外だと(pwshを入れていいならpwshと)vbsしかないようである。結局、この部分をついでにやってくれるWindows Terminalのほうが楽ということになる。
たとえば以下のようなvbsを用意する。
Dim objShell
Set objShell = CreateObject("WScript.Shell")
objShell.CurrentDirectory = WScript.Arguments(1)
objShell.Run(WScript.Arguments(0)),,False
すると以下のように書ける。
cmd /c chcp 65001 & powershell -Command "$mypath=($input | ForEach-Object { return $_ });$bak=$env:MY_PERCENT;$env:MY_PERCENT = '%';if ($mypath.Length -gt 258) { $mypath = cmd /c \"for %A in (`\"$($mypath.Replace('%', '%MY_PERCENT%'))`\") do @echo %~sA\" };$env:MY_PERCENT = $bak; wscript 'C:\path\to\startatdir.vbs' cmd ('\"'+$mypath+'\"')"
なおstartatdir.vbs
のパス指定はフルパスが必要そう(ただしここはcmd /cの中なので%USERPROFILE%
とかを使って書いてもよい)
1.1.標準入力から受け取る場合 - 管理者権限あり
上記の続きで、こちらは管理者権限ありのもの。
Git Bash
デフォルトシェル
cmd /c ""C:\Program Files\Git\usr\bin\cygpath" -f - | "C:\Program Files\Git\usr\bin\sh.exe" -c "IFS=;/bin/env LANG=en_US.UTF8 /bin/printf %q $(/bin/cat)" | "C:\Program Files\Git\usr\bin\sed.exe" "s/'/''/g" | "C:\Program Files\Git\usr\bin\xargs.exe" -d '\n' -I {} powershell -Command Start-Process -Verb Runas -Filepath '"C:\Program Files\Git\usr\bin\env.exe"' -ArgumentList '"MSYSTEM=MINGW64"', '"`"C:\Program Files\Git\usr\bin\bash.exe`""' , '--login', '-i' , '-c', "'\"cd {}; exec bash\"'" "
- Start-Process自体がstartみたいなものなので、引数にコンソールアプリケーションを指定したらデフォルトシェルで開かれる。
- 最後のところ、「
"'\"cd {}; exec bash\"'"
」のかわりに「'\'"cd {};exec bash"\''
」 とか「'\'\"cd {};exec bash"\''
」 でも動くのは謎。シングルクォーテーションのエスケープ規則がわからない。参考→https://twitter.com/e9g/status/1678283164689760256
Windows Terminal
cmd /c ""C:\Program Files\Git\usr\bin\cygpath" -f - | "C:\Program Files\Git\usr\bin\sh.exe" -c "IFS=;LANG=en_US.UTF-8 printf %q $(/bin/cat)" | "C:\Program Files\Git\usr\bin\sed.exe" "s/'/''/g;s/%/==%/g;s/;/\\\\;/g" | "C:\Program Files\Git\usr\bin\xargs.exe" -d '\n' -I {} powershell -Command Start-Process -Verb Runas -Filepath wt -ArgumentList "'"'"C:\Program Files\Git\usr\bin\env.exe"'"'" , '"MSYSTEM=MINGW64"' , "'"'"C:\Program Files\Git\usr\bin\sh.exe"'"'" , "'--login'", "'-c'", "'"'"IFS=\\;cd $(echo {}| /bin/sed s/==%/%/g) \\; exec bash"'"' " "
adminrunを使う
piperunex "C:\Program Files\Git\usr\bin\cygpath" -f - | "C:\Program Files\Git\usr\bin\sh.exe" -c "IFS=; /bin/env LANG=en_US.UTF8 /bin/printf %q $(/bin/cat)" | "C:\Program Files\Git\usr\bin\sed.exe" "s/%/==%/g;s/;/\\\\;/g" | "C:\Program Files\Git\usr\bin\xargs.exe" -d '\n' -I {} adminrun wt "C:\Program Files\Git\usr\bin\env.exe" "MSYSTEM=MINGW64" "C:\Program Files\Git\usr\bin\sh.exe" --login -c "IFS=\\;cd $(echo {}|| /bin/sed s/==%/%/g) \\; exec bash"
PowerShell
Windows Terminal
cmd /c ""C:\Program Files\Git\usr\bin\sed.exe" "s/'/''''/g;s/%/==%/g;s/;/\\\\;/g" | "C:\Program Files\Git\usr\bin\xargs.exe" -d '\n' -I {} powershell -Command Start-Process -Verb Runas -Filepath wt -ArgumentList 'powershell', '-noexit', '-command', 'Set-Location', '-LiteralPath', "'''\"{}\"''.Replace(''==%'',''%'')'" "
- Cygwin系なしで
cmd /c "chcp 65001 & powershell -Command Start-Process -Verb Runas -Filepath wt -ArgumentList 'powershell', '-noexit', '-command', 'set-location', '-literalpath', ('\"'''+($input -replace '''', '''''' -replace '%', '==%' -replace ';', '\;' )+'''.Replace(''==%'',''%'')\"') "
cmd
デフォルトシェル
再び、startatdir.vbsを使う。
cmd /c chcp 65001 > nul & powershell -Command "$mypath=($input | ForEach-Object { return $_ });$bak=$env:MY_PERCENT; $env:MY_PERCENT = '%';if ($mypath.Length -gt 258) { $mypath = cmd /c \"for %A in (`\"$($mypath.Replace('%', '%MY_PERCENT%'))`\") do @echo %~sA\" };$env:MY_PERCENT = $bak; Start-Process -Verb Runas -Filepath wscript -Argumentlist '//Nologo', 'C:\path\to\startatdir.vbs', 'cmd', ('\"'+$mypath+'\"')"
Windows Terminal
cmd /c chcp 65001 & powershell -Command "$mypath=($input | ForEach-Object { return $_ }); $bak=$env:MY_PERCENT; $env:MY_PERCENT = '%';if ($mypath.Length -gt 258) { $mypath = cmd /c \"for %A in (`\"$($mypath.Replace('%', '%MY_PERCENT%'))`\") do @echo %~sA\" };$env:MY_PERCENT = $bak;Start-Process -Verb Runas -Filepath wt -Argumentlist '-d', ('\"'+($mypath -replace '\\$','\\\\' -replace ';', '\;')+'\"'), cmd"
この場合はC:\
のための対処(-replace '\\$','\\\\'
)が必要である。259文字のフォルダを無視するなら以下の通り。
cmd /c "chcp 65001 & powershell -Command Start-Process -Verb Runas -Filepath wt -ArgumentList '-d', ('\"'+($input -replace '\\$','\\\\').Replace(';','\;')+'\"'), cmd"
2.直接コマンド記入の右クリックメニュー
レジストリのcommand
キーの既定の値に直接コマンドを記入する方式である。
デフォルトでは、右クリックメニューを押した時点でのフォルダ(フォルダを右クリックするメニューからだと対象フォルダの親、フォルダ背景を右クリックするメニューなら対象フォルダ)をカレントディレクトリとしてコマンドが起動される。そのため\\?\
を付けなければならないような長大なパスのフォルダでは動作しない。しかし、shell\xxxx
キーにNoWorkingDirectory
値を設定することで、カレントディレクトリが必ずC:\Windows\System32
に設定されるようになるので動くようになる。
対象のディレクトリはどう取得するかというと、レジストリに書き込んだ値のうち%V
という部分が対象ディレクトリに書き換えられてコマンドが実行される。この%V
もC:などの場合はC:\
になる。コマンドの中でそのまま%という文字を使いたい場合はバッチファイルと同様に%%とエスケープする必要があるようである(これはどこにも書いてない?)。
しかし、こうして渡される(特殊文字を含むかもしれない)ディレクトリ名を完璧に取得するのは意外と難しい。cmdに解釈させると%
VAR
%
が置換されてしまうし、シングルクォートとバッククォートが両方含まれているのでpowershellやshに渡すのも簡単ではない。
コツは、powershell -commandやsh -cの中に直接書くのではなく、スクリプトへのコマンドライン引数として渡した上で、スクリプト内で引数として取得することである。それぞれ、基本形(そのまま出力するだけ)は以下のようになる。
powershell
powershell -Command "& {echo $args[1].Trim('\"')}" --%% "\"%V\""
--%
というトークンを入れることでその後のパラメータの解析を行わないようにできる。args[0]は--%
になるのでargs[1]を使う。レジストリに書く際は%
をエスケープして--%%
。--
でも同様のことができる?powershell -Command "& {echo $args[0].Trim('\"')}" -- "\"%V\""
(args[0]になっていることに注意)- というか、
--%
で書き進めてしまったが、本当は--
のほうが安定するかもしれない。
あるいは
$MyInvocation.Line
を使う方法もある。この場合、powershellへの引数が全て(この例なら-noexit -Command "& {$spl = ...
のところから)取得されるので、目当ての部分を取り出すためにここでは最後から2番目の"
と最後の"
の間を取得するという風にしている。powershell -noexit -Command "& {$spl = $MyInvocation.Line -split'\"' ; Set-Location -LiteralPath $spl[$spl.Length-2]}" --%% "\"%V\""
昔のPowerShell(2くらい)で試してみたらこちらでないと
`
や&
あたりの処理がうまくいかない場合があった。argsを使うものからの書き換えは容易である。sh
sh -c "IFS=;echo $*" -- "%V
- 最後が
"%V
と閉じられていないのはミスではなく、"%V"
としてしまうとC:\
のときにC:"
が渡されてしまうのでそれを避けるためである(これはかなりトリッキーなのでもう少し真面目にやってもいいとは思う)。 - UNCパスの最初の
\\
が\
に変わってしまう(手元では、Cygwinの場合は\\?\
のみで発生している?)という問題があり、適宜置換する必要がある。
- 最後が
これらの書き方はいわゆる「ワンライナー」的なものであるが、もちろん外部にスクリプトファイルを用意してもよい。
また、外部にスクリプトを用意してよいという条件であれば、vbsも使用可能である。
この場合、まずレジストリには
wscript "C:\path\to\script.vbs" "%V"
などと書く。スクリプトの内容は上記のレジストリの内容をほぼそのままWscript.ShellでRunすればよいが、
%V
のかわりにWscript.Arguments(0)を挿入するのと、%
を%%
にエスケープしなくてよいのと、VBSなので文字列内の"
は""
に変えなければならない。これに加え、Runの引数に含まれる%
VAR
%
が置換の対象となるため、対象フォルダの文字列については==%
への置換でエスケープして、呼び出し先のpowershellなどで元に戻す。必要に応じてRunのオプションでvbHideを指定する。
というか別に258文字を超えていたら云々みたいな処理も全部vbs側でやってしまってもよい(面倒なのでそういう例は載せていない)
バッチファイルに渡すのは、バッチファイルをcmdが呼び出す時点で%PATH%
のようなファイル名の環境変数は既に展開されてしまうので、どうやっても不可能。
他には、(hiderunと)piperunとpechoを併用するという方法もあり、それらの実行ファイルを用意する必要があるという以外は綺麗にできる方法である。
いずれにしろ、一旦取得できてしまえばあとは前述のopenterm
に流し込むだけでよい。ただ、実際にはわざわざopenterm
に流し込まなくても(パイプを使わなくても)もっと簡単に起動できることもあるのでそちらを中心に紹介する。
- commandの最初で指定する(メインの)実行ファイル(先ほどなら
powershell
やsh
)をファイル名単体で書く場合は、それが(メニューをHKCUで設定するならHKCUの(最近のWindowsならHKLMのでも可?)、HKLMで設定するならHKLMの)App Pathsキーに登録されていなければならない(PATHは通っていなくてもいい)。登録されていない場合はフルパスで指定する必要がある。
設定例
管理者権限なし
PowerShell
デフォルトターミナルで
powershell -noexit -Command "& {Set-Location -LiteralPath $args[1].Trim('\"')}" --%% "\"%V\""
参考までに、Windowsにもともと入っているメニューでは以下のようになっている。
powershell.exe -noexit -command Set-Location -literalPath '%V'
これだとシングルクォートと連続スペースに対応できない。連続スペースだけなら
'%V'
を"'%V'"
に変えると解決できる。%$[]`
あたりは見た感じ問題なし。この問題はhttps://github.com/PowerShell/PowerShell/issues/6598やhttps://github.com/PowerShell/PowerShell/pull/6660 で扱われており、結局pwshでは-WorkingDirectoryというパラメータが追加された。https://github.com/PowerShell/PowerShell/issues/14091にある以下の文字列をレジストリに書いたところ、正しく動作したので、pwshではこれでよい。
pwsh.exe -NoExit -RemoveWorkingDirectoryTrailingCharacter -WorkingDirectory "%V!" -Command "$host.UI.RawUI.WindowTitle = 'PowerShell 7 (x64)'"
wtで
hiderun powershell -Command "& {wt powershell -noexit -Command Set-Location -LiteralPath ('\"'''+($args[1] -replace '''', '''''' -replace '%%', '==%%' -replace ';', '\;' )+'''.Replace(''==%%'',''%%'')\"')}" --%% "\"%V\""
cmd
wtで。標準入力のときとほぼ同じ。
powershell -Command "& {$bak=$env:MY_PERCENT;$mypath=$args[1].Trim('\"'); $env:MY_PERCENT = '%%';if ($mypath.Length -gt 258) { $mypath = cmd /c \"for %%A in (`\"$($mypath.Replace('%%', '%%MY_PERCENT%%'))`\") do @echo %%~sA\" };$env:MY_PERCENT = $bak;wt -d ($mypath -replace ';', '\;') cmd}" --%% "\"%V\""
デフォルトターミナルで
startatdir.vbsを使う。
powershell -Command "& {$bak=$env:MY_PERCENT;$mypath=$args[1].Trim('\"'); $env:MY_PERCENT = '%%';if ($mypath.Length -gt 258) { $mypath = cmd /c \"for %%A in (`\"$($mypath.Replace('%%', '%%MY_PERCENT%%'))`\") do @echo %%~sA\" };$env:MY_PERCENT = $bak; wscript 'C:\path\to\startatdir.vbs' cmd ('\"'+$mypath+'\"')}" --%% "\"%V\""
Windowsにもともと入っているメニューでは以下。
cmd.exe /s /k pushd "%V"
これは(
\\?\
が付くパスを除いて)ほとんどの場合に正しく機能するが、%PATH%
のような文字列が含まれているフォルダではうまくいかない。ちなみにpushdによりUNCパスには自動的にネットワークドライブが割り当てられる。
Git Bash
mintty(git-bash.exe使用)
"C:\Program Files\Git\git-bash.exe" -c "IFS=;cd $(echo $*|/bin/sed 's/\\\\/\\\\\\\\/'|/bin/cygpath -f -);exec bash" -- "%V
Git Bashのインストーラにより設定される「Git Bash Here」メニューでは以下のようになっている。
"C:\Program Files\Git\git-bash.exe" "--cd=%v."
これは、特殊文字にはすべて対応しているが、(NoWorkingDirectoryをつけたとしても)
\\?\
で始まるパスではうまくいかない。最後の「.」はよくわからないがこれを付けておくと--cd
がうまくやってくれるっぽい。
デフォルトシェルで
"C:\Program Files\Git\usr\bin\sh.exe" -c "IFS=;cd $(echo $*|/bin/sed 's/\\\\/\\\\\\\\/'|/bin/cygpath -f -);export MSYSTEM=MINGW64;exec /bin/bash --login" -- "%V
wtで
hiderun "C:\Program Files\Git\usr\bin\sh.exe" -c "IFS=;LANG=en_US.UTF8; echo $*|/bin/sed 's/\\\\/\\\\\\\\/'|/bin/cygpath -f -|printf %%q $(/bin/sed 's/%%/=%%/g')|/bin/sed 's/;/\\\\;/g'|/bin/xargs -d '\n' -I {} -- wt \"C:\Program Files\Git\usr\bin\env.exe\" MSYSTEM=MINGW64 \"C:\Program Files\Git\usr\bin\bash.exe\" --login -i -c 'IFS=\\;cd $(echo {}|sed s/=%%/%%/g)\\;exec bash'" -- "%V
Cygwin
デフォルトシェルで
"C:\cygwin64\bin\sh.exe" --login -c "IFS=;cd $(echo $*|/bin/sed 's/\\\\?/\\\\\\\\?/'|/bin/cygpath -f -);exec bash" -- "%V
- 置換の対象を
\\?\
のみとするためsedの引数に?
が増えている
- 置換の対象を
wtで
hiderun "C:\cygwin64\bin\sh.exe" -c "IFS=;echo $*|/bin/sed 's/\\\\?/\\\\\\\\?/'|/bin/cygpath -f -|printf %%q $(/bin/sed 's/%%/=%%/g')|/bin/sed 's/;/\\\\;/g'|/bin/xargs -d '\n' -I {} -- wt \"C:\cygwin64\bin\bash.exe\" --login -i -c 'IFS=\\;cd $(echo {}|sed s/=%%/%%/g)\\;exec bash'" -- "%V
管理者権限
PowerShell
powershell -Command "& {Start-Process -Verb Runas -Filepath wt -Argumentlist powershell, -noexit, -command, Set-Location, -LiteralPath, ('\"'''+($args[1].Trim('\"') -replace '''', '''''' -replace ';', '\;' -replace '%%', '==%%')+'''.Replace(''==%%'',''%%'')\"')}" --%% "\"%V\""
環境変数の置換を抑止する必要がある。デフォルトシェルで起動するならこれは不要(で、wtでないので
;
のエスケープも不要)で、以下のようになる。powershell -Command "& {Start-Process -Verb Runas -Filepath powershell -Argumentlist '-noexit', '-command', Set-Location, '-LiteralPath', ('\"'''+($args[1].Trim('\"') -replace '''', '''''' )+'''\"')}" --%% "\"%V\""
MyInvocationを使う例
powershell -Command "& {$spl = $MyInvocation.Line -split'\"'; Start-Process -Verb Runas -Filepath powershell -Argumentlist '-noexit', '-command', Set-Location, '-LiteralPath', ('\"'''+($spl[$spl.Length-2] -replace '''', '''''' )+'''\"')}" --%% "\"%V\""
vbsの例
次の節で解説する「(自分が起動された)カレントディレクトリでの起動」にも対応している(引数がない場合)。
Dim objShell Dim curDir Set ws = CreateObject("Wscript.Shell") Dim dir If WScript.Arguments.Count = 0 Then dir = ws.CurrentDirectory Else dir = Wscript.Arguments(0) End If dir = Replace(dir,"%","==%") ws.run "powershell -Command ""& {Start-Process -Verb Runas -Filepath wt -Argumentlist powershell, -noexit, -command, Set-Location, -LiteralPath, ('\""'''+($args[1].Trim('\""') -replace '''', '''''' -replace ';', '\;' )+'''.Replace(''==%'',''%'')\""')}"" --% ""\"""&dir&"\""""", vbHide Set objShell = Nothing
cmd
powershell -Command "& {$bak=$env:MY_PERCENT;$mypath=$args[1].Trim('\"'); $env:MY_PERCENT = '%%';if ($mypath.Length -gt 258) { $mypath = cmd /c \"for %%A in (`\"$($mypath.Replace('%%', '%%MY_PERCENT%%'))`\") do @echo %%~sA\" };$env:MY_PERCENT = $bak;Start-Process -Verb Runas -Filepath wt -Argumentlist '-d', ('\"'+($mypath -replace '\\$','\\\\' -replace ';', '\;')+'\"'), cmd}" --%% "\"%V\""
この場合も
C:\
のための対処(-replace '\\$','\\\\'
)が必要である。デフォルトシェルなら、startatdir.vbsを使う。powershell -Command "& {$bak=$env:MY_PERCENT;$mypath=$args[1].Trim('\"'); $env:MY_PERCENT = '%%';if ($mypath.Length -gt 258) { $mypath = cmd /c \"for %%A in (`\"$($mypath.Replace('%%', '%%MY_PERCENT%%'))`\") do @echo %%~sA\" };$env:MY_PERCENT = $bak;Start-Process -Verb Runas -Filepath wscript -Argumentlist '//Nologo', 'C:\path\to\startatdir.vbs', 'cmd', ('\"'+$mypath+'\"')}" --%% "\"%V\""
Git Bash
理論上はワンライナーでも書けるが、エスケープが面倒すぎるのでファイルに書いたほうがよいだろう。まずレジストリの中身は以下のようにする。
"C:\Program Files\Git\usr\bin\sh.exe" "C:\path\to\gb-wt-admin.sh" "%V"
で、
gb-wt-admin.sh
の中身は以下。#!/bin/sh IFS= LANG=en_US.UTF-8 echo $*|/bin/sed 's/\\/\\\\/'|/bin/cygpath -f -|printf %q $(/bin/cat) |/bin/sed "s/'/''/g;s/%/==%/g;s/;/\\\\;/g"|/bin/xargs -d '\n' -I {} powershell -Command Start-Process -Filepath wt -ArgumentList "'"'"C:\Program Files\Git\usr\bin\env.exe"'"'" , '"MSYSTEM=MINGW64"' , "'"'"C:\Program Files\Git\usr\bin\sh.exe"'"'" , "'--login'", "'-c'", "'"'"IFS=\;cd $(echo {}| /bin/sed s/==%/%/g) \; exec bash"'"'"
Cygwinも同様である。
VS Codeのメニュー(おまけ)
「Code で開く」メニューのcommand値は以下のように設定されている。
"C:\Program Files\Microsoft VS Code\Code.exe" "%V"
これは特殊文字などに関しても問題なく動作するうえ、実はそのまま\\?\
が付いたパスにも適用可能である。従って、VSCode
キーにNoWorkingDirectory
値を設定する(設定の書き換えまたはHKCUによるオーバーライド)だけで、長大フォルダでもVS Codeが使えるようになる。ターミナルの起動には失敗してしまうがファイル・フォルダの作成・編集などの基本操作がGUIでできるので意外と便利(もちろん、他のBetter Explorer的なツールを使う手もあるだろう)。
3. エクスプローラーのアドレスバーから
このときは、末尾が空白のパスや\\?\
が必要な長大パスやちょうど259文字のフォルダはそもそも対応していないので考えなくてよい。しかし通常のネットワークファイル向けのUNCパス(\\192.168.1.1\disk
など)に対応する必要はあるので、cmdをベースにすることはできない(cmdの中に入った時点でカレントディレクトリが変わってしまう)。また``
が含まれるパスをカレントディレクトリにできないことから、powershell.exeもベースとしては使えない。
なので、パイプを使って先ほどのopenterm
にむけて流し込もうと思ったら選択肢はpwsh.exeかpiperunかCygwin系だけである。それぞれの書き方は以下のようになる。
pwsh
pwsh -Command $pwd.Path.Replace('Microsoft.PowerShell.Core\FileSystem::\\', '\\') | openterm
piperun
piperun pwsh -Command $pwd.Path.Replace('Microsoft.PowerShell.Core\FileSystem::\\', '\\') | openterm
上とほぼ変わっていないが、pwshはディレクトリの取得だけで使っているので
openterm
のところで環境変数が汚染されるのを防げる。Cygwin/Git Bash
"C:\Program Files\Git\usr\bin\sh.exe" -c "cmd //c \"chcp 65001 > nul\"| pwd | /bin/cygpath -w -f - | openterm"
- 一つ問題があり、hiderunを使う(コンソールを非表示にする)とchcpが効かないようで、
openterm
でPowerShellの$input
を使っているとUnicode文字が化けてしまう。 - 先ほどの
openterm
部分で先頭にcygpathが入っているものを使うときは、そちらのcygpathとこちらでのcygpath -wは打ち消し合って無駄なので消してよい。
- 一つ問題があり、hiderunを使う(コンソールを非表示にする)とchcpが効かないようで、
このほかにvbs(wscript)もカレントディレクトリの取得のところは問題なくやってくれるので、それをさっきのWscript.Arguments(0)のかわりに使えばよい。
最初に表示されるコンソールを非表示にしたければ、hiderun
を付けてrunother経由で実行するのが手軽だが、前述の通りvbsも拡張子無しで呼び出せるように設定できるので、そうした上でvbHideで隠してもよい。
管理者権限なし
cmd
これは普通にcmdと打てばよい。
(デフォルトシェルがconhostだとして)wtで開きたいときは、;をエスケープして-dで渡す。以下の通りbatファイルを作ってpathを通す。
chcp 65001 & cd | powershell -Command wt -d $input.Replace(';','\;') cmd
wtではなくconhostの場合はconhost cmdだけでよい。batにするならstart "" conhost cmd
でよい。
コンソール非表示ならcmd /cをつけてhiderunする。runotherで、txtの中身は以下の通り。
hiderun cmd /c chcp 65001 & cd | powershell -Command wt -d $input.Replace(';','\;') cmd
powershell, pwsh
pwshはそのままpwshと打てばよい。powershell及びwtやconhostを明示的に指定する場合について以下で述べる。
たとえばwtでpwshなら、runotherを使って、txtの内容は以下。
hiderun pwsh -Command wt -d $pwd.Path.Replace('Microsoft.PowerShell.Core\FileSystem::\\', '\\').Replace(';', '\;') pwsh
powershellの場合は``
に対応するため起動後に移動する。runotherでpiperunを使って、txtは以下。最終的に起動する方のpowershellで==%
を%
に戻している。
hiderun piperunex cmd /c chcp 65001 | printcd | powershell -Command wt powershell -noexit -command Set-Location -LiteralPath ('"'''+($input -replace ';', '\;' -replace '''', '''''' -replace '%', '==%')+'''.Replace(''==%'',''%'')"')
pwshが使えるならprintcdのかわりにpwsh -Command $pwd.Path.Replace('Microsoft.PowerShell.Core\FileSystem::\\', '\\')
でもよい。
以下はpiperunを使わずshでやる例。前述の通りパイプからPowerShell側で$input
を使って受け取ると文字化けするので引数として渡す。
hiderun "C:\Program Files\Git\usr\bin\sh.exe" -c "WD=$(pwd | /bin/cygpath -w -f - | /bin/sed \"s/;/\\\\\\\\;/g; s/'/''/g; s/%/==%/g\"); wt powershell -noexit -command Set-Location -LiteralPath \(\"'$WD'.Replace('==%','%')\"\)"
Git Bash/Cygwin
Git Bashは--login
をつけてもカレントディレクトリを維持するので単純である。以下をrunotherのtxtに書く。
"C:\Program Files\Git\usr\bin\env.exe" MSYSTEM=MINGW64 /bin/bash --login
Cygwinは起動後にcdする。そのかわりexport MSYSTEM=MINGW64
が不要。
"C:\cygwin64\bin\sh.exe" -c "IFS=;/bin/bash --login -i -c 'cd '$(/bin/printf %q `pwd`)'; exec bash'"
printfで一旦エスケープしたものをcdの後につなげてそのまま渡している。
- wtで
Git Bash
wt "C:\Program Files\Git\usr\bin\env.exe" MSYSTEM=MINGW64 "C:\Program Files\Git\usr\bin\bash.exe" --login"
- あれ、wtなのにカレントディレクトリ維持されてる…?wt cmdではダメなのだが…
Cygwin
hiderun "C:\cygwin64\bin\sh.exe" -c "IFS=;WD=$(printf %q `pwd` | /bin/sed \"s/;/\\\\\\\\;/g; s/%/==%/g\"); wt \"C:\cygwin64\bin\bash.exe\" --login -c 'IFS=\\;cd $(echo '$WD'|sed s/==%/%/g)\\; exec bash'"
管理者権限あり
cmd
runotherを使って以下の通り。
hiderun cmd /c "chcp 65001 > nul & cd | powershell -Command Start-Process -Verb Runas -Filepath wt -ArgumentList '-d', ('\"'+$input.TrimEnd('\\').Replace(';','\;')+'\"'), cmd"
TrimEnd('\\')
のところはC:\
の末尾のバックスラッシュへの対応である(wt -d "C:"
は通らないので一見ダメそうだが、なぜかこれで動く。原因不明。)。
batならそのまま以下の通り(黒い画面が一瞬出てしまう)。
chcp 65001 & cd | powershell -Command Start-Process -Verb Runas -Filepath wt -ArgumentList '-d', ('\"'+$input.TrimEnd('\\').Replace(';','\;')+'\"'), cmd
別解としてパイプを使わないものも載せておこう。cmdでは特殊文字ではないので'
を''
に変えるのは簡単である。
hiderun cmd /c powershell -Command "& {Start-Process -Filepath wt -Verb Runas -ArgumentList '-d', ('\"'+$args[0].TrimEnd('\\').Replace(';','\;')+'\"'), cmd}" -- "'%CD:'=''%'"
以下もうまくいく。cmdの"
に関する仕様により、例えばStart-Processの直前にecho '\"';
のような"
を奇数個含む文字列を入れると動かなくなる。argsを使うとバッククォートがうまくいかなかったのでMyInvocationを使ってみる。
hiderun cmd /c powershell -Command "& {$spl=$MyInvocation.Line.Split('\"'); Start-Process -Filepath wt -Verb Runas -ArgumentList '-d', ('\"'+$spl[$spl.Length-2].TrimEnd('\\').Replace(';','\;')+'\"'), cmd}" -- "\"%CD%\""
powershell, pwsh
UNC非対応でよく、かつ(``
をカレントディレクトリとして起動できる)pwshでよければ、上記のcmdをpwshに変えればよい。powershellならSet-Locationで移動が必要。以下をrunotherのtxtに書く。
hiderun cmd /c "chcp 65001 & cd | powershell -Command Start-Process -Verb Runas -Filepath wt -ArgumentList 'powershell', '-noexit', '-command', 'Set-Location', '-LiteralPath', ('\"'''+($input -replace '''', '''''' -replace '%', '==%' -replace ';', '\;' )+'''.Replace(''==%'',''%'')\"')"
UNC対応なら、管理者でないときと同じで、PSReadLineのエラーを回避するためpwshを一貫して使うのがいいだろう。
hiderun pwsh -Command Start-Process -Verb Runas -Filepath wt -ArgumentList '-d', ('\"'+$pwd.Path.Replace('Microsoft.PowerShell.Core\FileSystem::\\', '\\').TrimEnd('\\').Replace(';','\;')+'\"'), pwsh
powershellならpiperunを使う。カレントディレクトリの取得にpwshを使っているがprintcdでもよい。
hiderun piperunex cmd /c chcp 65001 | printcd | powershell -Command Start-Process -Verb Runas -FilePath wt -ArgumentList powershell, -noexit, -command, Set-Location, -LiteralPath, ('\"'''+($input -replace ';', '\;' -replace '''', '''''' -replace '%', '==%')+'''.Replace(''==%'',''%'')\"')
shなら以下。
hiderun "C:\Program Files\Git\usr\bin\sh.exe" -c "WD=$( pwd | /bin/cygpath -w -f - | /bin/sed \"s/;/\\\\\\\\;/g; s/'/''''/g; s/%/==%/g\"); powershell -Command Start-Process -Verb Runas -Filepath wt -Argumentlist powershell, -noexit, -command, Set-Location, -LiteralPath, \(\"'\\\"''$WD''.Replace(''==%'',''%'')\\\"'\"\)"
Git Bash
runotherとpiperunで以下。
hiderun piperunex "C:\Program Files\Git\usr\bin\sh.exe" -c "IFS=; LANG=en_US.UTF8; pwd || printf %q $(/bin/cat)" | "C:\Program Files\Git\usr\bin\sed.exe" "s/'/''/g;s/%/==%/g;s/;/\\\\;/g" | "C:\Program Files\Git\usr\bin\xargs.exe" -d '\n' -I {} -- powershell -Command Start-Process -Verb Runas -Filepath wt -ArgumentList '"C:\Program Files\Git\usr\bin\sh.exe"','-c', "'"'"IFS=\\; export MSYSTEM=MINGW64\\;cd $(echo {}|| /bin/sed s/==%/%/g)\\; exec /bin/bash --login"'"'"
MSYSTEM=MINGW64
をもっと手前で設定して、-ArgumentList
で--login
を指定して、最後をexec bash
にしてもよい。
piperunを使わずにshで頑張ることもできるが、エスケープが多く読みづらくなる。
hiderun "C:\Program Files\Git\usr\bin\sh.exe" -c "IFS=; LANG=en_US.UTF8; pwd | printf %q $(/bin/cat) | /bin/sed \"s/'/''/g;s/%/==%/g;s/;/\\\\\\\\;/g\" | /bin/xargs -d '\n' -I {} -- powershell -Command Start-Process -Verb Runas -Filepath wt -ArgumentList '\"C:\Program Files\Git\usr\bin\sh.exe\"','-c', \"'\"'\"IFS=\\; export MSYSTEM=MINGW64\\;cd $(echo {}| /bin/sed s/==%/%/g)\\; exec /bin/bash --login\"'\"'\" "
Cygwinも同様。
関連
コマンドプロンプトから管理者権限のコマンドプロンプトに切り替える - Qiita
【Windows】GitBashをcontext menuからAdministrator権限付きで実行する - Qiita