Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable use of param() blocks in Chocolatey* Scripts #3344

Open
2 tasks done
JPRuskin opened this issue Oct 13, 2023 · 0 comments
Open
2 tasks done

Enable use of param() blocks in Chocolatey* Scripts #3344

JPRuskin opened this issue Oct 13, 2023 · 0 comments

Comments

@JPRuskin
Copy link
Member

JPRuskin commented Oct 13, 2023

Checklist

  • I have verified this is the correct repository for opening this issue.
  • I have verified no other issues exist related to my request.

Is Your Feature Request Related To A Problem? Please describe.

As a PowerShell developer, it would be great if we could support the commonly-used param-block usage seen in functions and scripts.

param(
    # The path to install to
    [string]$InstallDir = "C:\Python$($env:ChocolateyPackageVersion -replace "^(\d+\.\d+).*", "`$1")"
)
$ErrorActionPreference = 'Stop'
$toolsDir   = Split-Path $MyInvocation.MyCommand.Definition -Parent

<# ...Do install things... #>

When writing a PowerShell script, I expect this to work - but there is no nice way to pass arguments into Chocolatey to run scripts like this.

Instead, we have folk using something like this:

$pp = Get-PackageParameters
$twoPartVersion = $Env:ChocolateyPackageVersion -replace "^(\d+\.\d+).*", "`$1"
$defaultFolder = '{0}\Python{1}' -f $Env:SystemDrive, ($twoPartVersion -replace '\.')
if ( $pp.InstallDir ) {
  $installDir = $pp.InstallDir
  if ($installDir.StartsWith("'") -or $installDir.StartsWith('"')) { $installDir = $installDir -replace '^.|.$' }
  mkdir -force $installDir -ea 0 | out-null
}
else {
  $installDir = $defaultFolder
}

And there are many different ways people can write this handling logic.

Describe The Solution. Why is it needed?

Get-PackageParameters exists, but everyone has to reimplement the handling for parameters in their package, which is less simple than it might be. If we had a first-class handling for param-blocks (or some similar modern method), we could advise folk to handle parameters in a specific way, which would allow us to do potentially interesting things.

For instance, by enabling package maintainers to use param blocks:

  • They could (possibly) use standard PowerShell parameter validation and other fun attributes
  • They could add comment help to the parameters
    • This could then be analyzed and exposed in various ways (e.g. automatically displaying supported parameters and help for them on CCR, or within the CLI).
  • It leads into a known method and place to handle variables that can be changed, which will help community moderators and users reading install-scripts understand them more easily
  • It reduces the amount of package maintainers having to rewrite the (parameter handling) wheel every package

It could also ease testing of install scripts, though I am not convinced that is particularly useful at this point for various reasons.

Additional Context

I had a play with adding a very basic transformation attribute to parameters within a chocolateyInstall.ps1 script.

class PackageParameterAttribute : System.Management.Automation.ArgumentTransformationAttribute {
    [string]$TargetParameter

    static [string] GetPackageParameterValue ($Name) {
        $PP = Get-PackageParameters
        if ($PP.ContainsKey($Name)) {
            return $PP[$Name]
        } else {
            return $null
        }
    }

    [object] Transform([System.Management.Automation.EngineIntrinsics]$engineIntrinsics, [object]$inputData) {
        if ([PackageParameterAttribute]::GetPackageParameterValue($this.TargetParameter)) {
            return [PackageParameterAttribute]::GetPackageParameterValue($this.TargetParameter)
        } else {
            return $inputData
        }
    }

    # PackageParameterAttribute() {
    #     # Can't figure out a way to get the name of the parameter, so we currently have to specify one
    #     $this.TargetParameter = "Test"
    # }

    PackageParameterAttribute([string]$Target) {
        $this.TargetParameter = $Target
    }
}

This seems to work pretty nicely when added to the chocolateyInstaller helper functions, and handles using the default value in a script if a user doesn't pass a matching --package-parameter (whilst overriding it if they do).

As an example, by adding [PackageParameter("NameOfPackageParameter")] to a parameter, we can see the default, the help, and easily have a user provide input:

[CmdletBinding()]
param(
    # The path to extract the files to
    [PackageParameter("InstallPath")]
    $InstallPath = $(Split-Path $MyInvocation.MyCommand.Definition -Parent),

    # A message to output (completely arbitrary example)
    [PackageParameter("Message")]
    [ValidateNotNullOrEmpty()]
    $MessageOutput = "There was no additional message provided."
)

Write-Host "Installing package to '$($InstallPath)'"

Write-Host $MessageOutput

image

It has a few potential disadvantages:

  • Classes like this are only supported in PowerShell 5+ (but we could use Add-Type to bring it back to PowerShell 3+, or possibly use a compiled module to make it available everywhere?)
  • Adding parameter attributes like this will cause package scripts to fail if the attribute isn't available, which is obviously rubbish. Adding the attribute does seem to work both from helpers and from extensions, though, so we could have it available in Choco-latest and a compatibility package.
  • ArgumentTransformation is only triggered on parameters that have a value, it seems, so you need to provide a default value to any parameters that use it. It's possible we could implement this as a different attribute type and still modify the value?
  • It could be inefficient if someone had an obscene number of parameters (though I think the number would have to be silly, and this could be easily solved - again, this is just some POC fun to inspire discussion, here)
  • Current lack of handling for, e.g. switch-type parameters (again, POC - this could be handled, just needs thought)
  • I am unsure this method for implementation would allow use of parameter validation attributes, due to the way it's being passed. May be wrong / may be fixable.

Example of Add-Type equivalent:

Add-Type @'
using System.Management.Automation;

public sealed class PackageParameterAttribute : ArgumentTransformationAttribute {
    string _targetParameter;

    string _getPackageParameterValueScript {
        get {
            return string.Format(
                @"
                    $PP = Get-PackageParameters
                    if ($PP.ContainsKey('{0}')) {{
                        return $PP['{0}']
                    }} else {{
                        return $null
                    }}
                ",
                _targetParameter
            );
        }
    }

    // // This currently doesn't work, as we can't retrieve the ParameterName easily
    // public PackageParameterAttribute() {
    //   _targetParameter = '???'
    // }

    public PackageParameterAttribute(string packageParameterName) {
        _targetParameter = packageParameterName;
    }

    public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) {
        var result = engineIntrinsics.InvokeCommand.InvokeScript(_getPackageParameterValueScript)[0];
        if (null != result) {
            return result;
        }
        return inputData;
    }
}
'@

An alternative to adding an attribute like this would be rewriting the chocolateyScriptRunner.ps1 to pass in parameters where parameters are found, but that would involve a fair bit of calculation (or a requirement for an ignored parameter with ValueFromRemainingArguments to swallow unwanted splatting on all supporting scripts, perhaps?) and I've quite enjoyed this method so far.

Related Issues

No response

@JPRuskin JPRuskin changed the title Allow use of param() blocks in Chocolatey* Scripts Enable use of param() blocks in Chocolatey* Scripts Oct 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants