powershell.exe -file vs. -command
I am working on a PowerShell script that creates a GUI, using classes from Windows.Forms. The script runs fine in the PowerShell ISE, but when I select "Run with PowerShell" from the context menu in File Explorer, it fails and doesn't find the functions defined in the script.
Digging a bit further into the problem, I found that the "Run with PowerShell" invocation more or less executes a command line like
powershell.exe -Command "script.ps1"
while running the script within the ISE is equivalent to
powershell.exe -File "script.ps1"
Running these two commands from a cmd window exactly reproduce what I observed: The -File
version works well and the -Command
version fails.
I am including a stripped-down version of the script. It creates a GUI with the buttons "Save configuration" and "Exit". The "Exit" button does what you expect, and "Save configuration" calls the functions Get-Configuration-Data-from-GUI
and Save-Configuration
(which both do nothing). When you launch the script in the ISE or via the -File
parameter, all is well, but when you try "Run with PowerShell" or the -Command
parameter, you see these errors when you click "Save configuration":
Get-Configuration-Data-from-GUI : The term 'Get-Configuration-Data-from-GUI' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At C:\Projects\Internal\Tools\VersionInventoryTool\trunk\source\VersionInventory.ps1:54 char:9
...
Save-Configuration : The term 'Save-Configuration' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At C:\Projects\Internal\Tools\VersionInventoryTool\trunk\source\VersionInventory.ps1:55 char:9
...
This is what's left of my script after removing all unnecessary parts; the error is reported from the script block that is the argument to $save_button.Add_Click
:
using namespace System.Windows.Forms
using namespace System.Drawing
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
function Save-Configuration()
{
}
function Get-Configuration-Data-from-GUI($gui)
{
}
function New-Element
{
param(
[parameter()] [string] $class,
[parameter()] [int] $x,
[parameter()] [int] $y,
[parameter()] [int] $width,
[parameter()] [int] $height,
[parameter()] [string] $text,
[parameter(Mandatory = $false)] [Form] $parent
)
$ret = New-Object $class
if ($width -gt 0)
{
$ret.Width = $width
}
if ($height -gt 0)
{
$ret.Height = $height
}
$ret.Location = New-Object System.Drawing.Point($x, $y)
$ret.Text = $text
if ($parent -ne $null)
{
$parent.Controls.Add($ret)
}
return $ret
}
function Create-GUI()
{
$main_form = New-Element Form -x 100 -y 100 -width 600 -height 500 -text "My tool"
$border = 10
$width = $main_form.ClientRectangle.Width - 2 * $border
$save_button = New-Element Button -text "Save configuration" -x $border -width 120 -parent $main_form
$save_button.Top = $main_form.ClientRectangle.Bottom - $border - $save_button.Height
$save_button.Add_Click({
Get-Configuration-Data-from-GUI $main_form
Save-Configuration
}.GetNewClosure())
$exit_button = New-Element Button -text Exit -y $save_button.Top -parent $main_form
$exit_button.Left = $main_form.ClientRectangle.Right - $border - $exit_button.Width
$exit_button.DialogResult = [DialogResult]::OK
return $main_form
}
$gui = Create-Gui
$gui.ShowDialog()
Hoping for insights...
Greetings Hans
Solution 1:
After Create-GUI()
is done, the scope for {...}
in button.Add_Click({...$main_form...}.GetNewClosure())
is kinda left dangling which is why it can't get back to $main_form
; you've over compensated for this by adding the .GetNewClosure()
which more or less creates a whole new scope and copies $main_form
into it. However this new scope is tenuously attached to the scope that Create-GUI()
lives in, and like you've seen depending on how Create-GUI()
is sourced into the runspace (&
aka -Command
vs .
aka -File
), then {}.GetNewClosure()
may or may not have affinity to Create-GUI()
's parent runspace.
To get around this I've dropped the .GetNewClosure()
to keep {...}
in the runspace it looks like it should live in, and moved the .ShowDialogue()
into Create-GUI()
so that $main_form
doesn't go out of scope.
using namespace System.Windows.Forms
using namespace System.Drawing
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
function Save-Configuration{
param($cfg)
Write-Host ('Mocked: Save-Configuration({0})' -f $cfg) -ForegroundColor Magenta
}
function Get-Configuration-Data-from-GUI{
param($gui)
Write-Host ('Mocked: Get-Configuration-Data-from-GUI({0})' -f $gui.Text) -ForegroundColor Magenta
return $gui.Text
}
function New-Element{
param(
[parameter()] [string] $class,
[parameter()] [int] $x,
[parameter()] [int] $y,
[parameter()] [int] $width,
[parameter()] [int] $height,
[parameter()] [string] $text,
[parameter(Mandatory = $false)] [Form] $parent
)
$ret = New-Object $class
if($width -gt 0){
$ret.Width = $width
}
if($height -gt 0){
$ret.Height = $height
}
$ret.Location = New-Object System.Drawing.Point($x, $y)
$ret.Text = $text
if($parent -ne $null){
$parent.Controls.Add($ret)
}
return $ret
}
function Show-GUI(){
$gui_data = [ordered]@{
cfg = ''
res = $null
}
$main_form = New-Element Form -x 100 -y 100 -width 600 -height 500 -text "My tool"
$border = 10
$width = $main_form.ClientRectangle.Width - 2 * $border
$save_button = New-Element Button -text "Save configuration" -x $border -width 120 -parent $main_form
$save_button.Top = $main_form.ClientRectangle.Bottom - $border - $save_button.Height
$save_button.Add_Click({
$gui_data.cfg = Get-Configuration-Data-from-GUI $main_form
Save-Configuration $gui_data.cfg
})
$exit_button = New-Element Button -text Exit -y $save_button.Top -parent $main_form
$exit_button.Left = $main_form.ClientRectangle.Right - $border - $exit_button.Width
$exit_button.DialogResult = [DialogResult]::OK
$gui_data.res = $main_form.ShowDialog()
return $gui_data
}
Show-GUI
pause
You could even take it a step further by either eliminating Save-Configuration
and Get-Configuration-Data-from-GUI
, or moving them into the same scope as button.Add_Click({...})
. Which would then allow these two functions to access the form, controls, and data directly instead of needing to pass arguments.
using namespace System.Windows.Forms
using namespace System.Drawing
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
function Save-Configuration{
Write-Host ('Mocked: Save-Configuration({0})' -f $gui_data.cfg) -ForegroundColor Magenta
}
function Get-Configuration-Data-from-GUI{
Write-Host ('Mocked: Get-Configuration-Data-from-GUI({0})' -f $main_form.Text) -ForegroundColor Magenta
$gui_data.cfg = $main_form.Text
}
function New-Element{
param(
[parameter()] [string] $class,
[parameter()] [int] $x,
[parameter()] [int] $y,
[parameter()] [int] $width,
[parameter()] [int] $height,
[parameter()] [string] $text,
[parameter(Mandatory = $false)] [Form] $parent
)
$ret = New-Object $class
if($width -gt 0){
$ret.Width = $width
}
if($height -gt 0){
$ret.Height = $height
}
$ret.Location = New-Object System.Drawing.Point($x, $y)
$ret.Text = $text
if($parent -ne $null){
$parent.Controls.Add($ret)
}
return $ret
}
$gui_data = [ordered]@{
cfg = ''
res = $null
}
$main_form = New-Element Form -x 100 -y 100 -width 600 -height 500 -text "My tool"
$border = 10
$width = $main_form.ClientRectangle.Width - 2 * $border
$save_button = New-Element Button -text "Save configuration" -x $border -width 120 -parent $main_form
$save_button.Top = $main_form.ClientRectangle.Bottom - $border - $save_button.Height
$save_button.Add_Click({
Get-Configuration-Data-from-GUI
Save-Configuration
})
$exit_button = New-Element Button -text Exit -y $save_button.Top -parent $main_form
$exit_button.Left = $main_form.ClientRectangle.Right - $border - $exit_button.Width
$exit_button.DialogResult = [DialogResult]::OK
$gui_data.res = $main_form.ShowDialog()
return $gui_data