2015/11/14

既出ですが、まとめておきます。

今回登場する関数は、elevate.ps1という名前で一つにまとめて保存しておくと便利です。

起動中のスクリプトが管理者権限で実行されているかどうかの確認
function Test-Admin
{
     (
        [Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::
        GetCurrent()
     ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

スクリプト中でこのTest-Admin関数を実行してTrueが返れば、スクリプト(もしくはコンソール)は管理者権限で実行されている。

管理者権限で任意のスクリプトを起動
function Start-ScriptAsAdmin
{
    param(
        [string]
        $ScriptPath,
        [object[]]
        $ArgumentList
    )
    if(!(Test-Admin))
    {
        $list = @($ScriptPath)
        if($null -ne $ArgumentList)
        {
             $list += @($ArgumentList)
        }
        Start-Process powershell -ArgumentList $list -Verb RunAs -Wait
    }
}

Start-ScriptAsAdmin "任意のps1ファイルパス" を実行すると、指定のスクリプトが管理者権限で実行される。(実行前にUACダイアログが表示される)

なお、"powershell"の部分を、今起動しているホストの実行ファイルと同じにするには、(Get-Process -Id $pid).Path としても良い。ただ、ISEの場合だと単にスクリプトファイルが開かれた状態になるだけで実行まではしてくれない。

管理者権限がない場合、昇格してスクリプトを再実行する

管理者権限を要する処理の直前で、以下のようにStart-ScriptAsAdmin関数を実行すると、仮に管理者権限がない場合はスクリプトを再起動し、昇格して再実行する。(管理者権限がある場合は再実行せずそのまま継続する。)

. .\elevate.ps1
Start-ScriptAsAdmin -ScriptPath $PSCommandPath
if(Test-Admin)
{
    "昇格を要する処理"
}

v3未満の場合は、実行中のスクリプトパスを示すシェル変数$PSCommandPath の代わりに &{$MyInvocation.ScriptName}が使える

再起動するまでに実行した処理は再実行しないようにするには、以下のようにスクリプトファイルにパラメータを定義すれば良い。

param([switch]$SkipNormalTask)
. .\elevate.ps1

if(!$SkipNormalTask)
{
    "昇格不要な処理1"
}

Start-ScriptAsAdmin -ScriptPath $PSCommandPath -ArgumentList "-SkipNormalTask"

if(Test-Admin)
{
    "昇格を要する処理"
}

if(!$SkipNormalTask)
{
    "昇格不要な処理2"
}
UACダイアログを出さずに管理者権限でスクリプトを実行する

一般的な方法と同様、タスクスケジューラを利用する。

PowerShellからタスクスケジューラにスクリプトを登録するには、PowerShell3.0以上に同梱されているPSScheduledJobモジュールが必要。

function Register-ScriptAsAdmin
{
    param(
        [string]
        $ScriptPath,
        [object[]]
        $ArgumentList
    )

    $jobArgs=@{
        FilePath = $ScriptPath
        ScheduledJobOption = New-ScheduledJobOption -RunElevated
        Name = "RunAsAdmin $(Split-Path -Leaf $ScriptPath)"
    }
    if($null -ne $ArgumentList){$jobArgs+=@{ArgumentList = $ArgumentList}}

    Register-ScheduledJob @jobArgs 
}

function Invoke-ScriptAsAdmin
{
    param(
        [string]
        $ScriptPath
    )
    $job = Get-ScheduledJob -Name "RunAsAdmin $(Split-Path -Leaf $ScriptPath)"
    $job.RunAsTask()
}

function Unregister-ScriptAsAdmin
{
    param(
        [string]
        $ScriptPath
    )
    Unregister-ScheduledJob -Name "RunAsAdmin $(Split-Path -Leaf $ScriptPath)"   
}

まず、管理者権限で実行したいスクリプトをRegister-ScriptAsAdmin "スクリプトのフルパス"としてタスクスケジューラに登録。

この操作には当然ながら管理者権限を要するので、もし登録用スクリプトから登録をするには前述のStart-ScriptAsAdminを併用しても良い。(さすがにUACダイアログを1回も表示させない方法はなさげ…)

登録が済んだら後は、Invoke-ScriptAsAdmin "スクリプトのフルパス"とすれば、UACダイアログを表示させずに管理者権限でスクリプトを実行できる。

スクリプトの登録を解除するには、Unregister-ScriptAsAdmin "スクリプトのフルパス"を実行する。(この操作には管理者権限不要)

管理者権限がない場合はスクリプトを実行しない

逆に、管理者権限がない時は、一切処理を実施させたくない場合。

v4以上の場合は、以下をスクリプトの一行目に定義しておくだけでOK。管理者権限がない場合はエラーになってそのままスクリプトは終了する。

#Requires -RunAsAdministrator

v4未満の場合は、前掲のTest-Admin関数を以下のようにスクリプト先頭で呼び出せば良い。

if(!(Test-Admin))
{
    throw "管理者権限がありません"
}

2015/08/10

C#6.0のnameof演算子(じんぐるさんによる解説岩永さんによる解説)が羨ましかったので、PowerShellでも似たようなことができるようにしてみました。

function nameof
{
    param([scriptblock]$s)
    $element=@($s.Ast.EndBlock.Statements.PipelineElements)[0]
    if($element -is [System.Management.Automation.Language.CommandExpressionAst])
    {
        switch($element.Expression)
        {
            {$_ -is [System.Management.Automation.Language.TypeExpressionAst]}
                {$_.TypeName.Name}
            {$_ -is [System.Management.Automation.Language.MemberExpressionAst]}
                {$_.Member.Value}
            {$_ -is [System.Management.Automation.Language.VariableExpressionAst]}
                {$_.VariablePath.UserPath}
        }
    }
    elseif($element -is [System.Management.Automation.Language.CommandAst])
    {
        $element.CommandElements[0].Value
    }
}

nameof{$PSHOME}                      # 変数名 : PSHOME
nameof{$PSHOME.Length}               # プロパティ名 : Length 
nameof{[System.Diagnostics.Process]} # クラス名 : System.Diagnostics.Process
nameof{[string]::Empty}              # フィールド名 : Empty
nameof{[DayOfWeek]::Friday}          # 列挙体メンバー名 : Friday
nameof{Get-Command}                  # コマンド名 : Get-Command

原理的には、変数やプロパティ等をスクリプトブロックに格納し、生成されるAST(抽象構文木、abstract syntax tree)を解析して、含まれる変数名やプロパティ名を抽出しています。(なので、PowerShell 3.0以上でないと動作しないと思います)

そもそも、どういうシチュエーションで使うの?という話ですが、実はあえとすさんのPowerShell コマンドを C# で書くときに便利な拡張メソッド - 鷲ノ巣という記事を見て、じゃあPSでコマンド(高度な関数)を書く時にも同じことが出来るといいかな?と思ったのがきっかけです。

function Get-Test
{
    [CmdletBinding()]
    param([int]$Number)
    if($PSBoundParameters.ContainsKey((nameof{$Number})))
    {
        "-$(nameof{$Number})パラメータが指定された"
    }
}

こういう風に、"Number"という文字列をコード中に書かずに、-Numberパラメータ指定の有無を確認できるようになる、というわけです。

(この例の場合だと、Get-Test -Number 12 のようにすると、if文の中身が実行されます。)

ただ作ってはみたものの、使う意味がどれほどあるのか疑問に思えてきました。一応、ISEでは変数名やメンバ名に入力補完が効くので、実際の変数名を文字列で手打ちしなくて済むというメリットはなきにしもあらず、ですか。

しかし所詮は動的言語なので、存在しない変数名やメンバ名を入れても実行前にエラーは出ないですからね。(Set-StrictModeによるストリクトモードは編集時ではなくあくまで実行時(正確には変数やメンバを参照した瞬間)にエラーを出すためのもの)

それとISEのリファクタリング機能は弱い(というか無い)ので、リファクタリングに追従できるという本家nameof演算子に存在するメリットは、現状のところISEを使っている限りは享受できません。

PowerShell 5.0からの新要素、Script Analyzerによる静的解析を組み合わせればあるいは意味が出てくるのかもしれないですが、まだ確認できてないです。

というわけで、書いてはみたもののなんか微妙ですが、せっかくなんで公開しときます。



Copyright © 2005-2018 Daisuke Mutaguchi All rights reserved
mailto: mutaguchi at roy.hi-ho.ne.jp
プライバシーポリシー

Twitter

Books