Enable use of param() blocks in Chocolatey* Scripts
- 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
```

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
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