在我的最后一篇关于DSL的帖子中,我打破了其他人描述的DSL。它特别是作为RFC的示例性DSL起草。今天,我将提出替代的DSL语法,我将像我上次做的那样分解实施。

我的真正动力是为了破坏大多数DSL的实施方式。有一个强有力的租约可以将每个关键字视为拍摄字符串和脚本块的高级函数。我想表明我们有其他选择。

这是我的DSL系列中的第五篇文章。

指数

示例DSL.

Here is my draft example of how that DSL could look for creating TypeExtension properties for a class.

# Extend the System.Array type
TypeExtension [System.Array] {

    # Add an alias property
    Count = Property Length

    # Add a new Sum method (from Bruce Payette's "Windows PowerShell in Action", p. 435)
    Sum = Method {
        $acc = $null
        foreach ($e in $this)
        {
            $acc += $e
        }
        $acc
    }
}

# Add a DateTime property to the System.DateTime class
TypeExtension [System.DateTime] {
    DateTime = Property {
        if ((& {Set-StrictMode -Version 1; $this.DisplayHint}) -ieq "Date")
        {
            "{0}" -f $this.ToLongDateString()
        }
        elseif ((& {Set-StrictMode -Version 1; $this.DisplayHint }) -ieq "Time")
        {
            "{0}" -f $this.ToLongTimeString()
        }
        else
        {
            "{0} {1}" -f $this.ToLongDateString(), $this.ToLongTimeString()
        }
    }
}

这与上一篇文章几乎相同的例子。我进行了一个小调整,所以它看起来像是创造属性。这是语法的简单视图。

TypeExtension <Type> {
    <name> = Method <ScriptBlock>
    <name> = Property <PropertName>
    <name> = Property <ScriptBlock>
}

即使实施并不明显,我认为这会觉得自然会很自然。

执行

We will start with the Method and Property keywords. They will be the easiest to implement and look the most like our implementations from the last post.

方法关键字

This will be an advanced function that takes a single parameter. I will place that parameter into a Hashtable and return it.

function Method
{
    <#
        .Description
        Allows you to add a script method to a type
    #>
    [cmdletbinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [ValidateNotNullOrEmpty()]
        [scriptblock]
        $ScriptBlock
    )
    process
    {
        @{
            MemberType = 'ScriptMethod'
            Value = $ScriptBlock
        }
    }
}

I am also adding the MemberType as part of the return value. This will be very important later.

财产关键词

This will be just like the Method keyword except I am going to check the type on the input value to decide the MemberType.

function Property 
{
    <#
        .Description
        Allows you to add an alias or script property to a type
    #>
    [cmdletbinding()]
    param(
        [Parameter(
            Mandatory,
            Position=0
        )]
        [ValidateNotNullOrEmpty()]
        $Value
    )

    process
    {
        $typeData = @{
            Value = $Value
        }

        If($Value -is [ScriptBlock])
        {
            $typeData.MemberType = 'ScriptProperty'
        }
        else
        {
            $typeData.MemberType = 'AliasProperty'
        }

        $typeData
    }
}

typeeExtension关键字

The TypeExtension function will be the most complicated part of this. I have to be a little clever here because I am letting the design drive the implementation. In general it is best to stay away from clever code because it is hard to understand and maintain.

Our keywords are returning hashtables with two properties. The MemberType and the Value. Those are both parameters for Update-TypeData. If you want to see the examples for how to use Update-TypeData, please see my previous post where I showed how to do these things by hand.

If I looked at the ScriptBlock as if it was a Hashtable, then the keys would be the MemberName.

TypeExtension <Type> {
    <MemberName> = Method <ScriptBlock>
    <MemberName> = Property <PropertName>
    <MemberName> = Property <ScriptBlock>
}

So I am going to turn that ScriptBlock into a Hashtable using the method described in my DSL. 设计模式 post.

In that post, I convert the ScriptBlock to a string, add the syntax needed to transform it into a valid looking hashtable, and I execute it to get an actual Hashtable.

Then we walk the keys for the values that I need. Each key is the name of a property and the value has the TypeExtension data for that property.

function TypeExtension 
{
    <#
        .Description
        Allows you to update type information
    #>
    [cmdletbinding()]
    param(
        [Parameter(Mandatory,Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Type,

        [Parameter(Mandatory,Position=1)]
        [ValidateNotNullOrEmpty()]
        [scriptblock]
        $TypeData
    )
    process
    {
        try
        {
            $newScript = "[ordered]@{$($TypeData.ToString())}"
            $newScriptBlock = [scriptblock]::Create($newScript)
            [hashtable]$PropertyList = & $newScriptBlock
            
            If( $type -match '^\[(?<Type>.*)\]$' )
            {
                $type = $matches.Type
            }

            foreach( $property in $PropertyList.GetEnumerator() )
            {                    
                If( $property.Value -is [hashtable] )
                {
                    $options = $property.Value
                }
                elseIf( $property.Value -is [scriptblock] )
                {
                    $options = @{
                        MemberType = 'ScriptProperty'
                        Value = $property.Value
                    }
                }
                else
                {
                    $options = @{
                        MemberType = 'AliasProperty'
                        Value = $property.Value
                    }
                }
                
                $options.MemberName = $property.key
                $options.TypeName = $type.ToString()

                Update-TypeData @options -Force                    
            }
        }
        catch
        {
            $PSCmdlet.ThrowTerminatingError($PSItem)
        }
    }
}

搭档

I ended up adding a little more validation that allows for more flexibility for the user. That validation makes the Property keyword optional. So my new DSL syntax tree looks like this:

TypeExtension <Type> {
    <MemberName> = Method <ScriptBlock>
    <MemberName> = [Property] <PropertName>
    <MemberName> = [Property] <ScriptBlock>
}

此方法对这些特定选项的最终用户具有很好的感觉。此实现的下方是它具有单一的关注属性。如果这就是我们想要支持的一切,那么这将是完美的。

只是一个例子

请记住,这是一个替代的例子。对于这个具体的例子,我喜欢以前的方法更好。

If you have worked with Update-TypeData before then you know that is modifies a lot more than properties. You can modify what shows when format-list is executed or how group-object uses for grouping. The approach we used in the previous post would be much easier to extend to support these other options.