しばたテックブログ

気分で書いている技術ブログです。

PowerShellのホストとプロファイルについてまとめ

PowerShellがオープンソース、マルチプラットフォーム化したのを踏まえ、改めてPowerShellのホストとプロファイルについてまとめてみました。

ホスト

PowerShellにおけるホストとは簡単に言ってしまうとPowerShellの実行環境のことです。

基本的なホスト

従来のPowerShellではホストはPowerShell コンソール(powershell.exe)とPowerShell ISEの2つだけでした。それぞれ32bit/64bit版があります。
これに先日発表されたLinxu/Mac版のPowerShellが加わり、こちらは64bit版のみの提供となります。

  1. [Windows] PowerShell コンソール (32bit/64bit)
    • [Linux] PowerShell コンソール (64bit)
    • [Mac] PowerShell コンソール (64bit)
  2. [Windows] PowerShell ISE (32bit/64bit)

OSが違ってもPowerShell コンソールは同じコードベースなため、ホストの種類としては同一となり、ISEとあわせて2種類になります。

拡張機能のホスト

PowerShell コンソールPowerShell ISEの他にVisual Studio/Visual Studio Codeなどの拡張機能として以下のホストが存在します。

  1. [Windows][Linux][Mac] PowerShell Language Support for Visual Studio Code (ホストは32ibt/64bit)
    • Visual Studio Codeの拡張機能のデバッグコンソール。Version.0.6 以降で利用可能
    • 昔はMicrosoft.PowerShell.EditorServices.Host.exeが独自のホストだったが今はpowershell(.exe)を別途起動してから必要なアセンブリをロードする形式に変わっている。
  2. [Windows] PowerShell Tools for Visual Studio (32bit)
    • Visual StudioのPowerShell拡張のIntegrated Console。Version.3.0.235 以降で利用可能
  3. [Windows] Visual Studio Package Manager Console (32bit)
    • Visual Studio上のNuget Package Manager Console

現時点で公式にリリースされているホストはこの5種類になります。

余談ですが、PowerShellのホストはその気になれば自作できますので、探せばほかの開発環境などで使われているホストもあるかもしれません。
だだ、そこまで考慮するとキリがないため今回はこの5ホストに絞った内容にします。

ホスト名の一覧

各ホストの情報には$Host変数でアクセスすることができます。
アクセス可能な情報はホストによって異なりますので、本エントリではホスト名の一覧を紹介するにとどめておきます。

ホスト $Host.Name
[Windows/Linux/Mac] PowerShell コンソール (32bit/64bit) ConsoleHost
[Windows] PowerShell ISE (32bit/64bit) Windows PowerShell ISE Host
[Windows/Linux/Mac] PowerShell Language Support for Visual Studio Code (ホストは32ibt/64bit) Visual Studio Code Host
[Windows] PowerShell Tools for Visual Studio (32bit) PowerShell Tools for Visual Studio Host
[Windows] Visual Studio Package Manager Console (32bit) Package Manager Host

プロファイル

ここからプロファイルについて触れていきます。
プロファイルとは、about_Profilesに、

Windows PowerShell プロファイルは Windows PowerShell が開始するときに動作するスクリプトです。

と書かれている通りのものでBashでいうところの.bash_profileです。
基本的なところはぎたぱそ先生のこちらの記事を見てもらえば良いでしょう。

プロファイルの種類

プロファイルは実行ユーザー、実行ホストに応じて以下の4種類存在しています。

種類 実行順 説明 場所
AllUsersAllHosts 1 全ユーザー、すべてのホストで有効なプロファイル $PSHOME
AllUsersCurrentHost 2 全ユーザー、現在のホストで有効なプロファイル $PSHOME
CurrentUserAllHosts 3 現在のユーザー、すべてのホストで有効なプロファイル Windows : [MyDocument]\WindowsPowershellまたは[MyDocument]\Powershell
Linux/Mac : $HOME/.config/powershell
CurrentUserCurrentHost 4 現在のユーザー、現在のホストで有効なプロファイル Windows : [MyDocument]\WindowsPowershellまたは[MyDocument]\Powershell
Linux/Mac : $HOME/.config/powershell

プロファイルが実行される順序は上表の通り、AllUsersAllHostsAllUsersCurrentHostCurrentUserAllHostsCurrentUserCurrentHostとなっています。

また、プロファイルの場所についてですが、AllUsers*なプロファイルは$PSHOMEになります。
$PSHOMEはPowerShellの実行バイナリのあるディレクトリになりますので、OSの種類、32bit/64bit版によって異なります。

続けてCurrentUser*についてですが、Windowsの場合は[MyDocument]としていますが、正確にはSystem.Environment.SpecialFolder.Personalで取得できるパスとなっており、設定によりマイドキュメントの場所を変えている場合はプロファイルの場所も変わるので注意が必要です。
LinuxやMacの場合は$HOMEの場所をHOME環境変数から取得しているため、実際に試してはいないのですが、HOME環境変数を書き換えるとプロファイルの場所も変わるはずです。

どのドキュメントか失念しましたが、MSDN上でもプロファイルの場所がC:\Users\[UserName]\Document固定になっているものがあり、これは正確ではないので気をつけてください。

余談ですが、細かい仕様が気になる方はソースのこのへんこのへんを見ると参考になります。

プロファイルのファイル名

そして、プロファイルのファイル名は、*AllHostsなプロファイルはprofile.ps1固定ですが、*CurrentHostなプロファイルはホスト毎に異なります。
以下にホスト毎のファイル名を記載しておきます。

ホスト \ 種類 AllUsersAllHosts AllUsersCurrentHost CurrentUserAllHosts CurrentUserCurrentHost
PowerShellコンソール profile.ps1 Microsoft.PowerShell_profile.ps1 profile.ps1 Microsoft.PowerShell_profile.ps1
PowerShell ISE profile.ps1 Microsoft.PowerShellISE_profile.ps1 profile.ps1 Microsoft.PowerShellISE_profile.ps1
PowerShell Language Support for Visual Studio Code profile.ps1 Microsoft.VSCode_profile.ps1 profile.ps1 Microsoft.VSCode_profile.ps1
PowerShell Tools for Visual Studio profile.ps1 PoshTools_profile.ps1 profile.ps1 PoshTools_profile.ps1
Visual Studio Package Manager Console profile.ps1 NuGet_profile.ps1 profile.ps1 NuGet_profile.ps1

$PROFILE

プロファイルのパスはいちいち全部覚えなくても$PROFILE変数で取得することができます。
この$PROFILEstring型の値ですが、上記の各プロファイルの情報が取得できる様に特別なプロパティが付与されています。

どういう事かというと、普通に$PROFILEにアクセスした場合は、

PS C:\> $PROFILE
C:\Users\[Username]\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1

の様にCurrentUserCurrentHostの値が取得できます。
続けて以下の様にFormat-List -Forceとすると追加されたプロパティの情報を見ることができます。

PS C:\> $PROFILE | Format-List -Force

AllUsersAllHosts       : C:\Windows\System32\WindowsPowerShell\v1.0\profile.ps1
AllUsersCurrentHost    : C:\Windows\System32\WindowsPowerShell\v1.0\Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts    : C:\Users\[Username]\Documents\WindowsPowerShell\profile.ps1
CurrentUserCurrentHost : C:\Users\[Username]\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
Length                 : 80

このため、たとえばAllUsersAllHostsのパスを取得する場合は

PS C:\> $PROFILE.AllUsersAllHosts
C:\Windows\System32\WindowsPowerShell\v1.0\profile.ps1

の様にすればOKです。

【補足】Nano Serverのプロファイルについて

【2016/10/17追記】
ちょっと書き忘れていたので追記しておきます。
以前、

stknohg.hatenablog.jp

で書いた様にWindows Server 2016のNano Server(PowerShell Core)ではプロファイルの機能は無く、$PROFILE変数も使えません。
ただ、オープンソース化したPowerShell Core(6.0.0.alpha)ではプロファイルの機能はありますので、いつになるかはわかりませんが、そのうちNano ServerのPowerShellにもプロファイルは導入されるだろうと思われます。

最後に

とりあえずこんな感じです。
今後新しいホストが増える様でしたら追記していきます。

PowerShellでBOM無しUTF8を簡単に扱う、デフォルト設定を簡単に変える方法

【2017/11/06追記】

現在開発中のPowerShell 6.0からファイル出力に関わるエンコーディングの扱いが変わり、BOM無しUTF-8をより簡単に扱える様になっています。
詳細は以下のエントリを見てください。

blog.shibata.tech

【追記ここまで】


なんとなく思いついて試したら意外といい感じになったので。

はじめに

PowerShellのUTF8はBOM付きUTF8

PowerShellにはOut-File等といったファイルを簡単に扱うためのコマンドレットがいくつか存在します。

たとえばOut-FileでUTF8のファイルを出力する場合は以下の様に-Encodingパラメーターを指定してやればOKです。

"ファイルの内容" | Out-File ".\UTF8なファイル.txt" -Encoding utf8

ただし、PowerShell(とその基盤である.NET Framework)においてUTF8はBOM付きです。

BOM無しのUTF8を扱う場合はOut-Fileなどのコマンドレットは使用できず、以下の様にBOM無しのSystem.Text.UTF8Encodingクラスを生成してやり.NET FrameworkのIO処理を頑張って書いてやる必要があります。(追記あり)

# BOM無しのUTF8エンコーディングクラスを生成
#   コンストラクタの第一引数 = $false でBOM無し
$UTF8woBOM = New-Object "System.Text.UTF8Encoding" -ArgumentList @($false)
# あとは.NET FrameworkのIO処理を頑張って書く
[System.IO.File]::WriteAllLines((Join-Path $PWD "BOMなしUTF8なファイル.txt"), @("ファイルの内容"), $UTF8woBOM)

-Encodingパラメーターで指定可能なエンコーディング

また、先の-Encodingパラメーターは文字列でエンコーディング名を指定しており、コマンドレットによって指定可能なパラメーターは微妙に異なるのですが、おおむね以下となっています。

-Encodingパラメータの値 対応する.NET Frameworkのエンコーディング
unknown System.Text.Encoding.Unicode
string System.Text.Encoding.Unicode
System.Text.Encoding.Default
unicode System.Text.Encoding.Unicode
bigendianunicode System.Text.Encoding.BigEndianUnicode
utf8 System.Text.Encoding.UTF8
utf7 System.Text.Encoding.UTF7
utf32 System.Text.Encoding.UTF32
ascii System.Text.Encoding.ASCII
default System.Text.Encoding.Default
(PowerShell Coreだと System.Text.Encoding.GetEncoding(28591)(PowerShell Core 6.0だとUTF8) *1
oem GetOEMCP()関数で得られるコードページのエンコーディング

これら以外のエンコーディングを扱おうとするとBOM無しUTF8の場合と同様に.NET FrameworkのIO処理を頑張って書くしかありません。

せっかく便利なコマンドレットが提供されているにもかかわらず、これはちょっと不便でなりません。

BOM無しUTF8を簡単に扱う、デフォルト設定を簡単に変える方法

で、実際不便をするケースがあったのでInvoke-CustomEncodingBlockというファンクションを作ってみました。

Invoke-CustomEncodingBlock ファンクション

実装は次で説明しますのでまずは使い方から。

Invoke-CustomEncodingBlockでは引数にスクリプトブロックを指定し、そのスクリプトブロック中でのみutf8defaultパラメーターの挙動を変更する様にしています。

BOM無しUTF8を扱いたい場合は、スクリプトブロックと-UseBOMlessUTF8パラメータを使い以下の様にします。

Invoke-CustomEncodingBlock { "BOMなしUTF8なファイルの内容" | Out-File ".\BOMなしUTF8.txt" -Encoding utf8 } -UseBOMlessUTF8

これでOut-FileからBOM無しUTF8のファイルを出力することができます。

また、-DefaultCodePageパラメーターを使うとdefaultを任意のコードページに差し替えることができます。
例えば以下の様にするとEUC(コードページ51932)のファイルを出力することができます。

Invoke-CustomEncodingBlock { "EUCなファイルの内容" | Out-File ".\UEC_CP51932.txt" -Encoding default } -DefaultCodePage 51932

そしてOut-Fileコマンドレット以外にも利用できますので、たとえば、

Invoke-CustomEncodingBlock { Get-Content -Path ".\UEC_CP51932.txt" -Encoding default } -DefaultCodePage 51932

の様にするとGet-ContentでEUCのファイルを読み込みことができたりもします。

いかがしょうか?
既存のコマンドレットを生かしつつ好きなエンコーディングを扱えるので結構便利だと思います。

実装

実装はGistに上げています。
PowerShell 2.0, 4.0, 5.1, 6.0.0.alpha10 *2 で動作確認をしています。

仕組み

仕組みは極めて単純かつ強引ですw
uft8defaultパラメーターは最終的にSystem.Text.Encoding.UTF8System.Text.Encoding.Defaultに変わるため、リフレクションで強引にこれらのエンコーディングの中身を差し替え、スクリプトブロックを呼び終えたら元に戻すということをやっています。

このため、RunSpaceを使って並列処理をしている場合は予期せぬ問題が起きる可能性がありますが、それ以外の場合はおおむね問題ないはずです。
PowerShellのバージョンによって差し替える必要のあるフィールドに違いがあるため、そのあたりは工夫しています。

最後に

とりあえずこんな感じです。

思いつきで作ったものですが、想像以上に便利で個人的にはかなり気に入っています。

追記 BOM無しUTF8を簡単に扱う別解

【2016/10/03追記】

Add-ContentSet-Contentにバイナリデータを扱う-Encoding Byteパラメーターがあるのを今更ながら知りました...
Byte[]なバイナリデータをそのままファイルに書き込みできるため、BOM無しUTF8は、

"ファイルの内容" `
    | % { [Text.Encoding]::UTF8.GetBytes($_) } `
    | Set-Content -Path ".\BOMなしUTF8なファイル.txt" -Encoding Byte

の様にしてあっさり簡単に書き込むことができます。
自分のPowerShell力の低さがうらめしいです...

【2017/05/18追記】

こちらの方法ですが、状況によってはOut-Stringを使って明示的に改行込みの文字列にしてやらないと改行コードが抜かされて出力される場合があります。

[ファイルの内容を出力するコマンドなど] | `
    | Out-String `
    | % { [Text.Encoding]::UTF8.GetBytes($_) } `
    | Set-Content -Path ".\BOMなしUTF8なファイル.txt" -Encoding Byte

参考)

satob.hatenablog.com

*1:PowerShell Core 6.0 Beta.1よりUTF8を使う様に変更されている 参考 : https://github.com/PowerShell/PowerShell/pull/3467

*2:PowerShell 6.0では-Encodingパラメーターの扱いが変わったのでこの関数を使えない様にしました

符号化処理芸人たちのシェル芸をPowerShellで再現する

元ネタはこちら。

papiro.hatenablog.jp

はじめに

私はシェル芸人ではないので大したことも面白いこともできませんのでご了承ください。

本エントリはシェル芸人たちの匠の技をPowerShellで再現するにはどうするかという点だけに注力しています。
PowerShell(というかWindows)には残念ながら元ネタで使われている各種コマンドが無い*1ため、その部分をPowerShellおよび.NET Frameworkの機能をいかに使って補うかがキモになるかと思っています。

それでは元ネタのお題の順に進めていきます。
特にバージョン依存の処理は書いていないはずですが、Windows 10のPowerShell(5.1)で動作確認をしています。

1. Use Xamarin.

お題

最初はちょまどさんのこちら。

元ネタの回答はこちら。

echo 01010101 01110011 01100101 00100000 01011000 01100001 01101101 01100001 01110010 01101001 01101110 00101110 | tr -d ' ' | sed 's/^/obase=16;ibase=2;/' | bc | xxd -p -r

PowerShell版

PowerShellだとこんな感じで書けます。
長くなるのが嫌だったので適当なところで改行しています。

# PowerShell
-split "01010101 01110011 01100101 00100000 01011000 01100001 01101101 01100001 01110010 01101001 01101110 00101110" `
    | % { $o = "" } `
        { $o += [Char]([Convert]::ToByte($_, 2)) } `
        { $o }

実行結果はこんな感じ。

f:id:stknohg:20160923215425p:plain

解説

簡単に解説を入れていきます。
最初の行の、

-split "01010101 01110011 01100101 00100000 01011000 01100001 01101101 01100001 01110010 01101001 01101110 00101110"

は、お題の文字列がスペース区切りなので-split演算子を使って配列化しています。

2行目以降は配列化された二進表記の文字列をForEach-Object(%)を呼んで、3つのスクリプトブロックを使って変換しています。
ForEach-Object(%)では-Begin-Process-Endのパラメータで最大3つのスクリプトブロックを指定することができ、それぞれ、

  • -Begin : 最初のオブジェクトがパイプされる直前に呼ばれる処理
  • -Process : 各オブジェクトがパイプされる毎に呼ばれる処理
  • -End : 最後のオブジェクトがパイプされた後に呼ばれる処理

となっています。

今回はパイプラインには配列化した各要素("01010101", "01110011", ...)が順に渡されていきます。
最初のスクリプトブロック(-Begin)で最終出力用の変数$oを定義し、二番目のスクリプトロック(-Process)で入力文字列の変換処理を行っています。

[Char]([Convert]::ToByte($_, 2))

で入力文字列$_("01010101"など)をByte型に変換し、Char型にキャストしてASCII文字列に戻しています。
最後のスクリプトブロック(-End)で連結された$oを出力ストリームに出力しています。

ちなみに、

$o

Write-Output $o

は同義です。
今回はそれっぽくするためWrite-Outputを端折ってみました。

2. 届けiOS10

お題

次はぱぴろんさんのこちら。

元ネタの方ではRubyやPerlを使った回答が提示されていました。

PowerShell版

PowerShellだとこんな感じです。

# PowerShell
"111001011011000110001010111000111000000110010001011010010100111101010011001100010011000000001010" `
    | % { $n = 8; for( $i = 0; $i -lt $_.Length; $i += $n ){ -join $_[$i..$($i+$n-1)] } } `
    | % { $b = @() } `
        { $b += [Convert]::ToByte($_, 2) } `
        { [Text.Encoding]::UTF8.GetString($b) }

実行結果はこんな感じ。

f:id:stknohg:20160923222547p:plain

解説

今回はお題の文字列に区切り文字が無いので、2行目のForEach-Object(%)

% { $n = 8; for( $i = 0; $i -lt $_.Length; $i += $n ){ -join $_[$i..$($i+$n-1)] } }

で、8文字ごとに区切って後続にパイプしています。
スクリプトブロックを1つだけにした場合は-Processブロックになります。

3行目以降のForEach-Object(%)につては基本的に最初のお題と同じですが、最終的な答えがUTF8の文字列であったため、Byte[]列をUTF8に変換する様になっている部分が異なっています。

3. 焼肉

お題

次はぐれさんさんのこちら。

こちらの回答は自分の環境(Bash on Ubuntu on WindowsおよびVM上のUbuntuのBash)ではうまく動作せず、以下のコードであれば動作しました。

echo $(echo 1302140411021101140213011103110511011103110211 | fold -w 2 | awk '{for(i=1;i<=$2;i++){printf $1}}' FS= | sed 's/^/obase=16;ibase=2;/') | bc | xxd -p -r

シェル芸人ではないので細かいところはよくわかりません...

PowerShell版

PowerShellだとこんな感じです。

# PowerShell
"1302140411021101140213011103110511011103110211" `
    | % { $n = 2; for( $i = 0; $i -lt $_.Length; $i += $n ){ -join $_[$i..$($i+$n-1)] } } `
    | % { $s = "" } `
        { $s += "".PadRight([string]$_[1], $_[0]) } `
        { $s } `
    | % { $n = 8; for( $i = 0; $i -lt $_.Length; $i += $n ){ -join $_[$i..$($i+$n-1)] } } `
    | % { $b = @() } `
        { $b += [Convert]::ToByte($_, 2) } `
        { [Text.Encoding]::UTF8.GetString($b) }

実行結果はこちら。

f:id:stknohg:20160923225340p:plain

解説

だいぶつらくなってきました...
文字列をランレングス圧縮しているとの事ですので、そのあたりの処理が入ってきます。

PowerShellには良い感じのコマンドがないのでひたすらForEach-Object(%)のスクリプトブロックで頑張るしかありません。

2行目の

% { $n = 2; for( $i = 0; $i -lt $_.Length; $i += $n ){ -join $_[$i..$($i+$n-1)] } }

はお題の文字列を2文字区切りにしています。
3~5行目のForEach-Object(%)でそれぞれ二進表記に変換(13111などへ)しています。

% { $s = "" } `
  { $s += "".PadRight([string]$_[1], $_[0]) } `
  { $s } `

6行目の

% { $n = 8; for( $i = 0; $i -lt $_.Length; $i += $n ){ -join $_[$i..$($i+$n-1)] } }

で二進表記の文字列を8文字区切りにし、7~9行目の、

% { $b = @() } `
  { $b += [Convert]::ToByte($_, 2) } `
  { [Text.Encoding]::UTF8.GetString($b) }

でUTF8の文字列に変換しています。

4. YES

最後のお題です。

元ネタの回答はこちら。

echo 'In48FEBACHw8CEACCEBCCH48' | base64 --decode | xxd -b -c 3 | awk '$1="";$NF="";1' | sed 'y/01/ y/'

PowerShell版

PowerShellだとこんな感じ。

# PowerShell
"In48FEBACHw8CEACCEBCCH48" `
    | % { [Convert]::FromBase64String($_) } `
    | % { $s=@(); $map=@{ "0"=" "; "1"="y" } } `
        { $s += ([Regex]($map.Keys -join "|")).Replace([Convert]::ToString($_, 2).PadLeft(8, "0"), {param($m) $map[$m.Value] }) } `
        { for( $i = 0; $i -lt $s.Length; $i+=3 ){ -join $s[$i..($i+2)] } }

実行結果はこちら。

f:id:stknohg:20160923230555p:plain

解説

PowerShellでBASE64を処理するのは[Convert]::FromBase64String()メソッドを呼ぶだけですので楽勝です。
3~4行目の、

% { $s=@(); $map=@{ "0"=" "; "1"="y" } } `
  { $s += ([Regex]($map.Keys -join "|")).Replace([Convert]::ToString($_, 2).PadLeft(8, "0"), {param($m) $map[$m.Value] }) }

の部分でBASE64文字列をデコードしたByte値を二進表記に変換し、加えて0→" "1→"y"への置換を行っています。
文字列の置換には[Regex]クラスを使用しています。
最後の5行目

  { for( $i = 0; $i -lt $s.Length; $i+=3 ){ -join $s[$i..($i+2)] } }

の部分で、3Byteごとに区切っています。

まとめ

とりあえずこんな感じです。

シェル芸に対してPowerShellの圧倒的なコマンド不足が露呈しつつも、.NET Frameworkのもつ強力な機能のおかげでかろうじて何とかなっている感じといったところです。
良くも悪くも「コマンドが無いならスクリプトブロックで何とかするしかないじゃん!」といった体になってしまいました。

とはいえ、実際の業務でPowerShellを利用する場合であれば、足りないコマンドは自作の関数で補うことができますのでそこまで苦労することは無いかと思います。

割と思いつくままにコードを書きましたので、もっと良い方法は他にもたくさんあると思います。
より良い方法がありましたらフィードバック頂けると嬉しいです。

*1:いちおう補足しておくとBash on Ubuntu on Windowsにはありますよ