How To Add Completers, Transforms And Dynamic Parameters
Tab completion computed in Rust, arguments changed before the binder coerces them, and parameters that exist only when other parameters have certain values. Source: crates/pwrs/src/completer.rs, crates/pwrs/src/transform.rs, crates/pwrs-macros/src/completers.rs, crates/pwrs/src/runtime.rs (completer_invoke, transform_invoke, dynparams_invoke), crates/cargo-pwrs/dotnet/Pwrs.Runtime/CompleterBase.cs, TransformBase.cs and DynamicParametersBase.cs.
Argument completers
use pwrs::prelude::*;
const REGIONS: &[&str] = &["east", "west", "north"];
/// Completes -Region for Get-Site.
#[completer(cmdlet = "Get-Site", parameter = "Region")]
fn complete_region(ctx: &CompletionContext) -> PsResult<Vec<Completion>> {
let prefix = ctx.word.to_lowercase();
Ok(REGIONS
.iter()
.filter(|r| r.starts_with(&prefix))
.map(|r| Completion::value(*r).with_tooltip(format!("the {r} region")))
.collect())
}
pwrs::export_module! {
name: "Sites",
cmdlets: [GetSite],
completers: [complete_region],
}#[completer]turns the function into a unit struct of the same name implementingCompleterFn; that name is whatexport_module!lists.CompletionContextcarriesword(the partial word under the cursor),command(the full command text), andbound, aPsHashtableof the parameters already bound on the line as untyped objects.Completion::value(text)makes aParameterValuecompletion whose list item and tooltip are the text;with_tooltipchanges the tooltip; thekindfield takes anyCompletionKind(Text,Command,ProviderItem,ParameterName, and the rest ofCompletionResultType).- The generated shell attaches
[ArgumentCompleter(typeof(<generated class>))]to the parameter. The completer runs on the completion thread with no pipeline, so it receives noPipelinetoken and cannot write streams.
An Err or a panic inside a completer crosses back as a failed status and the managed side raises it as a PwrsException out of CompleteArgument, so the completion produces nothing rather than a partial list.
Argument transformations
#[transform(cmdlet = "Get-Size", parameter = "Size")]
fn as_bytes(value: &PsObject) -> PsResult<PsObject> {
let text = String::from_ps(value)?;
let trimmed = text.trim();
let (digits, scale) = match trimmed.to_ascii_uppercase() {
t if t.ends_with("KB") => (&trimmed[..trimmed.len() - 2], 1024_i64),
t if t.ends_with("MB") => (&trimmed[..trimmed.len() - 2], 1024 * 1024),
_no_suffix => return value.clone().into_ps(),
};
match digits.trim().parse::<i64>() {
Ok(n) => (n * scale).into_ps(),
Err(_not_a_number) => Err(PsError::new(ErrorCategory::InvalidArgument, "Size", format!("{trimmed} is not a size"))),
}
}pwrs::export_module! {
name: "Demo",
cmdlets: [GetSize],
transforms: [as_bytes],
}- The generated shell attaches an
ArgumentTransformationAttributeto the parameter. The engine runs it before coercing the argument to the parameter’s declared type and before validation, which is the only place this can happen:-Sizestays alongand still accepts2MB. - An
Errbecomes anArgumentTransformationMetadataException, which the engine reports as a binding failure naming the parameter. The cmdlet body never runs. Converting in the body instead would produce an error record after the call had already started. - Return the value unchanged for anything you do not recognize, as the
_no_suffixarm does; the binder then coerces or rejects it as it would have without the transform. - No cmdlet instance exists yet, so a transform takes no
Pipelineand cannot write to a stream. One transform per parameter. - Reach for it when the shape a caller wants to write is not a shape the parameter’s type can hold. When the value is already of the right type, convert in the body: the transform buys the binder-time error, not the conversion.
Dynamic parameters
/// Reads a value; -Unit exists only when -Kind is temperature.
#[cmdlet(verb = "Get", noun = "Reading")]
#[derive(Default)]
pub struct GetReading {
#[param(mandatory, position = 0)]
pub kind: String,
}
impl Cmdlet for GetReading {
fn process(&mut self, ps: &Pipeline<'_>) -> PsResult<()> {
let unit = if ps.parameter_is_bound("Unit") {
String::from_ps(&ps.parameter("Unit")?)?
} else {
"none".to_string()
};
ps.write(format!("{}:{}", self.kind, unit))
}
}
#[dynamic_params(cmdlet = GetReading)]
fn reading_dynamic_params(bound: &PsHashtable) -> PsResult<Vec<DynamicParam>> {
let kind = if bound.contains("Kind")? { String::from_ps(&bound.get("Kind")?)? } else { String::new() };
if kind == "temperature" {
Ok(vec![DynamicParam::string("Unit").with_validate_set(["C".to_string(), "F".to_string()])])
} else {
Ok(Vec::new())
}
}
pwrs::export_module! {
name: "Readings",
cmdlets: [GetReading],
dynamic_params: [GetReading],
}#[dynamic_params(cmdlet = Type)]makes the function the type’sDynamicParamsimpl; list the cmdlet type underdynamic_paramsinexport_module!, and the shell addsIDynamicParametersto it.- The function runs before binding, on the pipeline thread, with the statically bound parameters as a
PsHashtable: the engine’s bound-parameter table plus every parameter whose setter ran (completion sets the properties without filling the table), names compared ignoring case, values unwrapped fromPSObject. The table is built once per call, and a function that returns no parameters hands the engine$null, which it reads as no dynamic parameters. DynamicParam::string(name)builds aSystem.Stringparameter; the struct’s fields are public, soclr_type(a CLR type name such asSystem.Int32),mandatory,position(-1for named),set,helpandvalidate_setcan all be set, andmandatory()andwith_validate_set(values)are builders.- Dynamic parameters are not fields of the struct. Read them inside a phase with
ps.parameter_is_bound(name)andps.parameter(name), which consult the engine’s bound-parameter table.
examples/hello/tests/Completers.Tests.ps1 shows -Unit absent from (Get-Command Get-RustReading).Parameters until -Kind temperature is bound, rejected for another kind, and validated against its set.