none
拡張子に関連付けされた実行ファイルのパスを取得できません。 RRS feed

  • 質問

  • Win32APIのshlwapi.dllのAssocQueryString()関数を用いて、PowerShell上で任意の拡張子に関連付けられたアプリの実行ファイルのパスの取得を試みましたが、うまくいきません。

    まず、shlwapi.dllのAssocQueryString()関数を呼び出すクラスを以下のようにC#で定義して、shlwapi.csという名前で保存します。

    using System;
    using System.Runtime.InteropServices;
    using System.Text;
    
    
    
    [Flags]
    public enum AssocF {
    	None					= 0,
    	Init_NoRemapCLSID		= 0x1,
    	Init_ByExeName			= 0x2,
    	Open_ByExeName			= 0x2,
    	Init_DefaultToStar		= 0x4,
    	Init_DefaultToFolder	= 0x8,
    	NoUserSettings			= 0x10,
    	NoTruncate				= 0x20,
    	Verify					= 0x40,
    	RemapRunDll				= 0x80,
    	NoFixUps				= 0x100,
    	IgnoreBaseClass			= 0x200,
    	Init_IgnoreUnknown		= 0x400,
    	Init_FixedProgId		= 0x800,
    	IsProtocol				= 0x1000,
    	InitForFile				= 0x2000,
    	
    }
    
    
    public enum AssocStr {
    	Command 				= 1,
    	Executable,
    	FriendlyDocName,
    	FriendlyAppName,
    	NoOpen,
    	ShellNewValue,
    	DDECommand,
    	DDEIfExec,
    	DDEApplication,
    	DDETopic,
    	InfoTip,
    	QuickTip,
    	TileInfo,
    	ContentType,
    	DefaultIcon,
    	ShellExtension,
    	DropTarget,
    	DelegateExecute,
    	SupportedUriProtocols,
    	Max,
    	
    }
    
    
    public class Shlwapi {
    	[DllImport(
    		"shlwapi.dll",
    		SetLastError	= true,
    		CharSet			= CharSet.Auto
    		
    	)]
    	public static extern uint AssocQueryString(
    		AssocF flags,
    		AssocStr str,
    		string pszAssoc,
    		string pszExtra,
    		[Out] StringBuilder pszOut,
    		[In][Out] ref uint pcchOut
    		
    	);
    	
    }
    
    
    
    

    次に、入力された".txt"などの拡張子の文字列から関連付けられた実行ファイルパスの文字列を出力するPowerShell関数のGet-FileAssocPathを定義したスクリプトを以下のように作成して、同じフォルダーにshlwapi.ps1という名前で保存します。

    Set-StrictMode -Version '2.0'
    $ErrorActionPreference	= 'Stop'
    
    
    
    #C#で定義した関数を読み込む。
    Add-Type `
    	-LiteralPath ( [System.IO.Path]::Combine(
    		[System.IO.Path]::GetDirectoryName( $PSCommandPath ),
    		'shlwapi.cs'
    		
    	) )
    
    
    #指定された拡張子に関連付けられた実行ファイルのパスを取得する。
    function Get-FileAssocPath {
    	[CmdletBinding( PositionalBinding	= $true )]
    	[OutputType( [string[]] )]
    	
    	Param(
    		#".txt"などの拡張子。
    		[Parameter(
    			Mandatory			= $true,
    			Position			= 0,
    			ValueFromPipeline	= $true
    			
    		)]
    		[AllowNull()]
    		[AllowEmptyString()]
    		[AllowEmptyCollection()]
    		[string[]] $ExtNames
    		
    	)
    	
    	begin {
    	}
    	
    	process {
    		foreach ( $extName in $ExtNames ) {
    			#pszOutのサイズを取得するための1回目の実行。
    			[uint32] $pcchOut	= 0
    			
    			#ASSOCF_INIT_IGNOREUNKNOWNで関連付けられていないものを無視。
    			Write-Host `
    				-Object ( `
    					'AssocQueryString( 1回目 ) = 0x{0:X8}' -f [Shlwapi]::AssocQueryString(
    						[AssocF]::Init_IgnoreUnknown,
    						[AssocStr]::Executable,
    						$extName,
    						'',
    						$null,
    						( [ref] $pcchOut )
    						
    					) `
    					 `
    				) `
    				-ForegroundColor Magenta
    			
    			#取得したpszOutのサイズを表示。
    			Write-Host `
    				-Object ( 'pcchOut = {0:D}' -f $pcchOut ) `
    				-ForegroundColor Magenta
    				
    			if ( $pcchOut -eq 0 ) {
    				#見つからなかった時は、空の文字列を返す。
    				Write-Output -InputObject ''
    				
    			} else {
    				#結果を受け取るためのStringBuilderオブジェクトを作成する。
    				[System.Text.StringBuilder] $pszOut	= `
    					[System.Text.StringBuilder]::new( [int] $pcchOut )
    				
    				#関連付けられた実行ファイルのパスを取得するための2回目の実行。
    				Write-Host `
    					-Object ( `
    						'AssocQueryString( 2回目 ) = 0x{0:X8}' -f [Shlwapi]::AssocQueryString(
    							[AssocF]::Init_IgnoreUnknown,
    							[AssocStr]::Executable,
    							$extName,
    							'',
    							$pszOut,
    							( [ref] $pcchOut )
    							
    						) `
    						 `
    					) `
    					-ForegroundColor Magenta
    				
    				#結果を返す。
    				Write-Output -InputObject $pszOut.ToString()
    				
    			}
    			
    		}
    		
    	}
    	
    	end {
    	}
    	
    }
    
    
    
    

    そして、PowerShellコンソール上でこれを動かしてみました。

    #作成した関数を読み込む。 . '.\shlwapi.ps1' #自分の環境で有効な3個の拡張子で試してみる。 @( '.txt', '.bmp', '.avi' ) | Get-FileAssocPath


    しかし、実際には以下のようになってうまくいきませんでした。

    AssocQueryString( 1回目 ) = 0x80070483
    pcchOut = 0
    
    AssocQueryString( 1回目 ) = 0x80070483
    pcchOut = 0
    
    AssocQueryString( 1回目 ) = 0x80070483
    pcchOut = 0
    
    

    すなわち、実行ファイルパス文字列を格納するStringBuilderのサイズである$pcchOutを決定する1回目のAssocQueryString()の実行段階でエラー0x80070483が返ってきてしまいます。$pcchOutは0でif文で長さ0の文字力を出力しています。

    $pcchOutの初期値を1024に変えたり、AssocF列挙体の値をInit_IgnoreUnknown以外に変えたりしましたが駄目でした。

    環境は、Windows 10 Pro x64 1809、PowerShell 5.1、.Net Framework 4.7.2で、テストした3個の拡張子はいずれもデスクトップアプリに関連付けられています。

    なお、コードの作成にあたっては、

    https://dobon.net/vb/dotnet/system/findassociatedexe.html

    を参考にさせていただきました。

    2019年9月5日 3:47

回答

  • あれから更に調べたのですが、メソッドの引数に$nullの代わりに[NullString]::Valueを渡すことでも、null→空文字の変換を回避できます。
    • 回答としてマーク fzok4234 2019年9月6日 3:55
    2019年9月5日 11:01
    モデレータ
  • Hongliangさんのご指摘通り、AssocQueryStringの第4引数pszExtraの値はstring.Emptyではなくnullを渡す必要があります。

    しかし、PowerShellでstring型を引数にとるメソッドにnullを渡す場合、通常の.演算子によるメンバ呼び出しを行うと、nullがstring.Emptyに変換されてしまうという仕様があります。

    参照:stringを引数に取るメソッドに$nullを渡すと勝手にstring.Emptyに変換されてしまう - PowerShell Scripting Weblog

    この変換を回避するには、以下のようにリフレクションを利用する方法があります。

    (もっとも、C#側で拡張子を引数に取り、実行パスを結果として返すメソッドを定義しておく方が楽だとは思います)

    Set-StrictMode -Version '2.0'
    $ErrorActionPreference	= 'Stop'
    
    #C#で定義した関数を読み込む。
    $Shlwapi = Add-Type `
    	-LiteralPath ( [System.IO.Path]::Combine(
    		[System.IO.Path]::GetDirectoryName( $PSCommandPath ),
    		'shlwapi.cs'
    		
    	) ) -PassThru | where Name -eq "Shlwapi"
    
    #指定された拡張子に関連付けられた実行ファイルのパスを取得する。
    function Get-FileAssocPath {
    	[CmdletBinding( PositionalBinding	= $true )]
    	[OutputType( [string[]] )]
    	
    	Param(
    		#".txt"などの拡張子。
    		[Parameter(
    			Mandatory			= $true,
    			Position			= 0,
    			ValueFromPipeline	= $true
    			
    		)]
    		[AllowNull()]
    		[AllowEmptyString()]
    		[AllowEmptyCollection()]
    		[string[]] $ExtNames
    		
    	)
    	
    	begin {
    	}
    	
    	process {
    		foreach ( $extName in $ExtNames ) {
    			#pszOutのサイズを取得するための1回目の実行。
    			[uint32] $pcchOut	= 0
    			#ASSOCF_INIT_IGNOREUNKNOWNで関連付けられていないものを無視。
    			$arguments = @(
    				[AssocF]::Init_IgnoreUnknown,
    				[AssocStr]::Executable,
    				$extName,
    				$null,
    				$null,
    				$null
    			)
    			$ret = $Shlwapi.GetMethod("AssocQueryString").Invoke($null, $arguments)
    			$pcchOut = $arguments[5]
    
    			Write-Host `
    				-Object ('AssocQueryString( 1回目 ) = 0x{0:X8}' -f $ret) `
    				-ForegroundColor Magenta
    
    			#取得したpszOutのサイズを表示。
    			Write-Host `
    				-Object ( 'pcchOut = {0:D}' -f $pcchOut ) `
    				-ForegroundColor Magenta
    
    			if ( $pcchOut -eq 0 ) {
    				#見つからなかった時は、空の文字列を返す。
    				Write-Output -InputObject ''
    				
    			} else {
    				#結果を受け取るためのStringBuilderオブジェクトを作成する。
    				[System.Text.StringBuilder] $pszOut	= `
    					[System.Text.StringBuilder]::new( [int] $pcchOut )
    				
    				#関連付けられた実行ファイルのパスを取得するための2回目の実行。
    				$arguments = @(
    					[AssocF]::Init_IgnoreUnknown,
    					[AssocStr]::Executable,
    					$extName,
    					$null,
    					$pszOut,
    					$pcchOut
    				)
    				$ret = $Shlwapi.GetMethod("AssocQueryString").Invoke($null, $arguments)
    				Write-Host `
    					-Object ( 'AssocQueryString( 2回目 ) = 0x{0:X8}' -f $ret) `
    					-ForegroundColor Magenta
    				
    				#結果を返す。
    				Write-Output -InputObject $pszOut.ToString()
    				
    			}
    			
    		}
    		
    	}
    	
    	end {
    	}
    }
    
    



    2019年9月5日 5:39
    モデレータ

すべての返信

  • 参考ページのソースでは、AssocQueryStringの第4引数pszExtraの値はnullになっていますが?
    2019年9月5日 4:08
  • Hongliangさんのご指摘通り、AssocQueryStringの第4引数pszExtraの値はstring.Emptyではなくnullを渡す必要があります。

    しかし、PowerShellでstring型を引数にとるメソッドにnullを渡す場合、通常の.演算子によるメンバ呼び出しを行うと、nullがstring.Emptyに変換されてしまうという仕様があります。

    参照:stringを引数に取るメソッドに$nullを渡すと勝手にstring.Emptyに変換されてしまう - PowerShell Scripting Weblog

    この変換を回避するには、以下のようにリフレクションを利用する方法があります。

    (もっとも、C#側で拡張子を引数に取り、実行パスを結果として返すメソッドを定義しておく方が楽だとは思います)

    Set-StrictMode -Version '2.0'
    $ErrorActionPreference	= 'Stop'
    
    #C#で定義した関数を読み込む。
    $Shlwapi = Add-Type `
    	-LiteralPath ( [System.IO.Path]::Combine(
    		[System.IO.Path]::GetDirectoryName( $PSCommandPath ),
    		'shlwapi.cs'
    		
    	) ) -PassThru | where Name -eq "Shlwapi"
    
    #指定された拡張子に関連付けられた実行ファイルのパスを取得する。
    function Get-FileAssocPath {
    	[CmdletBinding( PositionalBinding	= $true )]
    	[OutputType( [string[]] )]
    	
    	Param(
    		#".txt"などの拡張子。
    		[Parameter(
    			Mandatory			= $true,
    			Position			= 0,
    			ValueFromPipeline	= $true
    			
    		)]
    		[AllowNull()]
    		[AllowEmptyString()]
    		[AllowEmptyCollection()]
    		[string[]] $ExtNames
    		
    	)
    	
    	begin {
    	}
    	
    	process {
    		foreach ( $extName in $ExtNames ) {
    			#pszOutのサイズを取得するための1回目の実行。
    			[uint32] $pcchOut	= 0
    			#ASSOCF_INIT_IGNOREUNKNOWNで関連付けられていないものを無視。
    			$arguments = @(
    				[AssocF]::Init_IgnoreUnknown,
    				[AssocStr]::Executable,
    				$extName,
    				$null,
    				$null,
    				$null
    			)
    			$ret = $Shlwapi.GetMethod("AssocQueryString").Invoke($null, $arguments)
    			$pcchOut = $arguments[5]
    
    			Write-Host `
    				-Object ('AssocQueryString( 1回目 ) = 0x{0:X8}' -f $ret) `
    				-ForegroundColor Magenta
    
    			#取得したpszOutのサイズを表示。
    			Write-Host `
    				-Object ( 'pcchOut = {0:D}' -f $pcchOut ) `
    				-ForegroundColor Magenta
    
    			if ( $pcchOut -eq 0 ) {
    				#見つからなかった時は、空の文字列を返す。
    				Write-Output -InputObject ''
    				
    			} else {
    				#結果を受け取るためのStringBuilderオブジェクトを作成する。
    				[System.Text.StringBuilder] $pszOut	= `
    					[System.Text.StringBuilder]::new( [int] $pcchOut )
    				
    				#関連付けられた実行ファイルのパスを取得するための2回目の実行。
    				$arguments = @(
    					[AssocF]::Init_IgnoreUnknown,
    					[AssocStr]::Executable,
    					$extName,
    					$null,
    					$pszOut,
    					$pcchOut
    				)
    				$ret = $Shlwapi.GetMethod("AssocQueryString").Invoke($null, $arguments)
    				Write-Host `
    					-Object ( 'AssocQueryString( 2回目 ) = 0x{0:X8}' -f $ret) `
    					-ForegroundColor Magenta
    				
    				#結果を返す。
    				Write-Output -InputObject $pszOut.ToString()
    				
    			}
    			
    		}
    		
    	}
    	
    	end {
    	}
    }
    
    



    2019年9月5日 5:39
    モデレータ
  • AssocQueryString()の引数pszExtraに$nullを指定してみましたが、結果は変わりませんでした。

    この引数pszExtraは拡張子に関連付けられたProgIDで定義されたVerb名を文字列で指定するものですが、アイコンのダブルクリックで呼び出されるVerbはProgIDによってまちまちなため、ここに特定のVerb名を指定するわけにはいきません。

    試しに、pszExtraに明示的にVerb名"Open"を指定したうえで、列挙体AssocFの値をNoneに変えて無効な関連付けを無視しないようにして、以下のコマンド

    @( '.txt', '.avi', '.cpl', '.bmp', '.qqqqq' ) | Get-FileAssocPath
    

    を実行したところ、次のようになりました。

    AssocQueryString( 1回目 ) = 0x00000001 pcchOut = 25 AssocQueryString( 2回目 ) = 0x00000000 C:\Apps\bin\Hidemaru.exe AssocQueryString( 1回目 ) = 0x00000001 pcchOut = 26 AssocQueryString( 2回目 ) = 0x00000000 C:\Apps\MPC-BE\mpc-be.exe AssocQueryString( 1回目 ) = 0x80070483 pcchOut = 0 AssocQueryString( 1回目 ) = 0x80070483 pcchOut = 0 AssocQueryString( 1回目 ) = 0x00000001 pcchOut = 33 AssocQueryString( 2回目 ) = 0x00000000 C:\Windows\System32\OpenWith.exe

    当方の環境で"Open"Verbが定義済みである拡張子".txt"と".avi"では正常に動作し、"Open"Verbが定義されていない".cpl"と".bmp"ではエラー0x80070483となって動作しませんでした。また、当方の環境に登録していない".qqqqq"では正常動作扱いで開くアプリの選択を促すOpenWith.exeが返ってきました。

    このことから、アイコンのダブルクリックで呼び出されるデフォルトVerbを判別するルーチンを組み込まないといけないことがわかりました。

    2019年9月5日 6:03
  • しかし、PowerShellでstring型を引数にとるメソッドにnullを渡す場合、通常の.演算子によるメンバ呼び出しを行うと、nullがstring.Emptyに変換されてしまうという仕様があります。

    へえ、これは知りませんでした。頭にメモっておこう。

    今回の場合、pszExtraを [In] StringBuilder pszExtra と定義しておくことで回避はできますね。まあ私もC#でメソッド化しちゃいますが。

    2019年9月5日 6:06
  • ありがとうございます。おかげで意図したとおりに動くようになりました。

    しかし、C#などで定義したメソッドの文字列型の引数にPowerShellからnullを渡すと自動的に空文字列に変換されるとは、かなり痛い罠でした。

    2019年9月5日 10:45
  • あれから更に調べたのですが、メソッドの引数に$nullの代わりに[NullString]::Valueを渡すことでも、null→空文字の変換を回避できます。
    • 回答としてマーク fzok4234 2019年9月6日 3:55
    2019年9月5日 11:01
    モデレータ
  • ありがとうございます。こちらでも動作確認できました。

    2019年9月6日 3:55