chocolatey / chocolatey/choco

Enable use of param() blocks in Chocolatey* Scripts

Open
#3,344 2 comments 2 reactions 0 assignees View on GitHub
0 - Triaging Enhancement Priority - Low
Dominant language
C#
Stars
11.5k
Forks
960
PR merge metrics
No merged PRs in 30d

Description

### Checklist

- [X] I have verified this is the correct repository for opening this issue.
- [X] 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.

```PowerShell
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:

```PowerShell
$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.

```PowerShell
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:

```PowerShell
[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](https://github.com/chocolatey/choco/assets/1975761/5e53f840-5d12-4fec-b840-47751046b63c)

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_

Contributor guide

Open the contributing guide

Research direction

Start with chocolateyScriptRunner.ps1 and the existing Get-PackageParameters helper behavior. Compare the proposed parameter-attribute approach with the alternative of passing parameters from the script runner, including the PowerShell-version and validation constraints described here. Done means a concrete, tested design for supporting param() blocks has been agreed and implemented.

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
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.