はじめに
Node.jsにしてもPythonにしても、ターミナル(コマンドプロンプト)とエクスプローラーを開いて、そこからVisualStudio Codeで作業を始める方は多いと思います。
いつも同じフォルダにcd
したり、エクスプローラーで開いたりします。
めんどくさいのでそこまでやるツールを作りたいんですが、Win11になって正式に現れたターミナルにしても、エクスプローラーにしても、コマンドラインがあるのかないのかわかりませんが公開はされていないみたい。
以前、Windows10時代に作った自分の記事を見ながら、ここでもう一度作ったので、そのメモです。
下記はWindows10のときの記事。
説明の仕方として、最初に、1つ1つの技法を紹介して、最後にまとめて、今回のスクリプトを紹介します。技法っていうほどのこともないけど。
技法
ps1ファイル
WindowsのPowerShellを使いたいので、新規テキストファイルで空のファイルを作って、拡張子を.ps1にします。私は、初期設定.ps1にしました。
これを右クリックから「編集」で、専用のエディタも出るので、ここで編集します。
実行は、ツールバーにある「スクリプトを実行」ボタン(再生ボタン)でいけます。このエディタの使い方は割愛します。試行錯誤してみてください。
試しに、下記を書いてみると、下のコンソールの画面に"hello world!"が出ます。
Write-Output "hello world!"
Write-Output
は、Pythonでいうprint
、JavaScriptではconsole.log
の、標準出力です。
ちなみに、編集はメモ帳でもよくて、私はメモ帳で書くことよくもあります。その場合の実行は、ファイルを右クリックして「PowerShellで実行」です。
変数
PowerShellの変数は、$xxx
と、$
から始まる文字です。そこに代入し、読むときは$xxx
を呼び出します。
文字列の結合は+
です。
それを組み合わせて、こんな感じ。
$hello = "hello"
$world = "world"
$message = $hello + " " + $world + "!"
Write-Output $message
# hello world!
難しい使い方は、今回しません。
ターミナルとエクスプローラーを起動
起動するコマンドはこれです。
# ターミナル(PowerShell)
Start-Process wt.exe
# エクスプローラー
Start-Process explorer.exe
wt.exe
は、Windows Terminalの実体名(exeの名前)で、頭文字からとられています。覚えよう。
エクスプローラーは、癖のあるオプションもあるのですが、今回は使うのはやめました。素のエクスプローラーが立ちあがります。
sleep
ちょっと時間を置く処理です。
sleep -Milliseconds
で、ミリ秒単位でスリープします。
Write-Output "1"
sleep -Milliseconds 1000
Write-Output "2"
これは、PowerShellが動くの速度に比べて、エクスプローラーなどのGUIの速度は遅いので、PowerShellがコマンドをバババッと送ると、空振りしちゃうことがあります。その場合、スリープしてちょっと間を開けるとうまくいくことがあるので、使っています。
本来は、コマンドを送った後、「終わったよ~」という返事を待って、次のコマンドを送るとやりたいです。実はStart-Process
で、-Wait
コマンドを送る仕組み
急にプログラミングっぽい話です。
今作っているps1のファイルが実行されているウィンドウから、新たにターミナルとエクスプローラーを開き、そこへコマンドを送り付けるので、その仕組みです。
プロセスIDが$processId
という変数に入っている前提で、それをアクティブにして(=windowsの最前面に持ってくる)、それからabcde
というキーを順番に押す、というコードが下記です。
Add-Type -AssemblyName Microsoft.VisualBasic
Add-Type -AssemblyName System.Windows.Forms
# ウィンドウをアクティブにする
[Microsoft.VisualBasic.Interaction]::AppActivate($processId)
# 今のトップウィンドウにキーを押したことを送る
[System.Windows.Forms.SendKeys]::SendWait("abcde")
プロセスIDってなんやねん!ということですが、タスクマネージャーのPIDの列に表示されているIDです。下記は、メモ帳=Notepad.exeを起動した状態のタスクマネージャーですが、メモ帳のプロセスIDが19476であることを示しているので、
こんな風に直で19476
と書くと、メモ帳がアクティブになって、メモ帳にabcdeって書かれます。
Add-Type -AssemblyName Microsoft.VisualBasic
Add-Type -AssemblyName System.Windows.Forms
$processId = 19476 ### ←追加
# ウィンドウをアクティブにする
[Microsoft.VisualBasic.Interaction]::AppActivate($processId)
# 今のトップウィンドウにキーを押したことを送る
[System.Windows.Forms.SendKeys]::SendWait("abcde")
これによって、ウィンドウにキー操作を送る、つまりコマンドを送ることができます。
これだけでも便利ツールになる場面がありそうですね。
Ctrlキーを押しながらTの場合は(^t)
とか、Enterキーは{ENTER}
など、特殊なことがいくつかあります。具体的には下記をお読みください。
ちょっと戻って、ターミナルとエクスプローラーを起動
あえて戻りますが、もう一度、ターミナルとエクスプローラーを起動する話です。
なんと起動するときに、戻り値としてプロセスIDをゲットできます。
Add-Type -AssemblyName Microsoft.VisualBasic
Add-Type -AssemblyName System.Windows.Forms
# ターミナル(PowerShell)を起動
$process = Start-Process wt.exe -PassThru
# 1秒くらい待つ
sleep -Milliseconds 1000
# ウィンドウをアクティブにする
[Microsoft.VisualBasic.Interaction]::AppActivate($process.Id)
こうするとプロセスIDを利用できて、アクティブにできます。
でも大きな問題があって、実はエクスプローラーはこれができません。
エクスプローラーは、Start-Processで起動すると、起動したプロセスは、別のプロセスを起動したらすぐ終了してしまいます。起動された別のプロセスが、普通に触るエクスプローラー。なので別IDになるとのこと。
プロセスIDを得るための対策は、こんな感じです。
# エクスプローラーを起動
Start-Process explorer.exe
$process = ps | ? {$_.Name -eq "explorer"} | sort -Property CPU -Descending | select -First 1
# ウィンドウをアクティブにする
[Microsoft.VisualBasic.Interaction]::AppActivate($process.Id)
起動したら"explorer"で全プロセスを探し、複数出てくるから、今一番動いてるものを使う、という技です。
ここでもう1つどんでん返し。
せっかく正しいプロセスIDを得ても、AppActivate
するとかえってフォーカスが外れてしまって、この後のSendKeys
が渡らなくなります。どうやら、エクスプローラーは階層的になっているらしく、ここで得たIDを指定すると1つ上のもののようです。
下記の図はタスクマネージャーの一部です。4472は"ホーム"と表示されるウィンドウのもう1つ上っぽいですよね。なのでもう、Activateすることはせず、起動しっぱなしにします。
業務アプリだったらこんな方法はしたくないけど、自分の便利ツールくらいならまぁいいかという感じ。でもとても気持ち悪いので、いい方法があったら教えてください。
ターミナルでタブ名を変更
ターミナルは、タブをダブルクリックすれば名前を変更できます。
普段これで名前を変えていますが、自動で変更したいので、PowerShellのコマンドで変更する必要があります。そのコマンドが下記。
$Host.ui.RawUI.WindowTitle="abc"
大したことではなく、そうやって変えるんだ、ふーん、という話でした。でも覚えられないですね。
ちなみにコマンドプロンプトの場合は、title abc
です。覚えやすい。
ターミナルとエクスプローラーを開くツール
さて、上記で紹介した手法を使って、ターミナルとエクスプローラーを開くツールを作ってみます。
要件を整理します。
- 【概要】
- クライアント・サーバー型のアプリケーションをコーディングするために、クライアントとサーバーの両方の環境を準備する
- クライアント、サーバーともにNode.js
- 今はクライアントを開発中なので、クライアントはgit用の環境もほしい
- 【具体】
- ターミナルで、3つのタブを開く
- (1) クライアントのgit環境
- (2) クライアントのNode.js環境、
npm run dev
を実行 - (3) サーバーのNode.js環境、
npm run dev
を実行
- エクスプローラーで、2つのタブを開く
- (1) クライアント
- (2) サーバー
- ターミナルで、3つのタブを開く
ターミナルで、3つのタブを開く
コードを貼ります。
あえて関数化せず、べたべた書いてます。
# 設定
# クライアントのgitフォルダ
$client_git_path = "D:\Public\github\yo16\sql_parse_client_202407"
# クライアントのnpmフォルダ
$client_npm_path = "D:\Public\github\yo16\sql_parse_client_202407\sql_visualizer"
# サーバーのnpmフォルダ
$server_npm_path = "D:\Public\github\yo16\sql_parse_server_202407\sql_parse_ts"
# ----------------------------------
# インポート
Add-Type -AssemblyName Microsoft.VisualBasic
Add-Type -AssemblyName System.Windows.Forms
# ターミナル
# 起動
$terminalProcess = Start-Process PowerShell.exe -PassThru
#Write-Output $terminalProcess
sleep -Milliseconds 1000
[Microsoft.VisualBasic.Interaction]::AppActivate($terminalProcess.Id)
# クライアント git
$cmd_str = "cd " + $client_git_path + "{ENTER}"
[System.Windows.Forms.SendKeys]::SendWait($cmd_str)
[System.Windows.Forms.SendKeys]::SendWait("`$Host.ui.RawUI.WindowTitle=""GIT""{ENTER}") # タイトルを変更
[System.Windows.Forms.SendKeys]::SendWait("git status{ENTER}")
# クライアント npm
# Shift+Ctrl+1で、新しいタブを開く
[System.Windows.Forms.SendKeys]::SendWait("(^+1)")
sleep -Milliseconds 1000
$cmd_str = "cd " + $client_npm_path + "{ENTER}"
[System.Windows.Forms.SendKeys]::SendWait($cmd_str)
[System.Windows.Forms.SendKeys]::SendWait("`$Host.ui.RawUI.WindowTitle=""client""{ENTER}") # タイトルを変更
[System.Windows.Forms.SendKeys]::SendWait("npm run dev{ENTER}")
# サーバー npm
# Shift+Ctrl+1で、新しいタブを開く
[System.Windows.Forms.SendKeys]::SendWait("(^+1)")
sleep -Milliseconds 1000
$cmd_str = "cd " + $server_npm_path + "{ENTER}"
[System.Windows.Forms.SendKeys]::SendWait($cmd_str)
[System.Windows.Forms.SendKeys]::SendWait("`$Host.ui.RawUI.WindowTitle=""server""{ENTER}") # タイトルを変更
[System.Windows.Forms.SendKeys]::SendWait("npm run dev{ENTER}")
同じようなことの繰り返しなので、関数化したほうがいいかもしれません。でも使いまわすようなこともないし、特有なことが起こりがちなので、私はこの手のツールの場合は、結構べた書きです。
ここまでで、ターミナルの3つのタブで、3つの環境(フォルダ)にいる、ということができました。
エクスプローラーで、2つのタブを開く
つぎにエクスプローラーです。
実際は、上のターミナル関係のコードの下に書きますので、設定とインポートの部分は書きません。
#~~~ ここより上は省略 ~~~
# エクスプローラー
# 起動
Start-Process explorer.exe
sleep -Milliseconds 1000
# クライアントのnpmフォルダを開く
[System.Windows.Forms.SendKeys]::SendWait("{F4}")
$cmd_str = $client_npm_path + "{ENTER}"
[System.Windows.Forms.SendKeys]::SendWait($cmd_str)
# 新しいタブを開く
[System.Windows.Forms.SendKeys]::SendWait("(^t)")
sleep -Milliseconds 1000
# サーバーのnpmフォルダを開く
[System.Windows.Forms.SendKeys]::SendWait("{F4}")
$cmd_str = $server_npm_path + "{ENTER}"
[System.Windows.Forms.SendKeys]::SendWait($cmd_str)
エクスプローラーのところで書いた通り、sleepはするけど、アクティブの操作は無視です。最上位に出ているはず!という願いにも似た前提で。
あとは、Ctrl+Tで新しいタブを開く点と、F4を押すとフォルダを直で入力できるようになるので入力する点。
以上で解説おしまいです。
おわりに
エクスプローラーの周辺が混とんとしていますね。最終的には、Activateするのをあきらめて、最前面に出ていることを願うという大技に出ました。 これはよくない。それが成り立つならターミナルもActivateしなくていいし。
まぁ、sleepすることも本当は気持ち悪いです。1秒待てば必ず使える状態になっているのかというとそうとは限らないわけで。
そういうわけで今回は、すっきりしない点がありますが、自分用の時短ツールだと思えば、そんなにカリカリせず許してやろうという気持ちにもなります。この記事を参考にご自分のツールを作られる方も、それくらいの気持ちでお使いください。
個人的には、ないよりはあった方がよいです。ちょっと時間を置くと、フォルダとかnpm run dev
なのかnpm run debug
なのかとか、忘れちゃうし。
ではよき時短ツールライフを。
2024/9/23追記
ターミナルも、Activateすると、そんなID知らんって言われました。必ずそうなるのかな。昨日は気づかなかったけど。
その場合はこちらも、Activateせず、起動しっぱなしにすればよいです。