Author Posts

April 23, 2018 at 10:32 pm

Below is the code I've started to convert Scanstate.exe to a Powershell function. The problem I'm running in to right now is the interaction between a dynamic parameter and parameter sets.

function Invoke-ScanState {

    [cmdletbinding(DefaultParameterSetName = 'StorePath')]
    param(
        
        [Parameter(ParameterSetName = 'StorePath',
                   Position = 0)]
        [string] $StorePath = 'C:\Backup',
        
        [Parameter(ParameterSetName = 'StorePath')]
        [Parameter(ParameterSetName = 'Hardlink')]
        [ValidateSet('Abort', 'Skip', 'DecryptCopy', 'CopyRaw', 'Hardlink')]
        $Efs,
        
        [Parameter(ParameterSetName = 'StorePath')]
        [Parameter(ParameterSetName = 'Hardlink',
                   Mandatory)]
        [switch] $NoCompress
    )
    dynamicParam {
        if ($PSBoundParameters.ContainsValue('Hardlink')) {
            $params = @{
                'Name' = 'Hardlink'
                'ParameterSetName' = 'Hardlink'
                'Mandatory' = $true
                'Type' = [switch]
            }
            New-DynamicParameter @params
        }
    }
    begin {
        $Hardlink = $PSBoundParameters.Hardlink
    }
}

When I run the 'Efs' parameter with the 'Hardlink' value the 'Hardlink' switch dynamic parameter displays as expected as an option to choose in my function. What doesn't work as expected is the 'Hardlink' switch parameter isn't registering as a mandatory parameter. I can run 'Invoke-ScanState -Efs Hardlink' and it completes without requiring the '-Hardlink' switch. Just to add to the confusion when I add the '-Hardlink' switch the 'NoCompress' switch becomes mandatory, as expected. Hopefully this scenario makes sense. Are my parameter sets messed up somehow? I'm not sure what to do to actually make the '-Hardlink' parameter mandatory when '-Efs Hardlink' is specified.

I've included the 'New-DynamicParameter' function below. It's the version from RamblingCookieMonster that I've modified slightly and added some things.

Function New-DynamicParameter {

    [cmdletbinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]  $Name,

        [string[]] $ValidateSet,
    
        [System.Type] $Type = [string],

        [string[]] $Alias,
    
        [switch] $Mandatory = $false,
    
        [string] $ParameterSetName = "__AllParameterSets",

        [switch] $ValueFromPipeline = $false,
    
        [switch] $ValueFromPipelineByPropertyName = $false,
    
        [string] $HelpMessage,

        [int] $Position = $null,

        [ValidateScript({
            if (-not ($_ -is [System.Management.Automation.RuntimeDefinedParameterDictionary] -or -not $_)) {
                Throw "DPDictionary must be a System.Management.Automation.RuntimeDefinedParameterDictionary object, or not exist"
            }
            $true
        })]
        $DPDictionary = $false
    )

    #Create attribute object, add attributes, add to collection   
    $ParamAttr = New-Object System.Management.Automation.ParameterAttribute

    switch ($PSBoundParameters.Keys) {
        'Mandatory' {$ParamAttr.Mandatory = $true}
        'ValueFromPipeline' {$ParamAttr.ValueFromPipeline = $true}
        'ValueFromPipelineByPropertyName' {$ParamAttr.ValueFromPipelineByPropertyName = $true}
        'ParameterSetName' {$ParamAttr.ParameterSetName = $ParameterSetName}
        'HelpMessage' {$ParamAttr.HelpMessage = $HelpMessage}
        'Position' {$ParamAttr.Position = $Position}
    }

    $AttributeCollection = New-Object Collections.ObjectModel.Collection[System.Attribute]
    $AttributeCollection.Add($ParamAttr)
    
    #parameter validation set if specified
    if ($PSBoundParameters.ContainsKey('ValidateSet')) {
        $ParamOptions = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList $ValidateSet
        $AttributeCollection.Add($ParamOptions)
    }

    #Aliases if specified
    if ($PSBoundParameters.ContainsKey('Alias')) {
        $ParamAlias = New-Object System.Management.Automation.AliasAttribute -ArgumentList $Alias
        $AttributeCollection.Add($ParamAlias)
    }

    #Create the dynamic parameter
    $Parameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter -ArgumentList @($Name, $Type, $AttributeCollection)
    
    #Add the dynamic parameter to an existing dynamic parameter dictionary, or create the dictionary and add it
    if ($PSBoundParameters.ContainsKey('DPDictionary')) {
        $DPDictionary.Add($Name, $Parameter)
    }
    else {
        $Dictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $Dictionary.Add($Name, $Parameter)
        $Dictionary
    }
}

April 24, 2018 at 1:04 am

Why are you using a dynamic parameter here? If -Hardlink is a mandatory switch for this parameter set, why is it not amongst the standard parameters?

April 24, 2018 at 5:38 am

This is more a proof of concept than anything so I'm trying to mirror the original command as much as possible. Looking at scanstate.exe '/hardlink' is required if '/efs:hardlink' is used. Changing '-Hardlink' to a standard parameter, set to mandatory, and added to the hardlink parameter set still has the same behavior as described above.

I guess the better question would be is there a way to make a single value in a ValidateSet a part of parameter set while the other values in the ValidateSet are part of a different parameter set?

April 24, 2018 at 2:41 pm

Probably not directly, no. You could mock it together by just having it check the right value is supplied at the start of the function and just throw a terminating error if it's an invalid syntax. Ideally, though, if the /hardlink parameter implies a specific option from another parameter is supplied, then the other parameter may be better simply inferred, not even part of that parameter set. (i.e., supplying /hardlink assumes the other parameter would be present, and doesn't require you to specify, if that's simply part of its function)