場所スタック関係のややこしい仕様

ややこしいので整理。

pushdなどで用いられる場所スタック(location stack)には無名スタック(unnamed location stack)と名前つきスタック(named location stack)があるが、それとは別に「既定スタック」(default location stack)という概念もある。たとえば、pushdとするとデフォルトでは無名スタックに現在のロケーションが追加されるが、それはデフォルトでは無名スタックが既定スタックになっているから。既定スタック以外のスタックに追加するには、pushd -s a (ちゃんと書くとPush-Location -StackName a)のようにスタック名を指定する。ここでは名前つきスタックaに追加している。

さて任意の場所スタックxは Set-Location -StackName x (sl -s x と簡単に書ける)で既定スタックにできる。xが既定スタックになると、pushdとするだけでスタックxに追加されるようになる。

実はそれだけではなく、既定スタックと無名スタックの内容は同期される。つまり、既定スタックに追加すると無名スタックにも自動的に追加され、無名スタックに追加すると既定スタックにも自動的に追加される。具体的な動作を以下に示す。

system32> gl -StackName a # この状態から開始する。スタックaの中身はこのとおり。( Get-Location -StackName a でスタックaの中身を表示できる。
ちなみに、Get-Location -Stack という既定スタックの中身を表示する構文もあるので、-StackNameというパラメータを-sと簡略化することはできない)

Path                                                                                                                                 
----                                                                                                                                 
C:\Windows\system32                                                                                                                  

system32> gl -StackName '' # 無名スタックの中身はこのとおり。現在無名スタックが既定スタックである。(無名スタックは空文字列で指定する)

Path                                                                                                                                 
----                                                                                                                                 
C:\Windows                             

system32> sl -s a # スタックaを既定スタックにする

system32> gl -StackName a # スタックaの中身はもちろん変わらない

Path                                                                                                                                 
----                                                                                                                                 
C:\Windows\system32                                                                                                                  

system32> gl -StackName '' # 無名スタックに入っていた中身が消えて、代わりにスタックaと同じ中身になっている。

Path                                                                                                                                 
----                                                                                                                                 
C:\Windows\system32                  

system32> cd c:\users

users> pushd -s a # スタックaに追加すると・・・

users> gl -StackName '' # 無名スタックにも追加され、

Path                                                                                                                                 
----                                                                                                                                 
C:\users                                                                                                                             
C:\WINDOWS\system32                                                                                                                  

users> cd '..\Program Files'

Program Files> pushd -s '' # 無名スタックに追加すると・・・

Program Files> gl -StackName a # スタックaにも追加される。

Path                                                                                                                                 
----                                                                                                                                 
C:\Program Files                                                                                                                     
C:\users                                                                                                                             
C:\WINDOWS\system32           

以上のように、既定スタックを名前つきスタックaにすると、無名スタックとスタックaは、
・両者とも名前つきスタックaの内容になる。無名スタックの側が名前つきスタックに合わせるともいえる。
・両者に加えられた変更は同期される。上の例ではpushdだったけれどもpopdでも同様。
となる。

紛らわしいことに、同期により無名スタックの中身が上書きされて失われるわけでは"ない"。もとあった内容は既定スタックを無名スタックに戻すと回復する。すなわち次のようになる。

WINDOWS> gl -StackName '' # 既定スタックは無名スタックで、スタックの中身はこのとおり。

Path                                                                                                                                 
----                                                                                                                                 
C:\WINDOWS\system32                                                                                                                  

WINDOWS> gl -StackName a # スタックaの中身。

Path                                                                                                                                 
----                                                                                                                                 
C:\WINDOWS                                                                                                                           

WINDOWS> sl -s a # 既定スタックをaに。

WINDOWS> gl -StackName '' # 無名スタックのもとの内容が隠れて見えなくなる。

Path                                                                                                                                 
----                                                                                                                                 
C:\WINDOWS                                                                                                                           

WINDOWS> gl -StackName a

Path                                                                                                                                 
----                                                                                                                                 
C:\WINDOWS                                                                                                                           

WINDOWS> sl -s '' # 既定スタックを無名スタックに戻す。

WINDOWS> gl -StackName '' # もとの内容が回復する。

Path                                                                                                                                 
----                                                                                                                                 
C:\WINDOWS\system32                                                                                                                  

WINDOWS> gl -StackName a

Path                                                                                                                                 
----                                                                                                                                 
C:\WINDOWS             

つまり、既定スタックを無名スタックにすると、
・無名スタックの内容がもとの内容に戻る。詳しく言うと、最後に無名スタックが既定スタックだった時の内容に戻る。
・直前に既定スタックだった名前つきスタックの内容は変わらない。
となる。

これまで無名スタックと既定スタックの挙動を見てきたけれども、スタックに関して他に気になる点、注意したい点を挙げておく。

まず、全てのスタックの名前を取得する方法が見当たらないこと。それができれば、全てのスタックの中身を表示する便利関数が書けるのに。

次は、既定スタックがどれかを取得する方法も見当たらないこと。既定スタックのsetはできるけどgetする方法がどうも用意されていないっぽい。

それから、名前つきスタックは中身が無くなるとスタック自体が削除されること。スタック自体が削除されても同名のスタックにpushdで追加することはできる。その場合はスタックが新規作成されるので問題ない。しかし、Get-Location -StackName foo でスタックの中身を取得しようとするとエラーになるから注意が必要だ。

引数 "stackName" の値が無効なため、引数を処理できません。引数 "stackName" の値を変更し、操作を実行し直してください。

例えば、pushdのラッパー関数としてスタックに既にあるロケーションは追加しない関数を書くとき、スタックとの重複をチェックする部分のエラー処理に注意が必要だろう。

最後は、pushd <パス名> の挙動。これはそのパスがスタックに追加されるわけではなく、カレントロケーションが追加されてそのパスにカレントが移動する。

以上、場所スタック関連は少し要注意な挙動が多くて慣れるまでに時間がかかりそうだ。

スクリプト自身のパス、スクリプトを呼び出したスクリプトのパスを返す関数

よく分からないけどこれでいいんじゃなかろうか。

function Get-ScriptName
{
	<#
	.SYNOPSIS
	スクリプト名を取得する。
	.DESCRIPTION
	引数なしならスクリプト自身のフルパス。
	引数が$trueならスクリプトを直接呼び出したスクリプトのフルパス。
	#>
	param([bool]$caller)

	if($caller)
	{
		$script:myInvocation.ScriptName
	}
	else
	{
		$script:myInvocation.MyCommand.path
	}
}

と思ったけど、せっかくだからこんな関数を作った。

# profile.ps1

function Get-CalledScript
{
	<#
	.SYNOPSIS
	コールスタック上にあるスクリプト名を取得する。
	.DESCRIPTION
	引数が0だとこの関数が呼ばれたスクリプトの名前を返す。ただし、このファイルからは呼ばれないものとする。
	引数が1以上だとコールスタックを遡ってn個前のスクリプトを返す。
	引数なしだとスクリプト名の配列を返す。
	#>
	param([int]$level)

	$calledScripts = (Get-PSCallStack).ScriptName | ? {
		if($tmp -eq $null){ $tmp = $_; }
		if($tmp -ne $_){ $tmp = $_; $true }
	}

	if($PSBoundParameters.Count -eq 0)
	{
		$calledScripts
	}
	else
	{
		$calledScripts[$level]
	}
}

学習の進捗

Tipsが豊富なサイトを手当たり次第に見ては、必要なことをポケットリファレンス、インアクション(第1版の邦訳)で調べ、コードを自作している。

が、分からないことが多くて学習のテンポが大幅に落ちてきた。PowerShellと直接関係のないことや細かいことへのこだわりで学習のテンションを落としたくない。

PowerShellのカスタマイズ方法がようやくつかめてきた。型データ、モジュール、プロファイル、コンソールなど。

ファイル管理、アプリケーションの起動など初歩的なことがいろいろとPowerShellで出来るようになってきた。まさにシェル。ランチャー等が要らなくなりつつある。

ISEをコンソールとして使い始めた。複数行コピペがしやすい、コマンドラインを保持したまま簡単にスクロールできるなど、コンソールとして優れていると気づいた。
ISEは常時起動し、スクリプトウィンドウを基本非表示にし、スクリプトの編集はVimで行っている。
必要なときだけスクリプトウィンドウを使う。コマンドウィンドウやアドオンもなかなか便利そうだけどまだまだ使いこなせていない。

学習のための便利関数や汎用データを作成して学習を能率化しようとしている。そうした関数やデータを作ること自体が学習になって楽しい。
スニペット作成にも取り組みたい。

最新の情報をチェックしたい欲が強まってきた。5年近く前の情報も役立つことが多いけど、今では非推奨な書き方も多い。
Invoke-WebRequest使ったほうがいいよとか、JSONはコマンドレットがあるよとか、Get-EventLogは古いよとか、それCOM使わずに簡単にできるよとか。

とはいえ書籍はv3.0までカバーしてあればさほど問題ない感じがする。v4.0やv5.0はWebで調べればいい。
本だとポケットリファレンスは重宝している。PowerShell Cookbook 3rd editionは1000ページもあって実例豊富で面白い。PowerShell in Actionもそろそろ第3版が出てほしいな。

ちなみにv4.0までカバーしたものにWindows Powershell 4.0 for .net Developersという本がある。
情報が新しいだけでなく、140ページとコンパクトに要点がまとまっていて、まだ少ししか読んでないけど内容も面白そう。Kindle版なら1293円と安かったので買った。

たくさんの読むべき記事や本、書きたいコード、やりたいこと、知りたいことがあって、それがどんどん増えている。
今月いっぱいはPowerShellに専念してある程度収拾をつけたい。

Everythingを使った関数を書いてみた

everythingのコマンドラインツールes.exeを利用してファイル検索する関数を書いてみた。

function Everything
{
	param([string]$word, [string]$dir=$(pwd))

	start cmd.exe -Verb runas -ArgumentList "/c es.exe", $word, $dir, " | clip"
	sleep -m 500 # 適宜調整
	get-clipboard # http://winscript.jp/powershell/229 を拝借。ありがとうございます。
}
sal evg Everything

補助関数として次のものも利用。

function Get-SpecificLocation
{
	switch ($args[0])
	{
		{ 'visualstudio', 'vs' -contains $_ } {
			switch ($args[1])
			{
				$null { return 'C:\doc\Visual Studio 2013'; break; }
				# 省略
			}
		}
		# 省略
	}
}
sal gsl Get-SpecificLocation

使い方。

> evg 検索文字列 (gsl vs)

検索結果が
フルパス文字列の配列として
ここに表示される

> evg 検索文字列 (gsl vs) | なにか処理

Everything関数のGet-ChildItemに比べたメリット
・gci -Recurseより速い(Everythingの検索が速いのに加え、フルパスしか情報を取得・出力しないため?)
・パスが手っ取り早くほしいときに、 | % FullName | をはさむ手間がいらない
・検索文字列を "word1 word2" の形にしてAND検索が書ける
・検索文字列を -r hoge の形にして正規表現が使える
・他にもEverythingの検索構文が使える

2014/5/8 17:28追記
param([ValidateSet("vs", "visualstudio", ... )]$category) として引数を補完させるほうが便利かも。

複数キーによるソート

$vectors = @()
1..5 | %{
	$x = $_
	1..5 | % { $vectors += @{ x = $x; y = $_ } }
}

$vectors | sort @{ e={$_.x}; asc=$true },@{ e={$_.y}; desc=$true } |
	%{ ("(" + $_.x + ", " + $_.y + ")") }
(1, 5)
(1, 4)
(1, 3)
(1, 2)
(1, 1)
(2, 5)
(2, 4)
(2, 3)
(2, 2)
(2, 1)
(3, 5)
(3, 4)
(3, 3)
(3, 2)
(3, 1)
(4, 5)
(4, 4)
(4, 3)
(4, 2)
(4, 1)
(5, 5)
(5, 4)
(5, 3)
(5, 2)
(5, 1)

switch文のfileオプションの2つの落とし穴

1つ目 Shift_JISで全角文字が化ける

switch文にはfileオプションがあり、指定したファイルの各行をイテレート処理できる。しかしファイルがShift_JISの時は――

$fname = "test.txt"
$txt = @"
1:Shift_JISにおける
2:全角のテスト
"@

if(-not (Test-Path $fname)){ ni -Type file $fname >$null }
# encoding defaultでshift-jisになる
sc $fname -Encoding default $txt

# 文字化けする
switch -regex -file $fname
{
	"^\d:(.*)" { $matches[1] }
}

# fileオプションをやめてGet-Contentで取得すればおk
switch -regex (gc $fname)
{
	"^\d:(.*)" { $matches[1] }
}

これがこうなる。

Shift_JIS???????
?S?p??e?X?g
Shift_JISにおける
全角のテスト

2つ目 $switchが使えない

switch文の内側では、$switchでswitchループのイテレータにアクセスできる。
$switch.currentで現在の値を取得。$switch.movenext()で次の値にイテレータを進める。foreach文における$foreachと同様の機能。
この$switchを『Windows PowerShell In Action』では、switch loop enumerator(スイッチループ列挙子)と呼んでいる。

$switchは便利な機能だが、どうやらfileオプション指定時には利用できない。

$fname = "test.txt"
$txt = @"
aaa
bbb
# ccc
ddd
<#
eee
fff
#>
ggg
"@

if(-not (Test-Path $fname)) { ni -Type file $fname >$null }

こういうテキストファイルがあって、PowerShell風にコメントアウトした部分を除いて取得したい。

しかし、次のコードは、「null 値の式ではメソッドを呼び出せません」とエラーになる。fileオプション指定時は$switchがnullになる模様。

sc $fname $txt
switch -regex -file $fname
{
	'^#' { continue }
	'^<#' {
		while($switch.current -notmatch '^#>') { [void]$switch.movenext() }
	}
	default { $_ }
}

1つ目の落とし穴と同様、fileオプションをやめてGet-Contentを用いればうまくいく

switch -regex (gc $fname)
{
	'^#' { continue }
	'^<#' {
		while($switch.current -notmatch '^#>') { [void]$switch.movenext() }
	}
	default { $_ }
}
aaa
bbb
ddd
ggg