PowerShellのShouldProcessを理解したい

VSCodeでPowerShellのモジュールとか書いてると、 PSScriptAnalyzer によるチェックが常時行われてるので普段は気にも留めないようなことも度々注意されるのよね

今回は SupportsShouldProcess のお話

続きを読む

広告

PSReadLineのHistory保存を制御

PSReadLineネタな

PSReadlineOptionAddToHistoryHandler というのがある
History 保存時に実行される ScriptBlock を指定できるぞ

PSReadLine はコマンドが実行される度に $(Get-PSReadlineOption).HistorySavePath に履歴を残すようになってます
PowerShellがもともと持ってる history とは関係ないんですね

で、場合によってはコマンド履歴に保存したくなーいなんてことがあるわけで

そこで AddToHistoryHandler の出番です

PS> help Set-PSReadlineOption -Parameter AddToHistoryHandler

-AddToHistoryHandler <Func[string,bool]>

    必須                         false
    位置                         名前付き
    パイプライン入力を許可する   false
    パラメーター セット名           OptionsSet
    エイリアス                      なし
    動的                     false

ヘルプを見ると型が [Func[string,bool]] になってるけどこれ、PowerShellでは Stringを受けてboolが返るScriptBlock でOKです
OKでした

ぼくの場合は直前に実行されたコマンドと同じものを実行した場合は保存しないといいなぁって常々思ってたのでそうしました
$PROFILE でもって指定しとくといいでしょう

# PSReadLine が入ってたら設定する
if ($(Get-Module -ListAvailable -Name PSReadLine))
{
    Set-PSReadlineOption -AddToHistoryHandler {
        param($Command)
        $LastCommand = $(cat $(Get-PSReadlineOption).HistorySavePath -Tail $([regex]::Matches($Command, '\n').Count + 1) -Encoding UTF8) -join ''
        return $LastCommand -ne ($Command -replace '\n','`')
    }
}

複数行コマンドにも対応したらなんだか複雑になっちゃったな
入力コマンドの行数分tailで読みだしてから各行を連結
履歴の複数行コマンドは行末に ` が入ってるので入力コマンドの改行を ` に置換
それらを比較して一致してなかったら履歴に保存、といった具合です

あと、最初に PSReadLine が入ってるかどうかも確認してます
Windows10未満だとデフォで入ってないですからね

Visual Studio CodeでPowerShellスクリプトを書こう!

バージョン0.10が出てベータ版になりましたー
という話っぽい Visual Studio Code さん
Extension機能(STでいうPackage)が追加されてより便利になったようで

で、その中に PowerShellExtension があるわけですよ
これはひょっとするとひょっとするぞ

Visual Studio Codeをインストール

PS> Find-Package visualstudiocode

Name               Version   Source       Summary
----               -------   ------       -------
VisualStudioCode   0.8.0.0   chocolatey   Visual Studio Code

chocolatey だとまだ0.8だったので、公式からダウンロードしてきましょうねぇ

ほい入れた

そして起動した

PowerShell Extensionをインストール

Ctrl+Shift+P を押せばSublime TextやAtomと同じくコマンドパレットが出てきます
install って入れて Extention: Install Extension を選んで、
続けて PowerShell って打ちかけてるとほら出てきた

早速追加するんだ

スクリプトを開いてみよう

適当なps1ファイルを作って、VSCodeで開いてみましょう

右下にPowerShellと出てればOK!
もしなってなくてもコマンドパレットから

  1. Change Language Mode
  2. PowerShell

としてやれば良いよ
あと、デフォルトだとファイルがUTF8で開かれるので、コマンドパレットから

  1. Change File Encoding
  2. Reopen with Encoding
  3. Japanese (Shift JIS)

したらいいと思います

スクリプトを書いてみよう

Intellisenseがいい感じに働いてるぞ

あと、常時PSScriptAnalyzerが働いてるようなので

波線が引かれた箇所にマウスカーソル乗せるとどう直したらいいかを教えてくれるよ
ただ、おそらく何でもかんでも波線引かれちゃうので物によっては無視(Invoke-ScriptAnalyzer-ExcludeRule)したいということもあるんだけど、それをどこで設定できるのかわかんないんだよね…

{
    //-------- PowerShell Configuration --------
    // Specifies the path to the PowerShell Editor Services host executable.
    "PowerShell.editorServicesHostPath": "../bin/Microsoft.PowerShell.EditorServices.Host.exe",
    // Launches the language service with the /waitForDebugger flag to force it to wait for a .NET debugger to attach before proceeding.
    "PowerShell.waitForDebugger": false,
    // Enables diagnostic logging for the extension.
    "PowerShell.enableLogging": true
}

いまのところPowerShellに関する設定はこの3つしかないみたい

ま、まだこのExtensionのバージョンは0.1.0だし今後色々できるようになるんじゃないかな!
VSCodeがさいきょうのPowerShellスクリプティング環境になるとイイネ

PSReadlineすげぇ

https://github.com/lzybkr/PSReadLine
すげぇ

PowerShell5.0だと最初から入ってる
それ以前なら PowerShellGet 導入してそっから入れてね、ですって

PowerShell5.0でカラフルになってたのはこいつのおかげらしい
あとこれ入ってるとなんか history の保存・復元を自前でやらんでいい感じがする
大変ありがたい

いろいろ便利なキーボードショートカットがあってやばい
Ctrl+Spaceで補完候補出すやつとかやばい

Ctrl+Spaceで補完候補出す

どんなショートカットがあるかは

Get-PSReadlineKeyHandler

でわかるよ

あとね、じぶんで割り当ての変更を好きなようにできるし
なんと自分で機能を作ることもできちゃうっぽい

Set-PSReadlineKeyHandler

を使う
Readmeにリンクされてる$PROFILEのサンプルで実際にどんな感じにするのかわかる
サンプル自体もかなり有用そうなのが多くてドキドキフワフワする

とりあえずカーソル位置の単語にクォートをかぶせてくれる(または取り除く)やつ
それとなんもないとこでクォートのキー打ったらクォートを2つ入力してその間をフォーカスするやつ
このふたつをパクってくっつけて Alt+'Alt+" でいい感じに動くようにした
なかなかゴキゲンだ
日本語キーボードじゃないとダメになってる気がするけどだいじょうぶぼくはこまらない

Set-PSReadlineKeyHandler -Key "Alt+'", 'Alt+"' `
                         -BriefDescription ToggleQuoteArgument `
                         -LongDescription "単語をクォートで括るまたはクォートを取り除く" `
                         -ScriptBlock {
    param($key, $arg)

    $Quote = switch ($key.Key)
    {
        D2 {'"'}
        D7 {"'"}
        default {return}
    }

    $ast    = $null
    $tokens = $null
    $errors = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$tokens, [ref]$errors, [ref]$cursor)

    $tokenToChange = $null
    foreach ($token in $tokens)
    {
        $extent = $token.Extent
        if ($extent.StartOffset -le $cursor -and $extent.EndOffset -ge $cursor)
        {
            $tokenToChange = $token

            # If the cursor is at the end (it's really 1 past the end) of the previous token,
            # we only want to change the previous token if there is no token under the cursor
            if ($extent.EndOffset -eq $cursor -and $foreach.MoveNext())
            {
                $nextToken = $foreach.Current
                if ($nextToken.Extent.StartOffset -eq $cursor)
                {
                    $tokenToChange = $nextToken
                }
            }
            break
        }
    }

    if ($tokenToChange -ne $null)
    {
        $extent = $tokenToChange.Extent
        $tokenText = $extent.Text
        if ($tokenText[0] -eq $Quote -and $tokenText[-1] -eq $Quote)
        {
            # Switch to no quotes
            $replacement = $tokenText.Substring(1, $tokenText.Length - 2)
        }
        else
        {
            # Add single quotes
            $replacement = $Quote + $tokenText + $Quote
        }

        [Microsoft.PowerShell.PSConsoleReadLine]::Replace(
            $extent.StartOffset,
            $tokenText.Length,
            $replacement)
    }
    else
    {
        $line   = $null
        $cursor = $null
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($Quote * 2)
        [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref] $line, [ref] $cursor)
        [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor - 1)
    }
}

細かいとこちゃんと理解してるわけじゃないので今後ちょいちょい勉強していこう
つまり [Microsoft.PowerShell.PSConsoleReadLine] ちゃんがキモなんだろう
これ使えばタブ補完だって好きにできちゃうんだぜ!

パイプされたSet-Locationは何をしているのか

先に結論言っとくと、なにやってんのかわかんない、です
なんにも解決しない
ひどい記事だ

PS> Get-Item HKLM:\SOFTWARE | Set-Location -PassThru

Path                                                           
----                                                           
Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE

これがね、よくわからない
なぜ Get-Item してパイプで渡すとPSPathに移動するのか
おまえは何をしているんだ

続きを読む

PowerShell小ネタ

ネタ単品だと記事にするほどじゃないんだけど書きとめときたいやつ
それなりに溜まったので投下するんじゃ

開いてるウィンドウのタイトルをPowerShellで列挙する

意外と面倒になる予感があったんだけど、Get-Processだけでどうにかなったよ

ps | ? MainWindowHandle -NE 0 | select MainWindowTitle

一行で書けちゃうね

アプリケーションを再起動

Windows10のThunderbirdがスリープ解除後にメールを受信しなくなるっぽいのでさくっと再起動させたかったのね

ps thunderbird* | % {
    $_.CloseMainWindow()
    & $_.Path
}

kill じゃなくちゃんと終了させたかったら CloseMainWindow() すればいいらしい
こんなメソッドあったのね
あとはexe叩きなおせばOK

Hashtableからキャスト

New-Object psobject するより Hashtable から pscustomobject にキャストしたほうが早いらしい

PS> $hashtable = @{Foo = 123; Bar = 'abc'; Baz = $true}
PS> New-Object psobject -Property $hashtable
PS> [pscustomobject] $hashtable

Measure-Command したら確かに若干差がある
まぁこれは好きにすればいいんじゃないって感じなんだけど
Hashtable をキャストするって発想がなかったのでちょっと面白いなって

もしかしてメンバの型が合ってれば任意の型にキャスト出来るんじゃないかな?
やってみよう

PS> class Hoge
{
    [int]    $Foo
    [string] $Bar
    [bool]   $Baz
}

PS> [Hoge] $hashtable

Foo Bar  Baz
--- ---  ---
123 abc True

PS> ([Hoge] $hashtable).GetType()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True     False    Hoge System.Object

ちゃんと Hoge 型になった!

zip圧縮の罠

Zipme
├Folder
│└File2.txt
└File1.txt

こんなふうにファイルが配置されてるとしてだね
ls から Compress-Archive にパイプしたい場合
フォルダあるから -Recurse したほうが良かろうとか、思うじゃん

ls .\Zipme\ -Recurse | Compress-Archive -DestinationPath Zipme.zip

結果

Zipme.zip
├Folder
│└File2.txt
├File1.txt
└File2.txt  <- お、お前ー!

ぎゃーし!

ls .\Zipme\ | Compress-Archive -DestinationPath Zipme.zip

-Recurse しなくても良かったようです

通知を出す

Windows10になって通知がアクションセンターに残るようになったし、ジョブの状況とか通知投げられたら素敵じゃん?
なんかやり方あるんだろうなーって探してたらそのものずばりっぽいモジュールがあった

導入は PackageManagement から、楽々です

PS> Find-Package -ProviderName PSModule -Name BurntToast | Install-Package

じゃあやってみようぜ

PS> New-BurntToastNotification "( ‘(I¥‘)" 'みなもちゃんかわいい!'
通知

通知

アクションセンター

アクションセンター

やったー!

と思ったけどこれ通知消えるとアクションセンターからも消える
アクションセンターからは消えないでほしいんだけど、そういう感じのパラメータない…ないね!
あとでちゃんと調べよう

別プロセスのシェルに入る

なんのこっちゃって思うじゃん、思った

PowerShellをふたつ立ち上げとこう、そんでだ

# PowerShell(2)
PS> $PID
12345

別のシェルのPIDを調べとこうね
12345 だったとします

メインで使う方のシェルは昇格しとく必要がある

# PowerShell(1) (管理者シェル)
PS> $PID
13579 # PIDは当然異なるよね
PS> Enter-PSHostProcess -Id 12345
[プロセス:12345]: PS C:\Users\stuncloud\Documents>
# ↑プロンプトが変わるぞ
[プロセス:12345]: PS C:\Users\stuncloud\Documents> $PID
12345 # 別のシェルのPIDになってますよ!!
[プロセス:12345]: PS C:\Users\stuncloud\Documents> Exit-PSHostProcess # 抜ける
PS> $PID
13579 # 戻ってきた

…なにこれ何に使うの!!
よくわからないけどすごいいりょくをひめているきがする!

( ‘(I¥‘)) …

と、これを書いた後に実際に仕事中使ってみる機会があったんだけど
なんと別のユーザーのプロセスでも構わず入っていけるぞすごい
具体的には

  1. リモートPCにユーザーXでローカルログオンして、PowerShellを起動しておく
  2. ユーザーXのPowerShellのPIDを調べる
  3. リモートPCにユーザーYの資格情報を使って Enter-PSSession する
  4. Enter-PSHostProcess -Id (ユーザーXのPowerShellのPID) する

で、リモートシェルからローカルで動いてるPowerShellに侵入?してしまったぞ
これで例えば cd HKCU:\ とかするとユーザーYではなくローカルログオンしているユーザーXのレジストリにアクセス出来ちゃう
すげーなおい…なんか悪さ出来そうですね!
ちなみに

PS> Start-Process -FilePath powershell -Credential ユーザーX

はエラーになってダメでした、まぁそりゃそうだよね
あ、もしかしてタスクスケジューラ使えばあるいは…?
しかしなるほどPowerShellを非表示で起動出来ないとかこれ考えるとなんか、うん

いやでもログオン中のユーザーの HKCU:\ 見たいーとか Cert:\CurrentUser 見たいーとかままあるのでこれはとても便利だ

PowerShellから対話形式でUWSCを動かすモジュールできた

随分前にUWSCを対話形式で操作できるのを作ったんだけど

UWSCのコマンドラインインタプリタ | たっぷす庵

これはUWSCが自前でCUI作ってたんですね
でもさーやっぱさー普段使ってるシェルからやりたいじゃーんて思うよね?
うん

そして今は空前のPowerShel時代です
じゃあPowerShelでやろう

やった
できた

https://github.com/stuncloud/PoshUWSC

やったー!

続きを読む