tech.guitarrapc.cóm

Technical updates

PowerShell の -PipelineVariable を使おう

PowerShell v5 で追加された PipelineVariable に関して、本では説明していたのですがブログに書いていませんでした。 いい感じの例があったので、説明しておこうと思います

目次

TL;DR

  • PipelineVariable を使うと、パイプラインの中で一度 $x = $_ と書いて変数を保持していた処理が不要になります
  • Sort-Object のような集計系のCmdlet を使うとパイプラインの後続にわたる値の挙動が変わるので注意しましょう
  • Az モジュール使いにくい

対象のPowerShellコード

いい感じの Pipeline Variable の例があります。

この例を通して PipelineVariable を見てみましょう。

PipelineVariable とは

このコードの | % -pv vnet { $_ } は Pipeline Variable を使っています。

Pipeline Variable というのは、パイプラインからオブジェクトを次のパイプラインに送出するときに、そのオブジェクトを指定した変数に保持する機能です。

公式ではこう言っています。

新しい共通パラメーター PipelineVariable が追加されました。 PipelineVariable を使用すると、パイプされたコマンド (またはパイプされたコマンドの一部) の結果を変数として保存し、パイプラインの残りの部分に引き渡すことができます。

docs.microsoft.com

そのパイプライン時点のオブジェクトを変数に保持して何が嬉しいかというと、パイプラインがつながっている限り、直後ではない後続のパイプラインでもそのオブジェクトを変数経由で利用できます。 どういうことでしょうか?

PowerShellでは、通常パイプライン直後のスクリプトブロックでは流れてきたオブジェクトを 自動変数$_ 経由で読めます。

PS > ps | %{$_} | Get-Member


   TypeName: System.Diagnostics.Process

しかし、その次のパイプラインのスクリプトブロックでは、$_ の中身は前のパイプラインの中身に変わります。 例えば次のように、%{$_.Name} とProcess型のName プロパティ出力すると、後続のパイプライン | %{$_} では Process型ではなく String型に 変わります。

PS> ps | %{$_.Name} | %{$_} | Get-Member

   TypeName: System.String

PipelineVariable を使わないと、一連のPipelineの中であるパイプラインにおける変数を保持して後続に渡すときにハッシュテーブルや適当な入れ物に入れて渡すなど手間がかかります。

# こんなことはしたくない!
PS> ps | %{@{ps=$_;name=$_.Name}} | %{$_.ps} | Get-Member

PipelineVariables を使う例

↑の例以外にも、パイプラインの中でさらにパイプラインを書く時に、一番初めにパイプラインに入った$_ を思うように取れなくて一瞬引っ掛かったという人は多いのではないでしょうか?

何もしない無駄にそれっぽいのを例にします。 末尾の %{$_} では Process型が欲しいのですが、当然 String が来ます。

PS> ps | %{$_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$_}}
pwhs
pwhs

この時にPipelineVariable を使わない場合、一時変数に入れてからやることが多いと思います。

PS> ps | %{$ps=$_; $_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$ps}}

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
    149   404.51     462.63      54.20    6820   1 pwsh
     71    55.12      96.09       1.95   13816   1 pwsh

PipelineVarible はこういった「パイプラインの後続で今のコンテキスト ($_) を使いたい」というシーンで機能します。 PipelineVarible に置き換えてみましょう。

PS> ps -pv ps | %{$_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$ps}}

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
    149   404.51     462.63      54.20    6820   1 pwsh
     71    55.12      96.09       1.95   13816   1 pwsh

%{$ps=$_;} と書いていた処理を、パイプラインを開始する前の ps コマンド時点に -pv ps と持ってきました。 このように、「パイプラインの中で $_ をいちいち変数に受け取っていた」という人は結構楽に書けるようになるはずです。

Aggregateする処理ではPipelineVariables の利用を気を付ける

Sort-Object のようにパイプラインをせき止める Aggregation 系の処理では、後段のパイプラインの結果は前段と変わります。 例えば先ほどのコードに意図的に Sort-Object を入れるとわかります。

PS> ps -pv ps | sort | %{$_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$ps}}

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
     31    29.86       9.57       0.42    9476   1 YourPhone
     31    29.86       9.57       0.42    9476   1 YourPhone

pwsh を拾っているはずなのに、YourPhone というプロセスに変わってしまいました。 YourPhone Sort-Object から渡った最後のオブジェクトに相当します。

PS> ps | sort | select -Last 1

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
     31    29.86       9.57       0.42    9476   1 YourPhone

では、Sort-Object のようなパイプラインを一度せき止めるCmdletを挟みたい場合、どうすればいいのでしょうか? 簡単です、Sort-Object で PipelineVariable に割り当ててください。

PS> ps | sort -pv ps | %{$_ | Where Name -eq "pwsh" | %{$_.Name.Replace("sh","hs")} | %{$ps}}

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
    148   403.27     461.28      59.45    6820   1 pwsh
     71    55.39      96.36       2.00   13816   1 pwsh

元コードからみる PipelineVariable

さぁ、PipelineVariable については概ね理解できたと思います。 元のコードを再度提示してみてみましょう。

AzVirtualNetwork | % -pv vnet { $_ } | % { $_.subnets } | select @{l="vnet";e={$vnet.name}},@{l="snet";e={$_.name}},addressprefix | sort vnet, snet

このコードの PipelineVariable は、先ほどの ps と違って一度 %{$_} を介しているように見えます。 どういうことか見てみましょう。

-pv vnet は早められる

もし、PipelineVariables を使うときに自分が % -pv vnet {$_} のように、ただ後続に $_ を流すだけの処理で PipelineVariablesを使うように書いていたら、その-pv、前段のコマンド時点に持っていくことができます。

AzVirtualNetwork -pv vnet | % { $_.subnets } | select @{l="vnet";e={$vnet.name}},@{l="snet";e={$_.name}},addressprefix | sort vnet, snet

元コードでは %{$_} でパイプラインを通るたびに新しく $vnet$_ を割り当てているのですが、それは AzVirtualNetwork | 時点でやっています。 ps の例で見せたように一番初めの Cmdletを実行した結果はパイプラインを通るのですが、その時点で PipelineVariable としてキャプチャできます。

単純な % {$_} をやるようなパイプラインが減るのは可読性、速度面の両面から嬉しいので検討するといいでしょう。

Select @{l={};v={}} をやめる

PipelineVariable とは関係ありませんが、Select @{l={};v={}} を使って PSObjectを生成している部分があります。

PSObject の生成方法としては、Select @{l={};v={}} 以外にも [PSCustomObject]@{} があります。1

Select-Object を使ったPSObject生成のメリットは、「プロパティの合成ができる」ことです。プロパティの合成は、ハッシュテーブルの後に書かれている addressprefix がそれにあたります。

select @{l="vnet";e={$vnet.name}},@{l="snet";e={$_.name}},addressprefix

一方で、[PSCustomObject]@{} を使うとハッシュテーブルから直接PSObjectに型変換します。 このやり方ではプロパティの合成はできず、自分で全プロパティに関してハッシュテーブルを定義しないといけません。 とはいえ、le のようなマジックキーに比べるとシンプルで速度も速く、可読性は高いでしょう。

% {[PSCustomObject]@{vnet=$vnet.name;snet=$_.name;addressprefix=$_.addressprefix}}

初めのコードをPSCustomObject に切り替えるとこうなります。

AzVirtualNetwork | % -pv vnet { $_ } | % { $_.subnets } | % {[PSCustomObject]@{vnet=$vnet.name;snet=$_.name;addr=$_.addressprefix}} | sort vnet, snet

まとめる

「-pv を早める」、「PSCustomObject に切り替える」の2つを組み合わせてみましょう。

AzVirtualNetwork -pv vnet | % { $_.subnets } | % {[PSCustomObject]@{vnet=$vnet.name;snet=$_.name;addr=$_.addressprefix}} | sort vnet, snet

幾分ワンライナー長い!読みたくない!感は減りました。

Azure環境の事前準備

もしコードを試す場合、Azureに VNet と Subnet が必要です。 せっかくなので、Az モジュールでサクッと組んでみましょう。

VNet作るだけなら料金かからないですしね。

AzureRm をアンインストールする

おわこん!それなのに Visual Studio で勝手に入る。

foreach ($module in (Get-Module -ListAvailable AzureRM*).Name |Get-Unique) {
   write-host "Removing Module $module"
   Uninstall-module $module
}

docs.microsoft.com

Az のインストール

代わりに Az モジュールを入れます。

Install-Module -Name Az -AllowClobber -Scope CurrentUser

docs.microsoft.com

vnet とかの準備

あとは Azure環境に ReousrceGroup、VirtualNetwork、Subnetを作ります。

$location = 'Japan East'
$rg = New-AzResourceGroup -Name test -Location 'Japan East'
$vnet = New-AzVirtualNetwork -Name test -ResourceGroupName $rg.ResourceGroupName -Location 'Japan East'  -AddressPrefix 10.0.0.0/16
$subnet = Add-AzVirtualNetworkSubnetConfig -Name a -VirtualNetwork $vnet -AddressPrefix 10.0.0.0/24
$vnet | Set-AzVirtualNetwork

これでコードを試せます。

AzVirtualNetwork | % -pv vnet { $_ } | % { $_.subnets } | select @{l="vnet";e={$vnet.name}},@{l="snet";e={$_.name}},addressprefix | sort vnet, snet

後片付け

リソースグループごとさくっと消せば全部消えます。

Remove-AzVirtualNetwork -Name $rg.Name

蛇足

Add-AzVirtualNetworkSubnetConfig で$vnet のプロパティを変更して (out相当の処理!?)、$vnet | Set-AzVirtualNetwork でVirtualNetwork に変更を適用しているの、とても書きにくいやり方ですね。

PowerShell っぽくないというか Azure に見られる特有に感じるのですが... 気のせいでしょうか。 Az モジュール、コマンドも探しにくく、Cmdletから使い方が予想できない使い方になってて、PowerShell の書く経験としては最悪に感じます。すごい、悲しい。

PowerShell 的には Addを使うと対象のオブジェクトに追加されることが多いので、それを期待している人は多いでしょう。Add-Content とか。

今回の場合、Add-AzVirtualNetworkSubnetConfig というCmdlet実行時点で $vnet に割り当てがされるのを期待するような気がほんわりします。(しない感じもある)

Add-AzVirtualNetworkSubnetConfig -Name a -VirtualNetwork $vnet -AddressPrefix 10.0.0.0/24

あるいは、VirtualNetworkSubnetConfig を作って、VirtualNetworkにAddするとか。(こっちのほうが納得感とCmdletからの予測ができそう)

$subnet = New-AzVirtualNetworkSubnetConfig  -Name a -VirtualNetwork $vnet -AddressPrefix 10.0.0.0/24 #実際には-VirttualNetwork パラメーターはない
Set-AzVirutalNetwork -SubnetConfig $subnet # こんなCmdlet もない

リソースを逐次分離したくてこうなったと思うのですが使い勝手が悪いのは Az Module でも改善されてないのでした。 az cli のほうが使いやすいので、私はもっぱら az cli です。

なお、AWS の Cmdlet 設計は秀逸で、どのCmdlet も aws cli と比較してもわかりやすい印象があります。 ただ、やはり型の扱いは若干めんどうさが表に出ていますが。


  1. ほかにもAdd-Member などいくつか方法がありますが本題ではないので省略します。

Windows開発環境の構築をChocolateyからscoopに切り替える

この記事は、PowerShell Advent Calendar 2019の一日目です。

qiita.com

Windows のパッケージマネージャーの裏はPowerShellが多く使われています。

そんなWindows におけるパッケージマネージャーと言えば、Package Management Chocolatey が有名なのではないでしょうか?

私もChocolatey をパッケージマネージャーに用いて開発環境の構築をしてきましたが、課題が多かったため Scoopに切り替えました。

Chocolatey で何が問題だったのか、なぜscoop を選んだのか、この一年 scoop をどのように環境構築に利用しているのかを紹介したいと思います。

目次

TL;DR

Chocolatey から scoop に切り替えることで開発環境の自動化、ほとんどのアプリケーションをユーザー権限でのインストールに抑え込むことが可能になりました。

scoop に切り替えて得られたものをざくっと紹介します。

  • UACの不要なインストール
  • 簡単なアップグレード
  • 常に最新環境への追随
  • 確実なアンインストール
  • アプリケーションのパス均一化
  • ローカル開発環境構築の自動化

将来的にmsix が広く使われるようになったら、Scoopより利用しやい仕組みになるとうれしいです。

Chocolatey の利用

Scoop に切り替えるまでChocolatey を使っていました。長年利用して感じていた課題を見てみます。

どのように Chocolatey を利用していたのか

2014年にDSCを自動化しようとしていることから、Chocolateyとは5年の付き合いです。2018年までは個人、会社の開発環境構築にChocolatey を使っていました。

Chocolatey は、Windows環境でのパッケージリポジトリの提供とCLI インターフェース choco を通して、Windows においてコマンドラインからアプリケーションをインストールする体験を提供します。

chocolatey.org

最近聞かなくなりましたが、Package Management のバックエンドとすることで「リポジトリの一つとみなして利用」したり、「独自リポジトリにChocolateyフォーマットのnugetパッケージを置いてチームのプライベートリポジトリとする」こともできます。

前職においては、Unityのリリースを検知、自動的にNuGetパッケージを生成、社内NuGetサーバーに配置、Package Managementを使ってChocolateyパッケージをインストール可能な状態にしていました。こうすることで、Unityのインストールパスをチームで共通化し、Unityインストールの最大の敵であるダウンロードの遅さ、パッケージの分かりにくさに対応していました。

今なら Unity Hub があるので不要です。(当時は Unity Hubがなかった)

Chocolatey で困ること

Chocolatey で開発環境を構築したものの困ることがあります。アンインストールとUACです。

アンインストール

Chocolatey は、MSI や exe 形式のインストールをchocolateyinstall.ps1で扱い、アンインストールをchocolateyuninstall.ps1 で扱います。しかしその実体は地道にインストーラをたたいており、実行の待ち受けやパスで失敗したり、インストールで行っていたWindows Serviceやレジストリは消し忘れていたりします。また、そもそも多くのパッケージでアンインストールスクリプトが提供されていません。

Chocolatey でも最大規模のインストールパッケージである、git.install のアンインストール処理でもこんな感じです。

chocolatey.org

gist.github.com

Chocolateyを構築時だけに使うという割り切りならいいのですが、開発環境はどんどん変わっていきます。当然アンインストールをする機会も少なくないでしょう。アンインストールが不安定だったり、未提供だとインストールとは非対称に以前のWindowsでのアプリケーション管理をすることになります。

使ってきた経験から言えるのは、残念ながらChocolatey でインストールしたものであってもWindowsのApps & Feature でアンインストールが必要なものがほとんどです。

私はChocolateyで今後もやっていくという判断ができませんでした。アンインストールはスパッと終わらせたいです。

UAC

UAC はWindowsでコマンドラインを使っていて感じるだるいと感じる筆頭かもしれません。Chocolatey でも残念ながらUACを求めてくるパッケージは多く存在します。 自動化するためのコマンドラインでchocolateyを使ってアプリケーションインストールの実行、席を立って戻ってみるとUACで止まっていた。という経験をお持ちの方もいるのではないでしょうか?

そもそもchoco installなどをするときに管理者に昇格したcmdやPowerShellを使うように言われます。

UACにはサンドボックスとしての安全さの提供という側面はあるものの、コマンドラインで作業していていきなりUACが表示されマウスクリックが必要だったりするのはストレスです。特にこれはLinux/macOS でコマンドラインによる操作に慣れていると顕著に感じるかもしれません。*1

Chocolatey が抱える問題点

Chocolatey が抱えている体験上の問題は2つに起因しているように思います。

  • アンインストールの不完全な提供
  • UACポップアップ

アンインストールに関しては、Chocolatey自体が MSI や exe といった各型式の実行ファイルを開発者が提供するスクリプトでなんとかする方式である以上避けられないように思います。Chocolatey 自身もValidation Program やChocolatey Install/uninstall/WebFile など各種サポート関数を提供していますが、たとえこれを使っても、Windows において回避するのは難しい側面があるのは否めません。

UAC も、MSIを使う以上シカタナイでしょう。%ProgramFiles% は特権ユーザーでないとアクセスできないので。しかし可能であればアプリケーションインストールごときでUAC昇格はしたくないですし、環境によっては厄介に感じます。

Chocolatey つかれました。

Scoop への切り替え

Scoop は Chocolatey とはコンセプトを別にするコマンドラインインストーラーです。

Looking for familiar Unix tools? Tired of Powershell’s Verb-Noun verbosity? Scoop helps you get the programs you need, with a minimal amount of point-and-clicking.

scoop.sh

Homebrew 的に、常に最新のアプリやUnix Toolを、UACの縛りなく、さくっと利用できるとうれしい。そういう使い方を想定されています、まさに私にはぴったりでした。

Scoop installs programs to your home directory by default. So you don’t need admin permissions to install programs, and you won’t see UAC popups every time you need to add or remove a program.

Scoopのインストール

PowerShell からワンライナーでインストールできます。

Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')

# or shorter
iwr -useb get.scoop.sh | iex

使い方は scoop help でわかります。

$ scoop help

Usage: scoop <command> [<args>]

Some useful commands are:

alias       Manage scoop aliases
bucket      Manage Scoop buckets
cache       Show or clear the download cache
checkup     Check for potential problems
cleanup     Cleanup apps by removing old versions
config      Get or set configuration values
create      Create a custom app manifest
depends     List dependencies for an app
export      Exports (an importable) list of installed apps
help        Show help for a command
hold        Hold an app to disable updates
home        Opens the app homepage
info        Display information about an app
install     Install apps
list        List installed apps
prefix      Returns the path to the specified app
reset       Reset an app to resolve conflicts
search      Search available apps
status      Show status and check for new app versions
unhold      Unhold an app to enable updates
uninstall   Uninstall an app
update      Update apps, or Scoop itself
virustotal  Look for app's hash on virustotal.com
which       Locate a shim/executable (similar to 'which' on Linux)


Type 'scoop help <command>' to get help for a specific command.

Scoop でアプリを利用する

sudo や git など、Windows にデフォルトで入ってほしいけど入っていないCLIツールは、まさに Scoop が向いています。

scoop install git

まとめてインストールすることもできます。

scoop install 7zip sudo git jq time unzip openssl

インストールしたアプリは、jq などアプリ名でそのまま利用できます。

$ jq --version
jq-1.6

インストールされているアプリケーションも一覧表示できます。

scoop list

アンインストールも簡単です。 プロセスがロックされていない限りは確実にアンインストールできるので安心できます。*2

scoop uninstall git

Scoop のアプリを検索する

利用可能なアプリは、scoop search で探すことができます。

GitHub にまとまっているので、そこを見て探してもいいでしょう。

github.com

Scoop のアプリをよりたくさんインストールする

Scoop は、アプリのダウンロード元に GitHub を利用しており これを Bucket と呼んでいます。 インストール直後は Main Bucketのみがインストールされています。

scoop bucket list

Bucket はscoop bucket add で追加できます。 Bucketを追加することで多くのアプリケーションをscoopでインストールできるようになります。 例えば、公式で提供されているExtra Bucket は、 Main Bucket にはないけどよく利用されるアプリケーションが含まれています。

scoop bucket add extras

extras Bucketの追加で gitkraken などもscoopでインストール可能になります。

scoop install gitkraken

インストールするときに、Bucket を気にしなくていいのでかなり楽です。

scoop にしたことで混乱したこと

scoopの特性を把握せずにいたときに混乱したこともあります。

  • 複数のパスに実行ファイルが存在することによる意図しない動作
  • グローバルパスに存在が期待されるソフトウェアの継続的な更新の困難さ

例えば、.NET Core SDK は Visual Studio でも入ったりし、あらゆるツールが利用しているため scoop でインストールすることは向きません。

「何をscoopで入れて、何を入れない。」という選択を自分で行う必要があるので、そういう意味では誰にでもオススメできるわけではありません。 自分で選んだ結果を自分で受け入れ、対応していける人向けだと思います。

Scoop で過去のバージョンのアプリをインストールする

Scoop は .env のような制御はないので素直に使うなら、常に最新を使うのが楽です。(scoop install / scoop update していくだけ)

もし過去バージョンを利用する場合、scoop reset で可能です。

scoop reset <app>@<version>

github.com

もし過去バージョンがたまっているversions Bucket にアプリがあるならこれを使うのも手でしょう。

scoop bucket add versions

github.com

私は過去バージョンを Scoop で利用することはしません。するぐらいならScoopやめる。

Scoop を継続的に使っていくための工夫

scoop でパッケージがインストールができるといっても、アプリをインストールするコマンドをずっと書き連ね、そのコマンドをスクリプトにしたりするのはつらいものがあります。

scoop install xxx
scoop install yyy
scoop install xxx

やりたいのは、アプリのインストールで scoopがいい感じにしてくれればなんでもいいのです。 ということで、こんなYAMLを書くことで Ansibles like に Scoop のアプリをインストール/アンインストールできる仕組みがほしいです。

- name: "Install linux tools"
  scoop_install:
    state: present
    bucket: main
    name:
      - cmake
      - gcc
      - gow
      - jq
      - sudo
      - time
      - unzip
      - openssl
      - apache

- name: "Install windows tools"
  scoop_install:
    state: present
    bucket: main
    name:
      - 7zip
      - aws
      - bat
      - chromedriver
      - git
      - git-lfs
      - ngrok
      - pwsh
      - terraform
      - gibo
      - helm
      - mysql-workbench

YAMLでの定義を実現するためのPowrShell モジュール ScoopPlaybook を作ったので次回紹介しようかと思います。

github.com

TIPS: Scoop のアプリ実体

余り意識する必要がないのですが、興味のある人向けです。

scoop でインストールされたアプリは、基本的に ~\scoop\shims\アプリ名.EXE のパスに存在します。shims でわかる通り、これらは ~/scoop/apps/アプリ名/current を参照しており、アプリケーションのインストールと利用が分離されています。

f:id:guitarrapc_tech:20191201224739p:plain
scoopのshimsフォルダー

apps のアプリディレクトリを見ると、バージョンフォルダと current が存在し、current は常に最新バージョンのシンボリックリンクになっています。

f:id:guitarrapc_tech:20191201224641p:plain
shims が参照しているappsのディレクトリ

まとめ

Chocolatey はインストールの一回に限れば便利です。が、今後もモデル的にアンインストールやUACが改善するには msix が来ない限り難しいでしょう。 Scoop を使うと、Unix Toolやsれ以外のツールの多くもいい感じでインストール、アンインストールできます。

私もScoopの独自Bucketでフォントとかいくつかパッケージを公開しているので、独自のアプリケーションを Scoop で配布することもいずれ記事にします。

*1:私はGUI操作時よりコマンドライン操作時の方がUACをストレスに感じます

*2:Windowsなのでプロセスが実行中はファイルがロックされます

PowerShell で1から100までの偶数の和を求めるワンライナー

PowerShell でどういうやり方がいいかを少し考えてみます。

「1から100の偶数の和を求めるワンライナー」まとめ - Qiita

というのがあり、Twitter でつぶやいたのですが、一応まとめておきます。

目次

TL;DR

リスト作ってもいいなら、メソッド方式で。作りたくないなら パイプラインで。 bitwise やシフト演算が最速と思いきや、普通に% (剰余) がいいです。

PowerShell でSIMD 活用ってどうやるといいのかが気になります。

算出方法

どれでもどうぞ。

gist.github.com

いずれも求められますが、大きな違いは2つです。(8つあるのは、2 x 4通りです)

  • フィルター方法がパイプライン or メソッド
  • 偶数の算出が 剰余(modulo) or 除算(division) or ビット演算(bitwise) or シフト演算(shift)

シーケンスの生成

フィルター方法の選択でメモリと実行速度に違いがでます。

  • パイプライン | を使うことで、1~100のメモリ域を確保しないので使用メモリが減る一方で、実行速度は落ちます。
  • メソッド(シーケンス).Where{}を使うことで、1~100のメモリ域を確保するため使用メモリが増える一方で、実行速度は上がります。

リスト作る必要ないならパイプラインがいいですね。

算出方法とベンチマーク

偶数の算出は、どれを選ぶかで実行速度が違いが出ます。

  • modulo

PowerShellでも算術演算子 % を使えます。奇数は剰余が1、偶数は0です。 よく書きます。

  • bitwise

8ビットで考えます。奇数は20 が常に1なので 1とand(論理積)を演算すれば常に1になります。偶数なら0です。 こっちのほうが早い時には使います。

00000001    
00000001   (00000001 is 1)
       &
--------
00000001

C系の(x & 1) == 0 をPowerShellに翻訳すると ($_ -band 1) -eq 0 になります。

  • division

残術演算子 / を使って2で割って、intで小数点を破棄してかけなおすと元に戻るかです。 明らかに無駄なので普段書きません。

  • shift

偶数の1ビット目が0であるため、右シフトして桁を落として左シフトで0を入れた時に元の値になれば偶数、そうでなく1少なくなれば奇数です。

00000011 (3)
00000001 (>> 1)
00000010 (<< 1)
--------
00000010 (2)

C系の ((3 >> 1) << 1) == 3 をPowerShellに翻訳すると (3 -shr 1) -shl 1 -eq 3 となります。

速度を見てみましょう。計算回数が少なければ早いので、オペレータのコストとJITでの最適化がかかるかがポイントです。

PowerShell は一回一回のベンチマークのずれが激しいので、10000回実行した算術平均をとって一回当たりの実行速度を見てみます。*1

BenchMark(Method) Times Avg(ms)
bitwise 1000 0.542
division 1000 0.521
modulo 1000 0.489
shift 1000 0.520
Benchmark(Pipeline) Times Avg(ms)
bitwise 1000 1.353
division 1000 1.486
modulo 1000 1.330
shift 1000 1.359

gist.github.com

余談 : クラス構文

PowerShellでは、同じ処理でもクラス構文にすると、dllからの呼び出しになるため高速化する傾向にあります。

といっても、偶数判定だけクラス構文にすると遅くなります。

Benchmark(Method) Times Avg(ms)
shift 1000 1.287
class 1000 1.088

gist.github.com

全体をクラス構文にして、インスタンスメソッド、スタティックメソッドでどうなるか見てみると早くなっていないことがわかります。

Benchmark(Class) Times Avg(ms)
bitwise 1000 0.536
division 1000 0.562
modulo 1000 0.533
shift 1000 0.559
Benchmark(Static) Times Avg(ms)
bitwise 1000 0.538
division 1000 0.553
modulo 1000 0.521
shift 1000 0.544

gist.github.com

この程度だと速度差つかないですね。(PowerShell 5.1 / 6.2)

*1:これでも差が出るのでウォームアアップがあるとよりいいですね

Windows10 のPowerShellでF7を押しても履歴が画面に表示されない

以前書いたPowerShell の履歴に関する操作ですが、Windows 10 のWindows PowerShell ではF7による画面表示がされません。

tech.guitarrapc.com

これに関して少し見てみましょう。

目次

TL;DR

PSReadLine を外すか、代替コマンドを使いましょう。 PowerShell Core では代替コマンド使えないのであきらめで。

Windows 10 と Windows PowerShell と PSReadline

Windows 10 でもコマンドプロンプトではF7 で実行履歴が見えます。

f:id:guitarrapc_tech:20190531222503p:plain
コマンドプロンプトでF7を押して実行履歴を表示する

一方でWindows 10 の Windows PowerShell でF7で実行履歴が見えないのは、 PSReadline がデフォルトで有効になったためです。 なお、もちろん PowerShell Core (6.1) でもF7で実行履歴は表示されません。

従来のようにF7で実行履歴画面を出したい

PSReadLineモジュールを外すことで表示されます。 が、今のPowerShell でPSReadlineを外すのは副作用が大きいためちょっと悩みどころです。

Remove-Module -Name PSReadLine

f:id:guitarrapc_tech:20190531222642p:plain
PSReadline モジュールを外すとF7が利用可能になる

これは Windows PowerShell / PowerShell Core 両方で有効です。(そりゃぁそうですね)

PSReadline で F7を使ってGridViewによる代替表示を行う

PSReadline は、特定のキー入力に対してハンドラーを登録できます。

例えば、F7 を押すと Windows PowerShell で使える Out-GridView 表示してみましょう。

gist.github.com

この関数を Windows PowerShell で実行してから F7を押すと、実行ヒストリが最新の入力から降順にすべて表示されます。*1

f:id:guitarrapc_tech:20190531224340p:plain
Out-GridView を使った代替表示

なお、関数の$history | Sort-Object -Descending | Out-GridView -Title History -PassThru$history | Out-GridView -Title History -PassThru にすると古い履歴から昇順になります。 好きなほうでどうぞ。

PowerShell Core は、マルチプラットフォームで表示する View に相当するコマンドレットがないのでいったんなしで。

Ref

基本的にここの通りです。

Making the Command-History Pop-Up work via F7 in Windows 10 Powershell - Stack Overflow

*1:ここ最近 Vue.js ばかりやっているのがばれる....

PowerShell でsudo っぽいものを内蔵した関数を作る

時々思い出したようにPowerShell の記事を書いてみます。

スクリプトでよくあるのが、sudo で実行時に権限があるスクリプトの許可をしたいというケースです。

Windows は組み込みsudo がないので面倒でしたが、現状なら scoop で sudo をインストールするといいと思います。

scoop.sh

scoop install sudo

これで sudo ./your_script.ps1 とできるので特権が必要なときに、必要な権限を渡すことができます。

さて今回の記事は、Windows において実行中のスクリプトや関数が特権が必要な場合に、sudo を使わずにUACダイアログを出して昇格したPowerShellで同関数を実行し直してほしいというケースです。

通常の特権昇格フロー + Windows Diffender操作のため利用には注意してください。

この2つが自動化できるのは運用上は非常に便利ですが、誤った利用は技術を良くない方向に追い込みます。 チーム内での潤滑な運用での利用に留めることを推奨します。

目次

TL;DR

悪用禁止だけど、チーム内で使うには便利です。 特に、PowerShell のExecution Policy やダブルクリック問題はだいたいこれで解決するのが定番です。(chocolatey や scoop もこの手法)

サンプル

今回は、Windows Defender によってdotnet のビルドが遅いので除外して対応しようという記事を使ってやってみます。

baba-s.hatenablog.com

サンプルスクリプトを置いておきます。

PowerShellUtil/WindowsDefender at master · guitarrapc/PowerShellUtil · GitHub

Windows PowerShell で次のコマンドを実行すると、Windows Defender のスキャン除外パスに指定したパス + Visual Studio のパスが入り、除外プロセスにVisual Studio や dotnet、msbuildが入ります。

実行前に十分に気をつけてください

iex  (new-object net.webclient).downloadstring('https://raw.githubusercontent.com/guitarrapc/PowerShellUtil/master/WindowsDefender/remote_exec.ps1')

Windows PowerShell を特権のない状態で起動して実行すると、昇格するか聞かれます。

f:id:guitarrapc_tech:20190508140824p:plain
UAC昇格が聞かれる

y を押した時だけ、UACダイアログがでて関数がそこで再実行されます。

f:id:guitarrapc_tech:20190508140916p:plain
特権に昇格されたPowerShellで関数が再実行される

f:id:guitarrapc_tech:20190508140943p:plain
実行結果

仕組み

処理の本体は、Add-DefenderExclusionForDevEnv です。

PowerShellUtil/Add-DefenderExclusionForDevEnv.ps1 at master · guitarrapc/PowerShellUtil · GitHub

キーはここです。

gist.github.com

自分が特権で実行されているかは、これで検出できます。

$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (!$currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {

自分の関数名を、自動変数 $MyInvocation から取ります。

$me = $MyInvocation.MyCommand

自分の関数の定義を、Get-Command から取ります。

$myDefinition = (Get-Command $me).Definition

これで関数文字列が生成できます。

$myfunction = "function $me { $myDefinition }"

新規プロセスでPowerShell.exe を実行するときに、実行するコマンド文字列を組み立てます。 今のパスに移動するようにしています。

$cd = (Get-Location).Path
$commands = "Set-Location $cd; $myfunction; Write-Host 'Running $me'; $me; Pause"

さて、生成したコマンド文字列は関数の改行が含まれており、このままでは PowerShell.exe の -Command 引数に渡せません。 そこで、バイナリにしてBase64文字列を -EncodedCommand にわたすことで解釈させます。

このあたりは、PowerShell を使ったワーム攻撃でもよく使われる手法です。

$bytes = [System.Text.Encoding]::Unicode.GetBytes($commands)
$encode = [Convert]::ToBase64String($bytes)
$argumentList = "-NoProfile","-EncodedCommand", $encode

あとは、PowerShell を 特権で起動するため、-Verb RunAs を指定して先程の引数を食わせます。 PowerShell.exeの実行時に、-Wait をつけることで、起動したPowerShell.exe が自動で閉じません。

$p = Start-Process -Verb RunAs powershell.exe -ArgumentList $argumentList -Wait -PassThru

おわりに

最近のほげもげみてると、こういう記事書くのは心配です。 ただ、標準で用意されている方法を用途を限定して使うことまで阻害されるのは望ましくないものです。

攻撃にすでに利用もされている方法でもあるので、UAC は自分でプロセスを起動させるほうがパブリックにはいいです。(Chocolatey や Scoop がそう)

どうか正しく使われますように。