「Windowsでエクスプローラーからフォルダを各種ターミナルで開く」の版間の差分

提供:Turgenev's Wiki
(Notion-MW)
(Notion-MW)
302行目: 302行目:
たとえば以下のようなvbsを用意する。
たとえば以下のようなvbsを用意する。


<syntaxhighlight style="margin-bottom:0.2em;" lang="python">vb.net
m_with_cap_m
Dim objShell
 
Set objShell = CreateObject("WScript.Shell")
<pre class="visual">Dim objShell
Set objShell = CreateObject(&quot;WScript.Shell&quot;)
objShell.CurrentDirectory = WScript.Arguments(1)
objShell.CurrentDirectory = WScript.Arguments(1)
objShell.Run(WScript.Arguments(0)),,False</syntaxhighlight>
objShell.Run(WScript.Arguments(0)),,False</pre>
<div style='text-align: center;'>startatdir.vbs</div>
<div style='text-align: center;'>startatdir.vbs</div>


489行目: 490行目:
<li><p>vbsの例</p>
<li><p>vbsの例</p>
<p>次の節で解説する「(自分が起動された)カレントディレクトリでの起動」にも対応している(引数がない場合)。</p>
<p>次の節で解説する「(自分が起動された)カレントディレクトリでの起動」にも対応している(引数がない場合)。</p>
<syntaxhighlight lang="</syntaxhighlight></li></ul>"></li></ul>
<pre class="visual">Dim objShell
Dim curDir


vb.net Dim objShell Dim curDir
Set ws = CreateObject(&quot;Wscript.Shell&quot;)
Dim dir
If WScript.Arguments.Count = 0 Then
  dir = ws.CurrentDirectory
Else
  dir = Wscript.Arguments(0)
End If
dir = Replace(dir,&quot;%&quot;,&quot;==%&quot;)
ws.run &quot;powershell -Command &quot;&quot;&amp; {Start-Process -Verb Runas -Filepath wt -Argumentlist powershell, -noexit, -command, Set-Location, -LiteralPath, ('\&quot;&quot;'''+($args[1].Trim('\&quot;&quot;') -replace '''', '''''' -replace ';', '\;' )+'''.Replace(''==%'',''%'')\&quot;&quot;')}&quot;&quot; --% &quot;&quot;\&quot;&quot;&quot;&amp;dir&amp;&quot;\&quot;&quot;&quot;&quot;&quot;, vbHide


<pre>    Set ws = CreateObject(&quot;Wscript.Shell&quot;)
Set objShell = Nothing</pre></li></ul>
    Dim dir
</li>
    If WScript.Arguments.Count = 0 Then
      dir = ws.CurrentDirectory
    Else
      dir = Wscript.Arguments(0)
    End If
    dir = Replace(dir,&quot;%&quot;,&quot;==%&quot;)
    ws.run &quot;powershell -Command &quot;&quot;&amp; {Start-Process -Verb Runas -Filepath wt -Argumentlist powershell, -noexit, -command, Set-Location, -LiteralPath, ('\&quot;&quot;'''+($args[1].Trim('\&quot;&quot;') -replace '''', '''''' -replace ';', '\;' )+'''.Replace(''==%'',''%'')\&quot;&quot;')}&quot;&quot; --% &quot;&quot;\&quot;&quot;&quot;&amp;dir&amp;&quot;\&quot;&quot;&quot;&quot;&quot;, vbHide
   
    Set objShell = Nothing
    ```</pre>
<ul>
<li><p>cmd</p>
<li><p>cmd</p>
<p><code>powershell &#45;Command &quot;&amp; &#123;$bak&#61;$env&#58;MY_PERCENT&#59;$mypath&#61;$args&#91;1&#93;.Trim(&#39;\&quot;&#39;)&#59; $env&#58;MY_PERCENT &#61; &#39;%%&#39;&#59;if ($mypath.Length &#45;gt 258) &#123; $mypath &#61; cmd /c \&quot;for %%A in (`\&quot;$($mypath.Replace(&#39;%%&#39;, &#39;%%MY_PERCENT%%&#39;))`\&quot;) do @echo %%&#126;sA\&quot; &#125;&#59;$env&#58;MY_PERCENT &#61; $bak&#59;Start&#45;Process &#45;Verb Runas &#45;Filepath wt &#45;Argumentlist &#39;&#45;d&#39;, (&#39;\&quot;&#39;+($mypath &#45;replace &#39;\\$&#39;,&#39;\\\\&#39; &#45;replace &#39;&#59;&#39;, &#39;\&#59;&#39;)+&#39;\&quot;&#39;), cmd&#125;&quot; &#45;&#45;%% &quot;\&quot;%V\&quot;&quot;</code></p>
<p><code>powershell &#45;Command &quot;&amp; &#123;$bak&#61;$env&#58;MY_PERCENT&#59;$mypath&#61;$args&#91;1&#93;.Trim(&#39;\&quot;&#39;)&#59; $env&#58;MY_PERCENT &#61; &#39;%%&#39;&#59;if ($mypath.Length &#45;gt 258) &#123; $mypath &#61; cmd /c \&quot;for %%A in (`\&quot;$($mypath.Replace(&#39;%%&#39;, &#39;%%MY_PERCENT%%&#39;))`\&quot;) do @echo %%&#126;sA\&quot; &#125;&#59;$env&#58;MY_PERCENT &#61; $bak&#59;Start&#45;Process &#45;Verb Runas &#45;Filepath wt &#45;Argumentlist &#39;&#45;d&#39;, (&#39;\&quot;&#39;+($mypath &#45;replace &#39;\\$&#39;,&#39;\\\\&#39; &#45;replace &#39;&#59;&#39;, &#39;\&#59;&#39;)+&#39;\&quot;&#39;), cmd&#125;&quot; &#45;&#45;%% &quot;\&quot;%V\&quot;&quot;</code></p>
514行目: 513行目:
<p><code>&quot;C&#58;\Program Files\Git\usr\bin\sh.exe&quot; &quot;C&#58;\path\to\gb&#45;wt&#45;admin.sh&quot; &quot;%V&quot;</code></p>
<p><code>&quot;C&#58;\Program Files\Git\usr\bin\sh.exe&quot; &quot;C&#58;\path\to\gb&#45;wt&#45;admin.sh&quot; &quot;%V&quot;</code></p>
<p>で、<code>gb&#45;wt&#45;admin.sh</code>の中身は以下。</p>
<p>で、<code>gb&#45;wt&#45;admin.sh</code>の中身は以下。</p>
<syntaxhighlight lang="</syntaxhighlight></li></ul>">
<syntaxhighlight lang="c#">#!/bin/sh
c# #!/bin/sh IFS= LANG=en_US.UTF-8 echo $*|/bin/sed 's/\/\\/'|/bin/cygpath -f -|printf %q $(/bin/cat) |/bin/sed &quot;s/'/''/g;s/%/==%/g;s/;/\\;/g&quot;|/bin/xargs -d '\n' -I {} powershell -Command Start-Process -Filepath wt -ArgumentList &quot;'&quot;'&quot;C:\Program Files\Git\usr\bin\env.exe&quot;'&quot;'&quot; , '&quot;MSYSTEM=MINGW64&quot;' , &quot;'&quot;'&quot;C:\Program Files\Git\usr\bin\sh.exe&quot;'&quot;'&quot; , &quot;'--login'&quot;, &quot;'-c'&quot;, &quot;'&quot;'&quot;IFS=;cd $(echo {}| /bin/sed s/==%/%/g) ; exec bash&quot;'&quot;'&quot; ```
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"'"'"</syntaxhighlight>
<p>Cygwinも同様である。</p></li></ul>


<pre>Cygwinも同様である。</pre>
<span id="vs-codeのメニューおまけ"></span>
<span id="vs-codeのメニューおまけ"></span>
==== VS Codeのメニュー(おまけ) ====
==== VS Codeのメニュー(おまけ) ====

2023年11月24日 (金) 19:54時点における版

Explorerの右クリックメニューやアドレスバーからフォルダを各種ターミナルで開くための設定方法の紹介。cmd.exe, powershell(pwsh含む), Cygwin, Git Bashの4つに対応。

基本

  • Windowsの右クリックメニューから複数ファイルをまとめて開く のように、レジストリでフォルダ・フォルダ背景の右クリックメニューを編集できる。
    • Windows10までの右クリックメニューに戻そう。
    • commandキーの既定値に直接コマンドを書く方法と、DelegateExecute値(など)を使ってCOMを通じて実行する方法(このためにhttps://github.com/ge9/ExecuteCommand-Pipe(使い方はレポジトリの説明を参照)を作った)がある。
    • HKLMにあるマシン全体の設定は、それ自体は管理者権限(あるいはTrustedInstallerなどさらに上の権限)がないと書き換えられないが、HKCUに値があればそちらが優先されるので、自分だけに関する設定変更ならユーザー権限だけで可能である。
    • DirectoryDirectory\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で見ると見られることもある。中身の閲覧などは不可。
  • また、末尾が空白またはピリオドのファイルも禁止で、Explorerやcmdでは作れない(自動で除去される)。
    • これらはGit Bash/Cygwinなら作れる。前者はエクスプローラーで閲覧など多少の操作ができるが後者はほとんど何もできず、DelegateExecuteの右クリックメニューでもピリオドが外れた名前しか取得できない。
  • 先頭が空白のファイルはExplorerだと作れないがcmdなら作れる。禁止ではないはず。

Git Bash/Cygwin

  • Git BashではWindowsの実行ファイルにスラッシュ付きオプションを渡すときはスラッシュを2つにする必要がある。cmd //cbcdedit //vなど。
  • printf関数の%qオプションを使うと、文字列をエスケープされた状態にして返してくれる。これはshからの呼び出し先の別のshで再びフォルダ名をcdなどの引数として利用したいときに使える。
    • shの組み込みコマンドのprintfと、独立実行ファイルである/bin/printfがある。基本的な趣旨としては同一のコマンドだが、組み込みコマンドのほうは入力に"が含まれていない限り結果にも"が使われることはないようで、このおかげで意図しない動作を回避できることがある。今回扱う例に関しては前者が後者の上位互換であるということになる。後者を使っているものはそれでも動いたものである。
    • そもそも%qは組み込みコマンドのほうにしかないという話もある。環境によって違いそう。
  • \\?\が付くような長いパスをカレントディレクトリにしていても、Git Bash/Cygwinに付属の(というかMSYS/Cygwin向けにコンパイルされた?)exeなら実行できる。
  • Git Bashの(ba)shは--loginをつけて起動してもカレントディレクトリが維持されるが、Cygwinの(ba)shは--loginをつけると強制的にホームディレクトリに移動する。
  • Git Bashにおいては環境変数を正しく設定するため--loginの時点で環境変数MSYSTEMMINGW64にしておく必要がある。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に標準では入っていないので、以下の例でもあまり依存しないように気を付ける。
  • powershell.exe
    • -Commandがなくても付けたのと同じ扱いになる?
      • 逆に、-Commandを付けなければいけないという点を除いてはpwshがpowershellの上位互換という感覚である。
    • `[]を含むフォルダの扱いにバグがあり、cmdなどでこれらのフォルダをカレントディレクトリとした状態で5系のPowerShellを起動するとC:\C:\Windows\System32\WindowsPowerShell\v1.0>をカレントディレクトリとして起動される(たとえばC:\somefolder][なら前者、C:\somefolder[]なら後者)。
  • powershell.exe・pwsh.exe共通
    • 連続した`があるフォルダに関する挙動にバグがある。コンソールプログラムを実行しても新規ウインドウで開かれる、そこをカレントディレクトリとしたときにファイルをmvできないなど。
    • `[]を含むフォルダに関しても、絶対パス指定したりきちんとエスケープしたりすれば操作自体はできる(場合もある)。
      • これらを特殊文字として扱いたくないときは-LiteralPathを付けると良い場合もある。
    • UNCパスをカレントディレクトリにすることができるが、Microsoft.PowerShell.Core\FileSystem::\\とかいう謎のプレフィックスが付く。
    • 末尾が空白のフォルダについてはcdできず、Cygwinなどでそのようなフォルダをカレントディレクトリとして起動したとしてもC:\Windows\System32\WindowsPowerShell\v1.0C:\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"cd"がそれぞれ別々に実行される
    • すなわち、"が一見特殊文字でないかのような振る舞いをする(そのまま出力される)割に、その前後で特殊文字の扱いが切り替わっている。

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が必要そう。
  • /bin/printfやprintfの実行時にはLANGen_US.UTF8とかに設定する。
    • Cygwinなら不要?

デバッグ時

  • 管理者権限のものを試すときでもまず-Verb Runasを外して動作確認するとよい。
  • 後述のwaitrunも役に立つだろう。
  • 特殊文字に関しては、最初は空白のないフォルダ、次に空白のあるフォルダや連続空白のあるフォルダ、次にシングルクォートや%PATH%を含むフォルダなどとだんだん難しくしていくとよい。
  • それとは別に、Unicode対応、C:\(ドライブ直下)対応、259文字のフォルダ対応(後述)などをチェックする
  • 難しい例は、例えばz  𠮷𠮷%PATH%  '  ''  `  `` $PATH & &&   %%PATH%%[] ] [  ^  ^^   '  ;  ;;  𩸽𩸽!PATH!#$%&'()=~{`+}_,.][;@^-のような感じ。こんなファイル名を見たことはないが、エクスプローラー上で普通に入力できる内容である。
    • 「𠮷」はBMP外かつJIS外の漢字としては最も変換で出しやすいのでテストに重宝する。

その他

  • デフォルトシェルは、通常ユーザーはwtでも管理者の場合はconhostに設定されているっぽい?
  • C:\のようなドライブ直下のパスは末尾に\を付けなければいけないことが多い。たとえばwt -dの引数にする場合など。またcmdでcdコマンドやCD環境変数を見るときもドライブ直下のときだけは\が付いて返ってくる。
    • ただ、wt -d にC:が渡されるように見えるのになぜか動いているものもある。未調査。
  • 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-delegator

ただし、これはあくまで筆者が独自に作ったものであり、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
  • uacrun

    startrunと同じだが、コンパイル時のマニフェスト設定でこのプログラム自体の実行に管理者権限を要求するようにしたので、adminrunと同じように使える(管理者権限で実行する対象が渡されたコマンドではなくuacrun自体になるという違いはある)

  • pecho

    与えられたコマンドライン引数をそのままコンソールに出力する。

  • printcd

    カレントディレクトリをコンソールに出力する。(ちなみにpwdの由来はprint working directoryなのでそれに倣った)

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.exeC:\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を用意する。

m_with_cap_m

Dim objShell
Set objShell = CreateObject("WScript.Shell")
objShell.CurrentDirectory = WScript.Arguments(1)
objShell.Run(WScript.Arguments(0)),,False
startatdir.vbs

すると以下のように書ける。

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の最初で指定する(メインの)実行ファイル(先ほどならpowershellsh)をファイル名単体で書く場合は、それが(メニューをHKCUで設定するならHKCUの(最近のWindowsならHKLMのでも可?)、HKLMで設定するならHKLMの)App Pathsキーに登録されていなければならない(PATHは通っていなくてもいい)。登録されていない場合はフルパスで指定する必要がある。

設定例

管理者権限なし

  • PowerShell

    デフォルトターミナルで

    powershell -noexit -Command "& {Set-Location -LiteralPath $args[1].Trim('\"')}" --%% "\"%V\""

    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は打ち消し合って無駄なので消してよい。

このほかに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

エクスプローラーから管理者権限でコマンドプロンプトを開く方法|ひるあんどん