当我在我的DSL上工作时,我发现我必须更具创造力,而我的高级功能实现比我的正常高级功能。撰写脚本cmdlet时,已有很多社区标准和已定义的预期行为。

创建DSL时,您可能会弯曲很多最佳实践以创建最佳用户体验。在优化用户时,您可能会发现自己使用不同技术收集和处理数据。

这篇文章的目标是向您展示不同的方法,您可以在制定DSL方面进行。

这是一系列关于在PowerShell中写入DSL的第三篇文章。

指数

设计模式和DSL命令类型

此帖子的其余部分充满了设计模式和不同类型的命令,即在制备DSL时发现有用。其中一些非常明显,但我想在这种情况下提及它们。在您处理他们可以解决的特定问题之前,还有其他模式您可能无法找到有用。

价值普斯斯湖

您可能希望拥有特定名称的命令以适合您的DSL,但它将它传递给它的值。这些DSL通常是用于更复杂的命令的速记,这就是您在这里所做的一切。

function Set-State 
{
    param($State)
    return $State
}

This could be done by creating an alias on Write-Output.

简单的模板

The idea behind a template is that you accept basic parameters, pass them into a template and return the resulting text. Our RdcServer command from the last post is a good example of that.

function Get-RdcServer
{
    param($ComputerName)
    @"
    <server>
      <properties>
        <name>$ComputerName</name>
      </properties>
    </server>
"@
}

RdcServer Server01

我主要想到返回字符串在使用此方法时构建文档。您还可以构建更复杂的对象。这可能是一个完整的XML对象或YPU正在使用的任何其他类型的对象。

嵌套模板

此模式通常涉及使用内容的页眉和页脚的模板数据。然后允许用户指定要为内容的主体调用的脚本块。我们上周的RDCGroup命令是这个的一个很好的例子。

function Get-RdcGroup
{
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $true,
            Position = 0
        )]
        [string]
        $GroupName,

        [Parameter(
            Mandatory = $true,
            Position = 1
        )]
        [scriptblock]
        $ChildItem
    )
    process
    {
        @"
    <group>
      <properties>
        <name>$GroupName</name>
      </properties>
"@
       $ChildItem.Invoke()

        '    </group>'
    }
}

此命令可以自身嵌套并与此简单模板模式加入。

RdcGroup GroupATX {
    RdcServer Server1
    RDCServer Server2
}

RdcGroup GroupATX {
    RdcGroup GroupDMZ {
        RdcServer ServerDMZ01
        RdcServer ServerDMZ02
    }
    RdcGroup GroupInternal {
    }
}

This is an example where you execute the scriptblock inline as the command is running. I mention that because there are situations where you would not execute that [scriptblock]

Hashtable Passthru.

You can use a DSL to easily build a [hashtable] based on the command parameters. This can be a great way to build a validated [hashtable] that must have a specific structure.

function Get-State 
{
    [cmdletbinding()]
    param(
        $State,
    
        [scriptblock]
        $StateScript
    )
    return $PSBoundParameters
}

使用Passthru类型的命令的想法是其他东西将收集此数据并处理它。

Hashtable Builder.

A true [hashtable] builder will allow the user to specify key value pairs and properly convert them to a [hashtable] or [pscustomobject]. It could be implemented like this:

function Get-ServerDetails
{
    param([scriptblock]$ScriptBlock)

    $newScript = "[ordered]@{$($ScriptBlock.ToString())}"
    $newScriptBlock = [scriptblock]::Create($newScript)
    & $newScriptBlock
}

然后,您可以拥有一个如下所示的DSL命令:

ServerDetails {
    Name = 'test'
    IP = '10.0.0.1'
}

The scriptblock is still a key component in the way the command is used. The key value pairs are provided in the scriptblock and then we reformat the input to become a valid hashtable before executing it.

这是指出您可以在运行之前修改脚本内容的好时机。

Hashtable收藏家

If you have a hashtable passthru or hashtable builder, then you may need to have a hashtable collector. This pattern uses a script block to hold the hashtable passthru values. The [scriptblock] is invoked and the return values are captured. We can use this to flesh out a statemachine.

function Get-StateMachine
{
    [cmdletbinding()]
    param(
        [scriptblock]
        $StateScript
    )

    $userScripts = & $StateScript   
    [hashtable]$stateEngine = @{}
    $userScripts | ForEach-Object {$stateEngine[$_.State] = $_}

    return $stateEngine        
}

This example captures all the hashtables that are created when the $StateScript is invoked. It performs some processing and then returns a statemachine based on this structure:

StateMachine {

    State Start {
        Write-Verbose "Start"
        Set-State "Monitor"
    }  

    State Monitor {
        Write-Verbose "Monitor"
        Set-State "End"
    }  
}

限制DSL.

When using a [scriptblock], you leave your DSL open to allow any PowerShell commands to be ran. You can restrict this to the DSL commands you specify with the data command.

    $newScript = "DATA -SupportedCommand Get-State,Set-State {$ScriptBlock}"
    $newScriptBlock = [scriptblock]::Create($newScript)
    & $newScriptBlock

当您定义要在PowerShell执行的文件中使用的DSL时,这是最有价值的。这允许您更像纯文本配置的基于DSL的文件,而不是执行代码的脚本。

内部或私人命令

您可能有一个命令,您不想在DSL实现之外无法导出或可用。您可以通过在容器功能中定义函数来执行此操作。执行时命令将在脚本块内有效。

function Get-StateMachine
{
    [cmdletbinding()]
    param(
        [scriptblock]
        $StateScript
    )
    
    function Set-State 
    {
        param($State)
        return $State
    }
    
    $userScripts = & $StateScript 
    [hashtable]$stateEngine = @{}
    $userScripts | ForEach-Object {$stateEngine[$_.State] = $_}

    return $stateEngine        
}

In the example above, Set-State is defined inside Get-StateMachine.

专注于用户体验

无论您使用哪种模式,请确保您关注用户将如何与DSL交互。当我编写一个DSL时,我经常模拟几种方法来描述使用假命令的东西,直到它感觉正确。

然后我去寻找一个设计模式,允许我创建将符合最佳示例的命令。有时,您正在执行所有内容以及其他时间在父函数中收集所有内容以进行处理。