PowerShell / PowerShell/PowerShell

Shared immutable storage for function and attributes

Open
#26,598 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Issue-Enhancement Needs-Triage WG-Engine
Dominant language
C#
Stars
55.5k
Forks
8.5k
Avg merge
1d 2h
Merged PRs (30d)
88

Description

EDIT: use real example, and propose such variables to be evaluated on completion-time(or whatever the name it should have)

Summary of the new feature / enhancement
Motivation

Sometimes I'd like to use same source for both validation attributes and function implementation.
However pwsh doesn't allow to share them as a variable. PowerShell has a great capability to manage completions within same script, I think this feature would make it even better.

The following example replicates $registry variable for real execution and completion, depending on the context of -AllUsers.

# Remove-StartupApplication.ps1
param(
    [ArgumentCompleter({
            param (
                $commandName,
                $parameterName,
                $wordToComplete,
                $commandAst,
                $fakeBoundParameters
            )
            if ($fakeBoundParameters.ContainsKey('AllUsers') -and $fakeBoundParameters.AllUsers) {
                $registry = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
            } else {
                $registry = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
            }
            Get-Item $registry | ForEach-Object Property
        })]
    [Parameter(Mandatory)]
    [string]$Name,

    [switch]$AllUsers
)

begin {
    if ($AllUsers) {
        $registry = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
    } else {
        $registry = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
    }
    $ExePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ExePath)
}

end {
    Remove-ItemProperty -Path $registry -Name $Name
}

I propose to add a readonly block to initialize shared variables on a phase can be triggered by tab completion and before execution.

# Remove-StartupApplication.ps1
param(
    [ArgumentCompleter({
            param (
                $commandName,
                $parameterName,
                $wordToComplete,
                $commandAst,
                $fakeBoundParameters
            )
            # now we share a same variable
            # maybe we need a dedicated scope to distinct such variables
            Get-Item $readonly:registry | ForEach-Object Property
        })]
    [Parameter(Mandatory)]
    [string]$Name,

    [switch]$AllUsers
)

readonly {
    # this block should get evaluated on tab completion
    # we may expose a event for PSReadline to trigger
    if ($AllUsers) {
        $registry = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
    } else {
        $registry = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
    }
}

begin {
    $ExePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ExePath)
}

end {
    Remove-ItemProperty -Path $readonly:registry -Name $Name
}
Solution Proposal

The solution I can think of is to add a new named block readonly, variables declared in readonly block are immutable.
Such variables should be initialized on a phase can be triggered by tab completion and before execution, and should be accessible for attributes/scriptblock in attributes.
To avoid conflicts with external variables we should use explicit $readonly: scope to access variables declared in readonly block.

function foo {
    param(
        [ValidateSet($readonly:candidates)] # attributes are aware of readonly variables
        [ValidateScript({ $_ -in $readonly:candidates })] # or access in scriptblock
        [ArgumentCompleter({ & $readonly:completer })]
        $foo
    )

    readonly {
        $candidates = 'foo', 'bar'
        $completer = {
            # ...complex logic for parsing completion for native executables
            # that you don't want to repeat in every ArgumentCompleter
        }
    }

    begin {
        if (-not $foo) {
            $foo = $readonly:candidates | fzf --prompt 'pick one'
        }
    }

    end {
        $readonly:candidates = $null # ERROR: can't mutate readonly variables
        $readonly:candidates[0] = 'baz' # ERROR: can't mutate readonly variables
    }
}
Potential Problem
  1. if access the readonly variable within scriptblock for attributes like ValidateScript, the error messages would be confusing for users:
The " $_ -in $candidates " validation script for the argument with value "baz" did not return a result of True. Determine why the validation script failed, and then try the command again.
  1. whether to prioritize readonly variables in attribute scriptblock to avoid ambiguity(should we add a $readonly: scope?)
$foo = 1

function foo {
   param (
     [ArgumentCompleter({
        # readonly variables should be explicit
        # to avoid breaking change
        $foo,$readonly:foo 
     })]
     $foo
   )

   readonly {
       $foo = 'foo'
   }
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No files, tests, or entry points are named. Start by reviewing the proposed readonly block and its interactions with tab completion, parameter attributes, scriptblocks, and execution phases. Done would require an agreed design and implementation for immutable shared variables, including scope access and mutation behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
powershell
Domain
cli
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.