提问人:tamir 提问时间:7/24/2023 最后编辑:mklement0tamir 更新时间:7/28/2023 访问量:77
如何在powershell中创建调用脚本函数的动态块?
How to create dynamic block in powershell, that calls script functions?
问:
我正在尝试生成一个动态 UI。我无法动态添加 OnClick 事件。下面是一个示例
function Say-Hello
{
Param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[String]$name
)
Write-Host "Hello " + $name
}
$name = "World"
$null = [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$mainform = New-Object System.Windows.Forms.Form
$b1 = New-Object System.Windows.Forms.Button
$b1.Location = New-Object System.Drawing.Point(20, 20)
$b1.Size = New-Object System.Drawing.Size(80,30)
$b1.Text = "Start"
#$b1.Add_Click({Say-Hello $name})
$b1.Add_Click({Say-Hello $name}.GetNewClosure())
$mainform.Controls.Add($b1)
$name = "XXXX"
$mainform.ShowDialog() | Out-Null
首先我尝试过,但会产生.然后我按原样尝试了上面的代码,但出现错误,找不到 Say-Hello($b1.Add_Click({Say-Start $name})
Hello XXXX
$b1.Add_Click({Say-Hello $name}.GetNewClosure())
Say-Hello : The term 'Say-Hello' is not recognized as the name of a cmdlet, function, script file...
)
我之所以要覆盖这个名称,是因为我实际上想将按钮创建转换为一个函数,我将调用多个 ties,每次都使用不同的参数。$name
有什么建议如何处理吗?
谢谢
答:
听起来您想使用脚本块在变量的状态上创建一个闭包,这意味着在使用 .GetNewClosure(),
而不受调用方作用域中变量值的后续更改的影响。$name
$name
$name
问题在于,PowerShell 使用动态模块来实现闭包,并且 - 与所有模块一样 - 动态模块与外部调用方共享的唯一祖先范围是全局范围。
换句话说:返回的动态模块不知道您的函数,因为它是在全局作用域的子作用域中创建的,默认情况下,脚本和函数是运行的地方。.GetNewClosure()
Say-Hello
顺便说一句:如果你要从全局作用域点源你的脚本,问题就会消失,但这是不可取的,因为这样你就会用所有的变量、函数、......脚本中的定义。
有选择地将您的功能定义为“污染较少”的替代方案,但仍然不是最佳选择。
function global:Say-Hello { ... }
解决方案:
在将为其创建闭包的脚本块的上下文中重新定义函数。
下面是一个简化的独立示例:
& { # Execute the following code in a *child* scope.
$name = 'before' # The value to lock in.
function Say-Hello { "Hello $name" } # Your function.
# Create a script block from a *string* inside of which you can redefine
# function Say-Hello in the context of the dynamic module.
$scriptBlockWithClosure =
[scriptblock]::Create("
`${function:Say-Hello} = { ${function:Say-Hello} }
Say-Hello `$name
").GetNewClosure()
$name = 'after'
# Call the script block, which still has 'before' as the value of $name
& $scriptBlockWithClosure # -> 'Hello before'
}
${function:Say-Hello}
是命名空间变量表示法的实例 - 有关一般背景信息,请参阅此答案。在获取诸如 的表达式时,将返回目标函数的正文作为实例。
${function:Say-Hello}
[scriptblock]
在赋值时,定义目标函数;赋值可以是脚本块,也可以是包含函数源代码的字符串(不将其包含在
${function:Say-Hello}
{ ... }
)在上面的代码中,一个可扩展的(双引号)字符串 (
“...”
),即字符串插值用于将返回的脚本块的字符串化源代码嵌入到传递给[scriptblock]::Create()
的字符串中${function:Say-Hello}
通过将引用包含在 中,字符串化脚本块(在没有外壳的情况下进行字符串化)成为构造脚本块的源代码中的脚本块文本。
${function:Say-Hello}
{ ... }
{ ... }
评论
Say-Hello
global:Call-LocalFunction
根据 @mklement0 的回答和评论,我写了一个小样本来演示问题和解决方案。下面的代码显示了接下来的选项
按钮“A” - 一个简单的块,使用带有“常量”字符串的内置(全局)cmdlet
按钮“B” - 从参数动态生成块。这显示了问题 - “B”保留在传递给的变量中,当按下按钮时它不存在。打印的消息为空。
AddButtonAutoBlock
按钮“C” - 从块生成闭合,如“B”所示。但是,“C”被复制了,但该函数在全局范围内是未知的,因此它得到了一个错误
Show-Message
按钮“D” - 用全局函数污染全局范围。这克服了“C”中的问题并有效
按钮“E” - 为了避免使用此脚本的函数填充全局范围,使用单个回调。回调在内部将调用调度到正确的本地函数
按钮“F” - 使用全局回调,调用本地函数。这一次的调用更加通用。调用被定向回实际持有按钮的同一对象。
评论
- “E” 有一个 if-else 结构,需要为每个新回调扩展,但 “F” 对所有回调使用相同的代码。但是,“E”在参数类型方面更通用。 正在调用假设是一个字符串
Call-LocalObjectCallbackString
"$callback('$arg0')"
$arg0
- 有没有办法将两者结合起来 - 使用通用参数列表进行通用回调?我尝试传递一个列表,但问题是将数据转换为(看起来像)原始字符串。也许一些打包和解包操作可以在这里有所帮助。
GetNewClosure
- 这里的单身节目是微不足道的,但这里有一个很好的、更正式的节目
function Show-Message
{
Param([String]$message)
Write-Host "Message: $message"
}
function Show-Message-Beautify
{
Param([String]$message)
Write-Host "Message: <<<$message>>>"
}
function global:Show-Message-Global
{
Param([String]$message)
Show-Message $message
}
function global:Show-Message-Global-Callback
{
Param($callback, $arg0)
if ($callback -eq "Show-Message")
{
Show-Message $arg0
}
elseif ($callback -eq "Show-Message-Beautify")
{
Show-Message-Beautify $arg0
}
else
{
# throw exception
}
}
function global:Call-LocalObjectCallbackString
{
Param($callback, $arg0)
Invoke-Expression -Command "$callback('$arg0')"
}
class MainForm
{
static [MainForm]$mainSingletone = $null
static [MainForm] Instance()
{
return [MainForm]::mainSingletone
}
static ShowMainForm()
{
$null = [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$main = [MainForm]::new()
$main.AddButtonBlock("A", {Write-Host "A"})
$main.AddButtonAutoBlock("B")
$main.AddButtonAutoBlockClosure("C")
$main.AddButtonAutoBlockGlobalClosure("D")
$main.AddButtonAutoBlockGlobalClosureCallback("E")
$main.AddButtonAutoBlockGlobalClosureCallbackObject("F")
$main.form.ShowDialog() | Out-Null
}
# non statics
$form
[int] $nextButtonOffsetY
MainForm()
{
$this.form = New-Object System.Windows.Forms.Form
$this.form.Text = "test"
$this.form.Size = New-Object System.Drawing.Size(200,400)
$this.nextButtonOffsetY = 20
[MainForm]::mainSingletone = $this
}
[object] AddButton($name)
{
$b = New-Object System.Windows.Forms.Button
$b.Location = New-Object System.Drawing.Point(20, $this.nextButtonOffsetY)
$b.Size = New-Object System.Drawing.Size(160,30)
$b.Text = $name
$this.nextButtonOffsetY += 40
$this.form.Controls.Add($b)
return $b
}
AddButtonBlock($name, $block)
{
$b = $this.AddButton($name)
$b.Add_Click($block)
}
AddButtonAutoBlock($name)
{
$b = $this.AddButton($name)
$b.Add_Click({Show-Message $name})
}
AddButtonAutoBlockClosure($name)
{
$b = $this.AddButton($name)
$b.Add_Click({Show-Message $name}.GetNewClosure())
}
AddButtonAutoBlockGlobalClosure($name)
{
$b = $this.AddButton($name)
$b.Add_Click({Show-Message-Global $name}.GetNewClosure())
}
AddButtonAutoBlockGlobalClosureCallback($name)
{
$b = $this.AddButton($name)
$b.Add_Click({Show-Message-Global-Callback "Show-Message" $name}.GetNewClosure())
$b = $this.AddButton("Beautify-$name")
$b.Add_Click({Show-Message-Global-Callback "Show-Message-Beautify" $name}.GetNewClosure())
}
Callback ($message)
{
Write-Host "Callback: $message"
}
AddButtonAutoBlockGlobalClosureCallbackObject($name)
{
$b = $this.AddButton($name)
$b.Add_Click({Call-LocalObjectCallbackString "[MainForm]::Instance().Callback" $name}.GetNewClosure())
}
}
[MainForm]::ShowMainForm()
评论
Hello World
Hello XXXX
Hello + World
Write-Host ("Hello " + $name)
Write-Host "Hello $name"
Hello XXXX
Hello World
Write-Host
Write-Host 'hi ' + 'there'
hi + there
(...)
+