月別アーカイブ: 2021年12月

whywaita Advent Calendar 2021 2日目

皆様お久しぶりです、けいなです。

去年もAC以外では更新していないと書いていましたが、今年もAC以外では更新しない人となっていました。あまりにもメンテナンスしていないのでいつになるかは分かりませんがどこかに移行するかstaticなHTMLに置き換えようと思います。いっそのこと廃止でも構わないのですが……それは少し寂しいので。

本記事はwhywaita Advent Calendar 2021の2日目の記事です。
今年も@masawadaに生成されて7年目らしいですね。去年はwhywaita Advent Calendar 2020 15日目として別人格たる白河観測所のことを書きましたが、C99開催とのことで新刊を用意しなくてはいけません。まだファイルを作ったところなのですが、大晦日に東に来る人は寄っていただければ幸いです。

昨日の記事は首謀者(?)であるmasawadaさんのwhywaita Advent Calendar 1日目でした。
masawadaとの関係性も怪しくて調べ返していましたが、UEC12らしいので2つ下……で合っているでしょうか。近く開催されるDentoo.LTDentoo.LT #26として26回目で主催はUEC20ということですので時間の流れは怖いですね…。

さて、本題に入りましょう。

皆様は普段どのような環境で生活されているでしょうか。最近初めてKubernetes+Dockerというプロジェクトに投入されて誰かがやりたがったのかなあとか思いつつ慌てて勉強しているような2次請PGとしては、モダンな環境とは基本的に縁がないのでWindowsとベッタリで生きています。とはいえ飽きが来ているのでぼちぼち次を探し始めています。お手伝いできることはあまりないと思うのですがお声がけいただければ幸いです。

Windowsと生きていると時より必要になるのがUACによる権限昇格です。雑な操作をスクリプトで多少自動化したいと思ったときに地味に阻んでくるやつでもあります。

その都度ネットの海を漂ってその場限りの1行コマンドであったり、昇格再投入スクリプトを書いていたりしたのですが、いい加減に汎用的なスクリプトを用意しておきたくなったので用意しました。

とりあえず最初に結論として、作成したsudo.cmdを提示します。

@REM 管理者権限で実行する。
@ECHO OFF
setlocal

openfiles > NUL 2>&1
if "%ERRORLEVEL%" == "0" (
    goto :execute
)
set SUDO_EXE="""%~dpnx0"""

set SUDO_ARG=%*
set SUDO_ARG=%SUDO_ARG:"="""%

set SUDO_PWD=%CD%

powershell.exe -Command "Start-Process cmd -ArgumentList /K,'"""start','/D','"""%SUDO_PWD%"""','/WAIT','/B','%SUDO_EXE%','%SUDO_ARG%"""' -Verb runas -WorkingDirectory '%SUDO_PWD%' -Wait"
exit /b

:execute
%*
endlocal

一応多少の解説を並べます。

openfilesで管理者権限の判定を実施、有効であれば引数のコマンドを実行して終わりです。
無効であれば、特権昇格と再実行のための準備を行います。

いくつか環境変数を作成するので明示的にsetlocalとendlocalで囲んであります。

まず、SUDO_EXEとしてこのスクリプト自体の絶対パスを取得します。パスにスペースが含まれていた場合に備えて”””で囲みます。
次に、引数全てをSUDO_ARGとして変数に入れ、引数内の”を”””に変換します。
最後に、コマンドを実行したパスをSUDO_PWDとして取得しておきます。
正確にはEXEもPWDも取得は不要ですが、自身の分かりやすさを優先しました。また、PWDをここで囲っていないのは後の都合です。

最後に、powershell.exeにこれらの変数を用いつつ投入します。

基本となるのはpowershell.exe -Verb runasです。一応こちら側にも-WorkingDirectoryを与えていますが、不要な気がします。結果の確認を優先して-Waitも付与しています。-Commandに投入する本題を記述します。-NoNewWindowを与えられればより良かったのですが、-Verb runasと併用できませんでした。

powershell.exeにコマンドとして与えているのはpowershellのStart-Processコマンドです。ここでcmdを起動し、-ArgumentListで引数を与えています。ここで各引数を’で囲って,でリストにするpowershell風味になっているのは気持ちの問題です(最終的にはスペースで繋がれて実行されるので、端からその想定で与えてしまっても一向に構わないのですが、せっかくpowershellのコマンドなので、という程度の理由です)。

cmdに対する引数は、与えたコマンドを実行して終了しない/Kスイッチがメインです(/Cスイッチでも良いのですが、そのまま消えてしまうとちゃんと動いたのかさえ分からないのでこうしています)。
/Kスイッチの引数として、startコマンドを与えています。

startコマンドに対して、/Dスイッチで実行パスのSUDO_PWDを”””で囲って設定します。cmdに対して与えたかったのですがそのようなスイッチがなく、'”””cd /d “””%SUDO_PWD%””” ^&^&’,’%SUDO_EXE%’,’%SUDO_ARG%”””‘のように実行する分かりにくいなにかになったのでもう1段噛ませることにしました。更に/WAITを与えてコマンド終了を待機するようにしています。また、startではウィンドウを作成しないように/Bスイッチを与えています。

最後に、SUDO_EXEにSUDO_ARGを与えたコマンドが投入されて完成です。

さて、ここまで放置していたいくつかの事項を最後に説明します。

まず、”を”””と展開する点についてです。
これについては、cmdが引数をどのように取り扱うか、ということが問題になります(根拠となる資料を発見していたような記憶があるのですが、現時点で探し出せなかったため実験をベースに解説します)。

>type test.cmd
@echo off
echo %*
echo %0
echo %1
echo %2
echo %3
echo %4
echo %5
echo.

if not "%~1" == "TEST" (
        call test.cmd TEST %*
)
>test.cmd  1 "2 b" """3 c""" "4_d" """5e"""
1 "2 b" """3 c""" "4_d" """5e"""
test.cmd
1
"2 b"
"""3 c"""
"4_d"
"""5e"""

TEST 1 "2 b" """3 c""" "4_d" """5e"""
test.cmd
TEST
1
"2 b"
"""3 c"""
"4_d"
"""5e"""

>python -c "import sys; [print(i) for i in sys.argv]" 1 "2 b" """3 c""" "4_d" """5e"""
-c
1
2 b
"3 c"
4_d
"5e"

>powershell echo 1 "2 b" """3 c""" "4_d" """5e"""
1
2
b
3 c
4_d
5e

>powershell echo '1' '"2 b"' '"""3 c"""' '"4_d"' '"""5e"""'
1
2 b
"3 c"
4_d
"5e"


cmd.exe自身は、与えられた引数について、”を区切りのような意味合いでしか使用しません。一定のペアで引数が分割されますが、ここの値自体は生きています。
問題は、cmd.exeから外部に引数として与えた場合の挙動で、これは呼び出される外部プログラムにも依りますが、引数を囲んだ”自体は消失するような挙動を示します。ただし、”””に変更した場合にはプログラム側まで貫通して提供されます。
このため、powershell.exeに提供する際にcmd.exeに再提供するためのパラメーターとして”を与え直すために、消失しないような形に変更する必要があり、”を”””として改変します。
powershell.exeは観察時点で”を省略する別の挙動があるので、’で囲って文字列として強制しておきます(これはechoもといWrite-Outputの挙動のように思うので違う気もします)。

次は、-ArgumentListの引数で、/Kの次に'”””と始まり、%SUDO_ARG%”””‘と終わらせています。
これはcmdのヘルプにもある次の挙動のためです。

>cmd /?
**中略**
/C または /K が指定されている場合、スイッチの後の残りのコマンド ラインが
コマンド ラインとして処理されます。次のルールが引用符 (") の処理に使われます:

    1.  次のすべての条件に一致する場合、コマンド ラインの引用符が有効になり
        ます:

        - /S スイッチがない
        - 引用符が 1 組ある
        - 引用符の中に特殊文字がない
          (特殊文字は &<>()@^| です)
        - 引用符の中に 1 つ以上のスペースがある
        - 引用符の中の文字列が、実行可能ファイルの名前である

    2.  最初の文字が引用符であるにも関わらず上の条件に一致しない場合は、最初
        の引用符とコマンド ラインの最後の引用符が削除され、最後の引用符の後
        のテキストが有効になります。
**後略**

/Kに与える引数として、引用符を与えると特殊な操作が発生します。
1の条件を満たすことは難しいため、2の処理を発生させた上で動くように、/K”COMMAND”のようにしてあげることとします。”start /d “%SUDO_PWD” /WAIT /B %SUDO_EXE% %SUDO_ARG%”となるようにすることで、最初と最後の”が取り除かれて動作することを狙っています。

自分用に作成したsudo.cmdの解説記事になりました。whywaita要素が全くありませんし、きっとwhywaitaには必要ない気がしますが、これを用意したことでmklinkを雑に呼べるようになったので重宝しています。ただし、呼び出したcmd.exeのPATHを引き継がないのでシステムの環境変数にないコマンドはフルパスで与える必要があります。改変しても良いのですが……そのためにはgotoとshiftを多用してSUDO_ARGの処理をする必要があるので妥協としています。

明日はなはれぽさんの”自営クラウドの意味とは”です。自宅環境もロマンがあるのでやってみたくはありますよね……業務でやりたいかは別ですが。

P.S.
ここまで書いたところでパス解決を含めた少し違うやり方に気がついて試していたのですが、あまりにも想定と異なる挙動をするので追記しないこととしました。うまいこと解決できれば埋まっていなさそうなmasawada Advent Calendar 2021にねじ込む……かもしれません。