パスワードを忘れた? アカウント作成
15597660 journal
Windows

route127の日記: コンソール出力を直接メモ帳へ書き込み 13

日記 by route127

調べ物をしていたら「一時ファイルを作成せずにメモ帳へ書き込みたいが何か良い方法はないだろうか?」という質問を見かけた。
読んでみるとvim使え、とかPowerShellでCtrl+V操作をエミュレートする、等ある中でautohotkeyを利用してコンソールからAutohotkeyA32にパイプする方法がなんか格好よく思えて自身のWin10環境でも試していた。
日本語環境ではメモ帳(notepad.exe)の捕捉を

IfWinActive Untitled - Notepad

から

IfWinActive 無題 - メモ帳

に変えれば問題なく動作した。

以下をstdin.ahkとして保存し、

StdIn(max_chars=0xfff)
{
    static hStdIn=-1
    ptrtype := (A_ptrSize = 8) ? "ptr" : "uint"
 
    if (hStdIn = -1)
    {
        hStdIn := DllCall("GetStdHandle", "UInt", -10, ptrtype) ; -10=STD_INPUT_HANDLE
        if ErrorLevel
            return 0
    }
 
    max_chars := VarSetCapacity(text, max_chars*(!!A_IsUnicode+1), 0)
 
    ret := DllCall("ReadFile"
        , ptrtype, hStdIn
        , "Str", text
        , "UInt", max_chars*(!!A_IsUnicode+1)
        , "UInt*", bytesRead
        , ptrtype, 0)
 
    return text
}
 
loop
{
    sleep 100
    Buffer:=StdIn()
    PipeText:=PipeText . Buffer
    ;IfWinActive Untitled - Notepad
    IfWinActive 無題 - メモ帳
        {
            SendInput {Raw}%PipeText%
            PipeText =
        }
}

このstdin.ahkをautohotkeya32.exeの引数にとり、任意のコンソールコマンドからパイプでつなぎ込むとコンソール出力が無題メモ帳に書き込まれる。

hogehoge | autohotkeya32.exe stdin.ahk

処理を眺めるとGetStdHandleで標準入力を開いてコンソールからのパイプされる出力を捕まえて、それをahkのSendInputで流し込むだけ、といえばそうなのだがニーズから適切なWin32API関数を選定し、それらを組合せ、かつahkというチョイスで提示するのが鮮やかに感じられた。
これがPerlなら似たようなものがWin32::GuiTestを使って

hogehoge | perl -MEncode -ne "BEGIN{use Win32::GuiTest qw(:ALL);($hwnd)=grep{GetWindowText($_)!~/cmd.exe/}FindWindowLike(undef, qq/無題 - メモ帳/);SetForegroundWindow($hwnd);}SendKeys($_)"

と書けるが、ahkに比べて動作が遅い上にフォーカスがずれた時の処理もない(バックグラウンドで動作できない)こともあってあまり使いどころは無さそうではある。
またPerlと関連モジュールの知識があれば書けるので、書けて当然、動いて当然という感じでWin32APIを使いこなす興奮も特にない。
ただ動作速度の問題がバッファリングの問題なのかSendKeys周りの問題なのかとか詰めていけば別種の面白さがあったりもするのかもしれない。

ところでどちらにも共通するのが多バイト文字の出力ができないことなのだが、先日文字コードでぐだついてたのはそれを解決しようとしての事だった。
Autohotkeyにパイプで渡す前にperlで文字コードを変換しようとしてみたり、Autohotkey本体をA32/U32/U64と切り替えたり、ahkファイル内で文字コードを変換しようとして結局解決していない。
Perlで書き直してたときに「SendKeysで漢字は送れないよなあ」みたいな感覚があってその辺でAutohotkeyにおけるSend{Raw}あたりの仕様が理解出来てないような気がしてきた。
あるいは入力側のReadFileあたりなのか?
だとすればPerlなら<STDIN>で済むところで色々躓いているのか。

5chAHKスレで聞こうと思ったらで書き込めなかった。
実況板もなんだかんだ半年以上書き込めない。

この議論は賞味期限が切れたので、アーカイブ化されています。 新たにコメントを付けることはできません。
  • by Anonymous Coward on 2022年03月16日 0時49分 (#4216253)

    クラス名"Edit"のウィンドウをメモ帳の子ウィンドウからFindWindowExで探してWM_SETTEXTかEM_REPLACESELで投げつける。
    Windows 11の新しいメモ帳ではクラス名を変えないと動かないけど、そもそも新しいメモ帳はタイトルも「無題 - メモ帳」ではないのでWindows 10専用ということで。

    • SendMessageでテキスト送りつけられる [coocan.jp]のを初めて知った。
      SendMessageはWM_CLOSEでウィンドウ閉じたり電卓のボタン押させたりする [perl-users.jp]位でしか使うシーンが思い浮かべられなかったし、不必要に使うとシステムが不安定化するみたいな心配があって積極的には触れてこなかった。
      いま改めてSendMessageを使う決心をしてみるとWin32::GuiTest [metacpan.org]にも

      SendMessage(hWnd,Msg,wParam,lParam)
      WMSetText($hwnd)

      の様な関数が用意されていることにも気付けた。
      それらを利用して試行錯誤でワンライナ書いてみた。

      hogehoge | perl -MWin32::GuiTest=:ALL -ne "BEGIN{($hwnd)=grep{GetWindowText($_)!~/cmd\.exe/}FindWindowLike(undef, qq/無題 - メモ帳/);($edit)=grep{GetClassName($_) eq 'Edit'}GetChildWindows($hwnd)}WMSetText($edit,WMGetText($edit) . qq/$_/)"

      最初Editの親の方にテキスト送りつけていてウィンドウタイトルを一生懸命書き換えていた。
      伺か [sakura.vg]にそんな機能あったな。
      Editにテキストをセットするのも毎行書き換えてしまうので一旦WMGetTextで取り出したものに連結していくようにしたがなんか冗長な感じがなくもない。

      親コメント
      • by Anonymous Coward

        EM_SETSEL(初回のみカーソル位置移動)とEM_REPLACESEL(置換と言いつつ未選択なら追記)でやれば追記になるからスッキリすると思う。

        • 最初のコメント [srad.jp]で何でEM_REPLACESEL [microsoft.com](0xC2 [ipentec.com])が提示されてるのかと訝しんだが伏線だったのか。
          一人でドキュメント読んでたら気付かなかったことを考えると恥ずかしさに耐えて日記書いてよかったのか。

          PerlならWin32::GuiTestのWMSetTextをSendMessageに変えれば簡単だろうと思ったらモジュールの仕様上子ウィンドウに対して使えないらしく、結局Win32::APIで呼ぶ羽目になった。

          hogehoge | perl -MWin32::GuiTest=:ALL -MWin32::API -ne "BEGIN{($hwnd)=grep{GetWindowText($_)!~/cmd\.exe/}FindWindowLike(undef, qq/無題 - メモ帳/);($edit)=grep{GetClassName($_) eq 'Edit'}GetChildWindows($hwnd)}Win32::API->new('user32','SendMessageA','NNNP','N')->Call($edit,0xc2,0,qq/$_/)"

          親コメント
    • by Anonymous Coward

      メモ帳使いたいならこれが正解でしょうね。

      ただgitシェル使ってるなら ログファイルにリダイレクトして
      tail -f ログファイル
      でもやっとけばいいじゃんと思います。ログ見るだけのために、いちいちスクリプトとかコードを実装する時間が勿体無いです。目的と手段を取り違えているように感じます。

      • by Anonymous Coward

        PowerShellでもGet-Content パス -WaitでOK

  • by Anonymous Coward on 2022年03月16日 12時14分 (#4216486)

    Autohotkeyとか全く知らんけど、Altコード [wikipedia.org]で解決するしかないかね?
    #4216253 [srad.jp]的解決法の方が外乱に強いので個人的には好きだけど。

    Send{Raw}の{Raw}が付いてる理由は、{Raw}が無いと、パイプに入って来る{}やら記号でAltキー等のメタキーが効いちゃうとかその辺の理由。
    多バイト非対応なのは、Send~は内部的に仮想キーコード [microsoft.com]をSendInput [microsoft.com]で送る仕様なんだろうね。
    この場合、プログラムが人間の代わりにキーボード叩いているだけなので、キーボードに無い文字はそのまま送れない=多バイト不可って事になる。
    ただし、左Altキー+テンキー(VK_NUMPAD0~9)で文字コードを入力する事で、多バイトもIME抜きで直接入れられる。(前述のAltコードとか呼ばれる)
    が、前提条件とかスループットがかなり落ち込むのであまりおススメしない。

    と書いて日記のSend{Raw}のリンク先見たら、最初からAltキーコードで送るような仕組み持ってるじゃん。
    {ASC nnnnn} [ahkwiki.net]
    色々準備とか要らなそうなので、大抵の言語が持ってる、一文字切り出しと、
    文字を文字コードにするASCやASCII関数と、{ASC nnnnn}で送るを繰り返すループを仕込むと多分出来るよ。

    SendInput {ASC %AscCode%}みたいな表記が出来るかは知らんので、その辺の試行錯誤は頑張って。
    a32.exeって名前からしてANSI 32bit版なんだろうけど、Unicode版じゃないと多バイト文字列を適切にハンドリングできずにハマるかも。
    一文字切り出しとかモロにハマりポイントだからね。

    • Altコードを調べていたがOADGで決まってるのとは違うのかいまいちわからなかった。
      Win10のメモ帳でもユニコードのU+nnnnからF5で変換できたけどそれともなんか違う気がするしちゃんと英語版Wikipedia読もうと思ったけどなんかすごいデカい地震来てドキドキしたので寝る。

      親コメント
      • by Anonymous Coward

        Altを押したまま10進数にしたOEMCPの文字コード打ってAlt離すだけ。
        「あ」と打つなら「Alt押しっぱにして33440って打ってAlt放す」
        改行なら10か13。
        現在のマルチバイト文字コードを使う以上、Unicodeや他ロケールにしかない文字は打てないし、
        Unicode文字列を扱っている状態で変換するには文字コード変換が必要。

    • by Anonymous Coward

      時間が出来たので、サンプルコード。
      知らん言語で、言語リファレンスもまともに見てないのでもっと良い書き方があるとか、
      実際に動かしてないので動くかどうかしらんが書き換え候補張っときます。

      {
          SendInput {Raw}%PipeText%
          PipeText =
      }

      を下記のように。
      {
          Loop, Parse, PipeText
          {
              AscCode := Asc( %A_LoopField% )
              SendInput {ASC %AscCode%}
          }
          PipeText =
      }

      スラドに空白が多いと怒られたから2タブになってます。

      キーボードにある文字も展開してるので処理時間が超伸びるし、
      Altキーなど

      • 試行錯誤してたが無理な気がしてきた。

                {
                    Loop, Parse, PipeText
                    {
                        AscCode := Asc(A_LoopField)
                        SendInput % AscCode
                    }
                    PipeText =
                }

        としてみたが意図した出力が得られなかった。どうも

        Loop, Parse, PipeText

        が8バイト毎に切り出してるっぽくて「あ」が33400(0x82a0)じゃなくて130,160(0x82,0xa0)で出力される。

                {
                    array:=StrSplit(PipeText)
                    For index, element in array
                    {
                        AscCode := Asc(element)
                        SendInput % AscCode
                    }
                    PipeText =
                }

        Loop,Parseの代わりにStrSplitを使っても同じだった。

        親コメント
typodupeerror

クラックを法規制強化で止められると思ってる奴は頭がおかしい -- あるアレゲ人

読み込み中...