Skip to content

Conversation

@dkattan
Copy link
Contributor

@dkattan dkattan commented May 30, 2022

PR Summary

Implements #8651

Adds constructor to ValidateSetAttribute that accepts a ScriptBlock.

Adds logic that evaluates the ScriptBlock when ValidValues is accessed

Adds logic to Compiler.cs that detects the presence of a ScriptBlock and uses the new constructor

Adds test to TabCompletion.Tests.ps1 to verify functionality

PR Context

I would like to be able to have dynamic parameter values self-contained in a function definition.

PR Checklist

@ghost ghost assigned iSazonov May 30, 2022
@dkattan dkattan marked this pull request as ready for review May 30, 2022 18:48
@dkattan dkattan requested a review from daxian-dbw as a code owner May 30, 2022 18:48
@dkattan
Copy link
Contributor Author

dkattan commented May 31, 2022

I'm thinking about this, and it appears that we don't get access to other parameters that may be specified which would be the next logical thing you would want to have in the script block.

@iSazonov How would you feel about providing the ScriptBlock parameters similar to that of ArgumentCompleter?

param ( $commandName,
$parameterName,
$wordToComplete,
$commandAst,
$fakeBoundParameters )

@iSazonov
Copy link
Collaborator

iSazonov commented Jun 1, 2022

@dkattan Could you please share demo script?

@dkattan
Copy link
Contributor Author

dkattan commented Jun 1, 2022

@dkattan Could you please share demo script?

Inspired by the sample for ArgumentCompleters:
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_argument_completion?view=powershell-7.2#argumentcompleter-script-block

function Test-DynamicValidateSet {
[CmdletBinding()]
 param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Fruits', 'Vegetables')]
        $Type,

        [Parameter(Mandatory=$true)]
        [ValidateSet({ 
            param ( $commandName,
            $parameterName,
            $commandAst,
            $fakeBoundParameters )

            $possibleValues = @{
                Fruits = @('Apple', 'Orange', 'Banana')
                Vegetables = @('Tomato', 'Squash', 'Corn')
            }

            if ($fakeBoundParameters.ContainsKey('Type')) {
                $possibleValues[$fakeBoundParameters.Type]
            } else {
                $possibleValues.Values | ForEach-Object {$_}
            }
        })]
        $Value
      )
}

Test-DynamicValidateSet -Type Fruits -Value 
Explains the various argument completion options available for function parameters.

@SeeminglyScience
Copy link
Collaborator

@iSazonov How would you feel about providing the ScriptBlock parameters similar to that of ArgumentCompleter?

Validation based on other parameters would rely on the order that parameters are bound in. I believe that order is currently documented as undefined (or maybe undocumented).

@dkattan
Copy link
Contributor Author

dkattan commented Jun 1, 2022

@iSazonov How would you feel about providing the ScriptBlock parameters similar to that of ArgumentCompleter?

Validation based on other parameters would rely on the order that parameters are bound in. I believe that order is currently documented as undefined (or maybe undocumented).

I think that's why they chose to call it "fakeBoundParameters" in the ArgumentCompleter implementation. It does a pseudo-binding that seems to be best effort, which is good enough for most cases.

@SeeminglyScience
Copy link
Collaborator

I think that's why they chose to call it "fakeBoundParameters" in the ArgumentCompleter implementation. It does a pseudo-binding that seems to be best effort, which is good enough for most cases.

Yeah it's called that because binding hasn't happened yet, it's all based on statically inferred values. Best effort makes perfect sense for argument completion, but for actual binding time validation I don't think we want to surface something that inconsistent.

@dkattan
Copy link
Contributor Author

dkattan commented Jun 2, 2022

function Test-DynamicValidateSet {
[CmdletBinding()]
 param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Fruits', 'Vegetables')]
        $Type,

        [Parameter(Mandatory=$true)]
        [ValidateSet({ 
            param ( $commandName,
            $parameterName,
            $commandAst,
            $fakeBoundParameters )

            $possibleValues = @{
                Fruits = @('Apple', 'Orange', 'Banana')
                Vegetables = @('Tomato', 'Squash', 'Corn')
            }

            if ($fakeBoundParameters.ContainsKey('Type')) {
                $possibleValues[$fakeBoundParameters.Type]
            } else {
                $possibleValues.Values | ForEach-Object {$_}
            }
        })]
        $Value
      )
}

Test-DynamicValidateSet -Type Fruits -Value 

This is beginning to make sense. In order for this to work the way I want it to work I'd need to introduce a "DelayedValidation" feature that waits for all the parameters to be bound and then passes in the bound parameters to the ScriptBlock.

Then what happens if multiple parameters are awaiting validation?

This is getting deep fast.

I'm going to revert the parameter change so we can at least get the basic functionality working.

If someone needs access to data in the runspace, they can always declare global variables that would be equally available to both ArgumentCompleters and ValidateSet.

@dkattan dkattan force-pushed the feature/validateset-scriptblock branch from 9d38722 to e903d41 Compare June 2, 2022 16:48
@pull-request-quantifier-deprecated

This PR has 27 quantified lines of changes. In general, a change size of upto 200 lines is ideal for the best PR experience!


Quantification details

Label      : Extra Small
Size       : +26 -1
Percentile : 10.8%

Total files changed: 3

Change summary by file extension:
.cs : +19 -1
.ps1 : +7 -0

Change counts above are quantified counts, based on the PullRequestQuantifier customizations.

Why proper sizing of changes matters

Optimal pull request sizes drive a better predictable PR flow as they strike a
balance between between PR complexity and PR review overhead. PRs within the
optimal size (typical small, or medium sized PRs) mean:

  • Fast and predictable releases to production:
    • Optimal size changes are more likely to be reviewed faster with fewer
      iterations.
    • Similarity in low PR complexity drives similar review times.
  • Review quality is likely higher as complexity is lower:
    • Bugs are more likely to be detected.
    • Code inconsistencies are more likely to be detetcted.
  • Knowledge sharing is improved within the participants:
    • Small portions can be assimilated better.
  • Better engineering practices are exercised:
    • Solving big problems by dividing them in well contained, smaller problems.
    • Exercising separation of concerns within the code changes.

What can I do to optimize my changes

  • Use the PullRequestQuantifier to quantify your PR accurately
    • Create a context profile for your repo using the context generator
    • Exclude files that are not necessary to be reviewed or do not increase the review complexity. Example: Autogenerated code, docs, project IDE setting files, binaries, etc. Check out the Excluded section from your prquantifier.yaml context profile.
    • Understand your typical change complexity, drive towards the desired complexity by adjusting the label mapping in your prquantifier.yaml context profile.
    • Only use the labels that matter to you, see context specification to customize your prquantifier.yaml context profile.
  • Change your engineering behaviors
    • For PRs that fall outside of the desired spectrum, review the details and check if:
      • Your PR could be split in smaller, self-contained PRs instead
      • Your PR only solves one particular issue. (For example, don't refactor and code new features in the same PR).

How to interpret the change counts in git diff output

  • One line was added: +1 -0
  • One line was deleted: +0 -1
  • One line was modified: +1 -1 (git diff doesn't know about modified, it will
    interpret that line like one addition plus one deletion)
  • Change percentiles: Change characteristics (addition, deletion, modification)
    of this PR in relation to all other PRs within the repository.


Was this comment helpful? 👍  :ok_hand:  :thumbsdown: (Email)
Customize PullRequestQuantifier for this repository.

@dkattan
Copy link
Contributor Author

dkattan commented Jun 2, 2022

@SeeminglyScience @iSazonov I've reverted the change. How are you guys feeling about this PR?

@iSazonov
Copy link
Collaborator

iSazonov commented Jun 2, 2022

@SeeminglyScience @iSazonov I've reverted the change. How are you guys feeling about this PR?

It is question for WG. The PR is implementing initial proposal from #3744 but the discussion in #3744 was finished with the conclusion that we need a consistency with binary cmdlets and as result we implemented IValidateSetValuesGenerator in #3784. ( Clearly, this is not very convenient to use in scripts. We could discuss this in #8651)
So we need a review from WG-Engine before we can continue.

@iSazonov iSazonov added the WG-Engine core PowerShell engine, interpreter, and runtime label Jun 2, 2022
@dkattan
Copy link
Contributor Author

dkattan commented Jun 2, 2022

Dumb question, what’s WG?

@dkattan
Copy link
Contributor Author

dkattan commented Jun 2, 2022

Dumb question, what’s WG?

Nevermind, I Googled it

https://github.com/PowerShell/PowerShell/blob/master/docs/community/working-group-definitions.md

GitHub
PowerShell for every system! Contribute to PowerShell/PowerShell development by creating an account on GitHub.

@iSazonov
Copy link
Collaborator

iSazonov commented Jun 3, 2022

@vexx32 Could you add this in WG plan?

@ghost ghost added the Review - Needed The PR is being reviewed label Jun 10, 2022
@ghost
Copy link

ghost commented Jun 10, 2022

This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days.
Maintainer, please provide feedback and/or mark it as Waiting on Author

@dkattan
Copy link
Contributor Author

dkattan commented Jun 16, 2022

@vexx32 Could you add this in WG plan?

Hey @vexx32 Any updates/eta?

@dkattan
Copy link
Contributor Author

dkattan commented Jun 24, 2022

@vexx32 @iSazonov Any updates?

@SeeminglyScience SeeminglyScience self-assigned this Jun 24, 2022
@SeeminglyScience
Copy link
Collaborator

The Engine-WG discussed this yesterday and we've found a few issues with this concept that will not be easily solvable.

  1. Accepting a ScriptBlock here will imply the UX of generation based on supplied value, e.g. $_. I know your original draft did do something like this and it was changed based on feedback, but the implication will still be there.
  2. When this attribute is applied within a module, the ScriptBlock invoked will not be invoked in the correct session state. This is already a problem with ArgumentCompleterAttribute, but we do not want to add additional features with the same problem until we have a solution.

When the latter issue is resolved, the language WG should revisit the former and determine if there is a feasible design.

@SeeminglyScience SeeminglyScience added Resolution-Declined The proposed feature is declined. and removed Review - Needed The PR is being reviewed labels Jun 24, 2022
@iSazonov iSazonov closed this Jun 24, 2022
@dkattan
Copy link
Contributor Author

dkattan commented Jun 25, 2022

@SeeminglyScience I’d be happy to work on the module context issue.

My end game is to create a UX similar to Show-Command but for the web. My project (immy.bot) is very tightly integrated with PowerShell with custom PSHosts that push Write-Progress to progress bars in the interface, for example.

We already have static forms where you can create parameters for scripts, but it gets rather clunky when the need for parameter sets arise.

Rather than reinvent the dynamic forms wheel, my thought was to simply parse the params block of the script and save parameter values as a hashtable that can splatted on to the script in question and PowerShell can handle the binding.

Obviously this can be done without the feature in the PR, however the primary use case for me does require it, I’ll elaborate.

My application installs software on computers based on the user you select. Users are dynamically retrieved from AzureAD. This is currently implemented in C#, but to offer maximum flexibility I’d like to move this logic into PowerShell so users can define their own behavior.

Now I suppose I could implement dynamic parameters, assuming that works with Show-Command/Get-Command the syntax is just very cumbersome.

I’d rather keep everything in a scriptblock and simply call Get-AzureADUser.

ANYWAY, if you wouldn’t mind, can you elaborate on the issue with module context? Is it related to how modules imported using InitialSessionState.ImportPSModuleFromPath are not bound by constrained language mode?

@iSazonov
Copy link
Collaborator

I think this requires the implementation of stateless script block. Theoretically it is possible, but in fact it may be difficult to implement, and most importantly lead to negative consequences for PowerShell language as a whole.

@dkattan
Copy link
Contributor Author

dkattan commented Jun 25, 2022

I think this requires the implementation of stateless script block. Theoretically it is possible, but in fact it may be difficult to implement, and most importantly lead to negative consequences for PowerShell language as a whole.

Can you give me an example on what sort of negative consequences this would have on the language as a whole?

@iSazonov
Copy link
Collaborator

Today any scriptblock exists and run only in one runspace. This is how PowerShell works.
If this principle is abandoned completely, it will be a completely different language. If partially, it could lead to chaos.

@dkattan
Copy link
Contributor Author

dkattan commented Jun 26, 2022

It sounds like the concern is that the implementation of a stateless script block (which I’m interpreting as being a purely functional/isolated script block similar to a static method in C#) would violate the one runspace paradigm of PowerShell. I could argue that by bringing up implicit remoting for Windows PowerShell compatibility or PS Remoting in general, but I’d rather try to understand why you think a stateless scriptblock is necessary in the first place?

If not for me, for potential future readers who want to fully understand.

And also, help me understand the existing problems with Modules

I really am trying to understand this at a deeper level, not trying to be difficult. I appreciate the time you’ve already put into this.

@iSazonov
Copy link
Collaborator

iSazonov commented Jun 26, 2022

In remoting PowerShell doesn't send scriptblock, it send text. Also there is 1-to-1 mapping local to remote runspace.
I think the main problem is how users perceive it. On the one hand they would like to make the script code independent. On the other hand it is always required to run it in some context. PowerShell historically has a solution. For example, a module has its own context. This is what the module concept exists for. It's simple and it's understandable to users. If we allow an exception, there must be an idea behind it that everyone understands. What I mean by that is that the design of the language should change.

I don't know what else to add. :-)

@SeeminglyScience
Copy link
Collaborator

SeeminglyScience commented Jun 26, 2022

I think this requires the implementation of stateless script block.

Sorry I think there was a misunderstanding. It just needs the right state, it doesn't need to (and shouldn't) be stateless.

@dkattan a quick way to demo what I'm talking about:

New-Module {
    $ModuleScopedVar = 'one', 'two'

    function Test-Command {
        [CmdletBinding()]
        param(
            [ArgumentCompleter({ $script:ModuleScopedVar })]
            [string] $Name
        )
    }
} | Import-Module

There are workarounds but this is already a huge UX issue that comes up often on the discord. Unfortunately it wasn't caught (or maybe the impact wasn't realized) for ArgumentCompleter, but until it's solved we should avoid adding more things with the same behavior.

Edit:

ANYWAY, if you wouldn’t mind, can you elaborate on the issue with module context? Is it related to how modules imported using InitialSessionState.ImportPSModuleFromPath are not bound by constrained language mode?

^ that's also a good point, language mode is also unlikely to be propagated.

@dkattan
Copy link
Contributor Author

dkattan commented Jun 26, 2022

@SeeminglyScience that example helped tremendously.

To better illustrate the problem, I created a variable with the same name with value three, four and it tab completes three,four instead of one,two

It seems like the problem is that CompleteInput not taking into consideration that Test-Command is defined in a module and therefore shouldn’t have access to variables in the script scope.

If I had to create an issue, it would be “Results of CompleteInput in modules potentially affected by variables defined in parent runspace”

I understand now that if we allow ValidateSet to be dynamic and not a constant it would have the same problem.

Also, do DynamicParameters have the same problem? I’ll test shortly.

I have no issue using DynamicParameters for my use case, but I’m afraid I won’t be able to instantiate the required types in Constrained Language mode. (The scripts in my application all run in Constrained Language mode)

I suppose I could inject custom Cmdlets that instantiate these objects from C# as a workaround.

@iSazonov
Copy link
Collaborator

Sorry I think there was a misunderstanding. It just needs the right state, it doesn't need to (and shouldn't) be stateless.

Yes, you mean rather "scope," I mean "sessionstate".
I suspect the problem with variable propagation like $ErrorActionPreference is of the same nature. This makes me think that the flexible solution could be rather related to "sessionstate" (which would certainly in turn be a means of manipulating scopes).

@SeeminglyScience
Copy link
Collaborator

It seems like the problem is that CompleteInput not taking into consideration that Test-Command is defined in a module and therefore shouldn’t have access to variables in the script scope.

It's more that the ScriptBlock is created in a way that ScriptBlock.SessionStateInternal is not populated. When that is null, it implicitly takes on the state in ExecutionContext.EngineSessionState (whatever was last running at the time).

Curiously, ValidateScript does not have this problem. I'm unsure if it's explicitly special cased, or if it happens to work based on when the script is invoked. Either way worth looking into.

Also, do DynamicParameters have the same problem? I’ll test shortly.

They do not! Neither does ValidateScript or Register-ArgumentCompleter (the pair of them is what I'd recommend you use to implement this feature in the mean time).

@SeeminglyScience
Copy link
Collaborator

Yes, you mean rather "scope," I mean "sessionstate". I suspect the problem with variable propagation like $ErrorActionPreference is of the same nature. This makes me think that the flexible solution could be rather related to "sessionstate" (which would certainly in turn be a means of manipulating scopes).

SessionState is what I'm referring to when I say state, sorry if that was unclear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Extra Small Resolution-Declined The proposed feature is declined. WG-Engine core PowerShell engine, interpreter, and runtime

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants