イネマルのプログラミング備忘録

趣味プログラマーのメモ

バッチファイルの PowerShell 起動ワンライナーを 管理者権限 で実行する

バッチファイルのPowerShell起動ワンライナーを管理者権限で実行する

バッチファイルからPowerShellを起動するワンライナーは、
多種ありますが、管理者権限なしで、実行された場合、
ドライブ等のアクセスに制限がかかります。

ワンライナーを使用する場合、
管理者権限で再起動させると動作に若干、癖があるようです。
ProcessStartInfoの動作で無駄にハマったので、対応方法を考えてみました。
以下は、管理者権限で起動する方法の一例です。

ワンライナーは、reosabloさんの記事の物をベースに使用させて頂きます。
reosablo.hatenablog.jp

注意

今回は、C#コードをPowerShellから使用しています。
おそらく、PowerShellのみでも実装可能だと思います。

また、C#コードを経由して、権限を昇格させる方法なので、
初回起動時は、起動待ちが発生すると思われます。

確認用のプログラム

管理者権限で起動されなかった場合は、
UACによって権限を昇格させて再起動します。

バッチファイルを保存する際は、文字コードに注意が必要です。
(日本語環境では、ShiftJISにすれば問題ないと思われます。)

※動作確認は、Windows10で行いました。

template.bat

@setlocal enabledelayedexpansion&set a=%*&(if defined a set a=!a:"=\"!&set a=!a:'=''!)&@start /min powershell -windowstyle hidden -sta /c $i=$input;iex ('$i^|^&{$PSCommandPath=\"%~f0\";$PSScriptRoot=\"%~dp0";#'+(${%~f0}^|Out-String)+'} '+('!a!'-replace'[$(),;@`{}]','`$0'))&exit/b
<#
/// <summary>
/// 管理者権限で実行するサンプル
/// </summary>
#>

# アプリケーション名
set-variable -option readonly -name APP_NAME -value "テンプレート"

# フォームサイズ
set-variable -option readonly -name FORM_SIZE -value "640, 480"
set-variable -option readonly -name FORM_MIN_SIZE -value $FORM_SIZE

# 管理者権限
set-variable -option readonly -name ENABLE_ADMIN -value "true"

<#
	開発者向け定義
#>

# アプリケーションファイル名
set-variable -option readonly -name APP_FILE_NAME -value (Split-Path -Leaf $PSCommandPath)
set-variable -option readonly -name APP_FILE_BASE_NAME -value (Get-Item $APP_FILE_NAME).BaseName

#<# -------------------------------------------------------------------------------------------------------
Add-Type -IgnoreWarnings -Language CSharpVersion3 `
-ReferencedAssemblies (
#	"System",
	"System.Drawing",
	"System.Windows.Forms"
) `
-TypeDefinition @"
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Principal;
using System.Text;
using System.Drawing;
using System.Windows.Forms;

public class MainForm: Form
{
	public MainForm()
	{
		if (Admin.IsAdmin) {
			this.Text = "$APP_NAME (管理者)";
		}
		else {
			this.Text = "$APP_NAME";
		}
		this.FormBorderStyle = FormBorderStyle.Sizable;
		this.Size = new Size($FORM_SIZE);
		this.MinimumSize = new Size($FORM_MIN_SIZE);
	}
}

public static class Admin
{
	public static bool RestartAdmin(Object[] Args)
	{
		if (IsAdmin) {
			return true;
		}
		var psi = new ProcessStartInfo("cmd", string.Format("/C \"\"{0}\" {1}\"",
			@"$PSCommandPath",
			string.Join(" ", Args
				.Select(x => string.Format("\"{0}\"", x))
				.ToArray())));
		psi.Verb = "runas";
		try {
			Process.Start(psi);
		}
		catch (System.ComponentModel.Win32Exception){}
		return false;
	}
	
	public static bool IsAdmin
	{ 
		get {
			var principal = new WindowsPrincipal(WindowsIdentity.GetCurrent());
			return principal.IsInRole(WindowsBuiltInRole.Administrator);
		}
	}
}

public static class Entry
{
	[STAThread]
	public static void Run(Object[] Args)
	{
#if $ENABLE_ADMIN
		if (Admin.RestartAdmin(Args)) {
			MainProc(Args);
		}
#else
		MainProc(Args);
#endif
	}
	
	static void MainProc(Object[] Args)
	{
		MainForm f = new MainForm();
		f.ShowDialog();
	}
}
"@
# -------------------------------------------------------------------------------------------------------#>

# アセンブリ読み込み
Add-Type -Assembly System.Windows.Forms

try
{
	# 実行
	[Entry]::Run($args)
}
catch [Exception] {
	# 例外
	[System.Windows.Forms.MessageBox]::Show($error, "エラー", "OK", "Error")
}
finally {
	# プロセス解放
	[GC]::Collect()
}

その他

ENABLE_ADMIN の初期値を"false"にすれば、管理者起動を強制しないようになります。

あとがき

PowerShellコマンドプロンプトで起動すると、
使用するコマンドプロンプトPowerShellに乗っ取られるような動作をします。
バッチは、知識があまりないので最適な方法かわかりませんが、
対応策として、start コマンドを使用して別枠起動させています。

また、フォームを表示する際に、コマンドプロンプトが表示されるのが気になったので、
最低限の表示で済むように書いたつもりです。
(batをダブルクリックした時点で、一瞬は表示されますが、仕様です。)