2017年2月6日月曜日

HTAからPowerShellの機能を利用するサンプル

フォルダ選択ダイアログを表示したくて、できればデフォルト値も設定できるメソッドを利用したいと思いました。その要求にマッチするものがPowerShellで利用できることが分かったので、思い通りに使えるようにするついでにPowerShellをHTAから利用するためのサンプルを作りました。


PowerShellはコマンドラインからワンライナーを実行させることができます。



これだけで、以下のように少し複雑なことも出来るのですが



コマンドライン経由なので、エスケープに対する配慮が必要で、面倒です。
例えばダブルクォーテーションをを含むスクリプトをPowerShellで扱いたい場合、以下のようになります。(PowerShellには「@(1,1,2,3,5,8,13) -join "`n`r"」という文字列が渡ることになります)



そういう制約から逃れる手段として、「ps1」形式のファイルを作成して実行するという方法がありますが



ps1という拡張子のファイルはデフォルト設定では実行できない設定になっています。



ただし、PowerShellの起動オプションで実行ポリシーを変更したセッションを用意すればps1ファイルも実行できますし、少し長くなりますがファイルを読み込んでテキストをスクリプトとして評価するメソッドに渡して実行させることもできます。


・・・というのを実際に動作確認したソースが以下。
<html>
<body>
</body>
<script>
AXO = function(name){return new ActiveXObject(name)}
shell = AXO('WScript.Shell')
fs = AXO('Scripting.FileSystemObject')
gsf2 = fs.GetSpecialFolder(2)
Read = function(p){var o=fs.OpenTextFile(p), s=o.AtEndOfStream?'':o.ReadAll(); o.Close(); return s}
Write = function(path,v){with(fs.CreateTextFile(path)){Write(v);return Close()}}
インデント調整=function(){
var reg=/(\n[ \t]+)[^ \t]*$/, g='g', n='\n'
return function(str){ str+=''; return str.match(reg) ? str.replace(new RegExp(RegExp.$1,g), n) : str }
}()
com=function(){
var reg0=/\/\*([^\v]*[^\r\n \t])[\r\n \t]*\*\//, reg1=/^[\r\n]*/g, L0=''
return function(fun){ return インデント調整((fun+'').match(reg0)[1]).replace(reg1,L0) }
}()
PowerShell={
実行:function(cmd, op){
var path=gsf2+'/'+(new Date()).getTime()
shell.run('cmd /C powershell '+(op||'')+' "'+cmd.replace(/"/g,'""')+'" > '+path, 0, true)
var ret=''
if(fs.FileExists(path)){
ret = Read(path)
fs.DeleteFile(path)
}
return ret
},
FolderBrowserDialog:function(defaultPath, rootPath, swNewFolderNG){
var ret=this.実行(com(function(){/*
Add-Type -assemblyName System.Windows.Forms;
$d=new-object System.Windows.Forms.FolderBrowserDialog;
$d.ShowDialog();
$d.SelectedPath;
*/}).replace(/[\r\n\t]/g,''), '-sta')
return ret
},
ReadFile:function(path){
var ret=this.実行(com(function(){/*
$file=new-object System.IO.StreamReader('-path-');
$arr=@();
while(($line = $file.ReadLine()) -ne $null){ $arr += $line };
$file.Close();
echo($arr -join "`n`r"")
*/}).replace(/[\r\n\t]/g,'').replace(/-path-/,path))
return ret
},
ReadEval:function(str){
// コマンドライン経由ではエスケープでややこしくなるようなスクリプトを実行するためのメソッド。
var path=gsf2+'/'+(new Date()).getTime()
Write(path, str)
var ret=this.実行(com(function(){/*
$file=new-object System.IO.StreamReader('-path-');
$arr=@();
while(($line = $file.ReadLine()) -ne $null){ $arr += $line };
$file.Close();
Invoke-Expression ($arr -join "`n`r"") # ←「`r」の後ろの「"」は1つで良いと思うが、実際に動かすと1つでは「終端記号無し」エラーになってしまう。
*/}).replace(/[\r\n\t]/g,'').replace(/-path-/,path), '-sta')
fs.DeleteFile(path)
return ret
},
FolderBrowserDialog:function(defaultPath, rootPath, swNewFolderNG){
// https://msdn.microsoft.com/ja-jp/library/system.windows.forms.folderbrowserdialog(v=vs.110).aspx
// rootPathに入れられる値はEnvironment.SpecialFolder。任意のフォルダをルートとして表示することはできない。
// https://msdn.microsoft.com/ja-jp/library/system.windows.forms.folderbrowserdialog.rootfolder(v=vs.110).aspx
// https://msdn.microsoft.com/ja-jp/library/system.environment.specialfolder(v=vs.110).aspx
// Environment.SpecialFolderについてはネットにいくつか情報があるが、自分の環境ではいずれも正常動作しなかった。
// http://www.atmarkit.co.jp/fdotnet/dotnettips/056folderdlg/folderdlg.html
var code=com(function(){/*
Add-Type -assemblyName System.Windows.Forms;
$d=new-object System.Windows.Forms.FolderBrowserDialog;
-option-
$d.ShowDialog();
$d.SelectedPath;
*/}).replace(/[\r\n\t]/g,'')
var arr=[]
if(defaultPath ){ arr.push('$d.SelectedPath = "'+defaultPath+'";') }
if( rootPath ){ arr.push('$d.RootFolder = Environment.SpecialFolder.'+rootPath+';') }
if(swNewFolderNG){ arr.push('$d.ShowNewFolderButton = $false;') }
code = code.replace(/-option-/, arr.join('\r\n').replace(/\//g,'\\')) // ←Pathが「/」区切りのままだと正常に動作しない。
return this.ReadEval(code)
},
ps1実行:function(pathPS1, op){
// デフォルト状態ではps1ファイルは実行できない設定になっているが
// 起動オプションで実行ポリシーを変更したセッションを用意すれば実行できる。
// http://qiita.com/tomoko523/items/df8e384d32a377381ef9
var path=gsf2+'/'+(new Date()).getTime()
shell.run('cmd /C powershell '+(op||'')+' -NoProfile -ExecutionPolicy Unrestricted '+pathPS1+' > '+path, 0, true)
var ret=''
if(fs.FileExists(path)){
ret = Read(path)
fs.DeleteFile(path)
}
return ret
},
ps1CreateExec:function(str){
// コマンドライン経由ではエスケープでややこしくなるようなスクリプトを実行するためのメソッド。
var path=gsf2+'/'+(new Date()).getTime()+'.ps1'
Write(path, str)
var ret=this.ps1実行(path, '-sta')
fs.DeleteFile(path)
return ret
},
FolderBrowserDialog:function(defaultPath, rootPath, swNewFolderNG){
// https://msdn.microsoft.com/ja-jp/library/system.windows.forms.folderbrowserdialog(v=vs.110).aspx
// rootPathに入れられる値はEnvironment.SpecialFolder。任意のフォルダをルートとして表示することはできない。
// https://msdn.microsoft.com/ja-jp/library/system.windows.forms.folderbrowserdialog.rootfolder(v=vs.110).aspx
// https://msdn.microsoft.com/ja-jp/library/system.environment.specialfolder(v=vs.110).aspx
// Environment.SpecialFolderについてはネットにいくつか情報があるが、自分の環境ではいずれも正常動作しなかった。
// http://www.atmarkit.co.jp/fdotnet/dotnettips/056folderdlg/folderdlg.html
var code=com(function(){/*
Add-Type -assemblyName System.Windows.Forms;
$d=new-object System.Windows.Forms.FolderBrowserDialog;
-option-
$d.ShowDialog();
$d.SelectedPath;
*/}).replace(/[\r\n\t]/g,'')
var arr=[]
if(defaultPath ){ arr.push('$d.SelectedPath = "'+defaultPath+'";') }
if( rootPath ){ arr.push('$d.RootFolder = Environment.SpecialFolder.'+rootPath+';') }
if(swNewFolderNG){ arr.push('$d.ShowNewFolderButton = $false;') }
code = code.replace(/-option-/, arr.join('\r\n').replace(/\//g,'\\')) // ←Pathが「/」区切りのままだと正常に動作しない。
return this.ps1CreateExec(code)
}
}
switch(3){
case 0:
document.body.innerText = PowerShell.FolderBrowserDialog()
break
case 1:
document.body.innerText = PowerShell.ReadFile('C/aaaaa.txt')
break
case 2:
document.body.innerText = PowerShell.ReadEval(com(function(){/*
Add-Type -assemblyName System.Windows.Forms;
$d=new-object System.Windows.Forms.FolderBrowserDialog;
$d.ShowDialog();
$d.SelectedPath;
exit; # ←exitを入れないとHTA側のshell.runが終了待ちのまま固まってしまう。コマンドラインから実行する場合はなくても大丈夫。
*/}).replace(/\t/g,''))
break
case 3:
document.body.innerText = PowerShell.FolderBrowserDialog('', '')
break
}
</script>
</html>
view raw powershell.hta hosted with ❤ by GitHub


メソッドの名前が重複しているところがあるのは動作確認の名残です。

ソースを見てお気づきになるかもしれませんが、実行ポリシー変更セッションならps1ファイルを実行できるということを知ったのは動作確認の終盤でした。
ファイルを読んでスクリプトとして評価するサンプルも有っても良いと思い、今回の目的に対しては若干冗長な手法でしたが情報として残すことにしました。

1 件のコメント:

  1. Shell.Applicationに「BrowseForFolder」というメソッドがありますが、これではデフォルトが設定できません。。
    http://www.haijin-boys.com/wiki/%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80%E9%81%B8%E6%8A%9E%E3%83%80%E3%82%A4%E3%82%A2%E3%83%AD%E3%82%B0

    返信削除