diff --git a/backend/parsers/windmill-parser-bash/src/lib.rs b/backend/parsers/windmill-parser-bash/src/lib.rs index 3629ee1e79..0893d65273 100644 --- a/backend/parsers/windmill-parser-bash/src/lib.rs +++ b/backend/parsers/windmill-parser-bash/src/lib.rs @@ -22,22 +22,82 @@ pub fn parse_bash_sig(code: &str) -> anyhow::Result { args, auto_kind: None, has_preprocessor: None, + ..Default::default() }) } else { Err(anyhow!("Error parsing bash script".to_string())) } } +/// PowerShell common parameter names that are automatically added by [CmdletBinding()]. +/// These should be filtered from the parsed signature since they are not user-defined. +const POWERSHELL_COMMON_PARAMS: &[&str] = &[ + "verbose", + "debug", + "erroraction", + "errorvariable", + "informationaction", + "informationvariable", + "outvariable", + "outbuffer", + "pipelinevariable", + "warningaction", + "warningvariable", + "whatif", + "confirm", + "progressaction", +]; + +/// Detects whether the script uses [CmdletBinding()] and whether it declares SupportsShouldProcess. +fn detect_cmdlet_binding(code: &str) -> (bool, bool) { + let attr_region = match extract_powershell_param_block_with_attributes(code, true) { + Some((region, _)) => region, + None => return (false, false), + }; + // Strip comment lines to avoid false positives from commented-out [CmdletBinding()] + let uncommented: String = attr_region + .lines() + .filter(|line| !line.trim_start().starts_with('#')) + .collect::>() + .join("\n"); + let lower = uncommented.to_lowercase(); + let has_cmd_binding = lower.contains("[cmdletbinding"); + let supports_should_process = has_cmd_binding + && lower.contains("supportsshouldprocess") + && !lower.contains("supportsshouldprocess=$false") + && !lower.contains("supportsshouldprocess = $false"); + (has_cmd_binding, supports_should_process) +} + pub fn parse_powershell_sig(code: &str) -> anyhow::Result { let parsed = parse_powershell_file(&code)?; - if let Some(x) = parsed { - let args = x; + if let Some(args) = parsed { + let (has_cmd_binding, supports_should_process) = detect_cmdlet_binding(code); + + // Filter out common parameters only when CmdletBinding is present + // (without CmdletBinding, $Verbose etc. are regular user-defined parameters) + let args = if has_cmd_binding { + args.into_iter() + .filter(|arg| !POWERSHELL_COMMON_PARAMS.contains(&arg.name.to_lowercase().as_str())) + .collect() + } else { + args + }; + Ok(MainArgSignature { star_args: false, star_kwargs: false, args, auto_kind: None, has_preprocessor: None, + has_cmd_binding: if has_cmd_binding { Some(true) } else { None }, + supports_should_process: if supports_should_process { + Some(true) + } else { + None + }, + + ..Default::default() }) } else { Err(anyhow!("Error parsing powershell script".to_string())) @@ -96,7 +156,10 @@ fn parse_bash_file(code: &str) -> anyhow::Result>> { /// This function uses the existing extract_powershell_param_block validation, which already /// ensures that only comments, whitespace, and attributes appear before param. So we can /// simply return everything from the beginning to the end of the param block. -pub fn extract_powershell_param_block_with_attributes(code: &str, include_attributes: bool) -> Option<(&str, &str)> { +pub fn extract_powershell_param_block_with_attributes( + code: &str, + include_attributes: bool, +) -> Option<(&str, &str)> { // First, use the existing function to validate and find the param block let param_block = extract_powershell_param_block(code, true)?; @@ -450,11 +513,15 @@ fn parse_powershell_parameters(content: &str) -> anyhow::Result> { // Check if this is a Parameter attribute with Mandatory (case-insensitive) let lower = bracket_content.to_lowercase(); - if lower.starts_with("parameter(") || lower.starts_with("parameter ") { + if lower.starts_with("parameter(") + || lower.starts_with("parameter ") + { // Check for Mandatory (case-insensitive) if lower.contains("mandatory") { // Check if it's explicitly set to false - if !lower.contains("mandatory=$false") && !lower.contains("mandatory = $false") { + if !lower.contains("mandatory=$false") + && !lower.contains("mandatory = $false") + { is_mandatory = true; } } @@ -471,7 +538,11 @@ fn parse_powershell_parameters(content: &str) -> anyhow::Result> { // Check if this looks like a type (simple word, possibly with []) let is_type = !bracket_content.contains('(') && !bracket_content.contains('=') - && (bracket_content.chars().next().unwrap_or(' ').is_alphabetic() + && (bracket_content + .chars() + .next() + .unwrap_or(' ') + .is_alphabetic() || bracket_content.starts_with('[')); if is_type && !found_dollar { @@ -529,7 +600,9 @@ fn parse_powershell_parameters(content: &str) -> anyhow::Result> { while let Some((i, ch)) = chars.peek().copied() { if in_string { - if ch == string_char && content.chars().nth(i.saturating_sub(1)) != Some('`') { + if ch == string_char + && content.chars().nth(i.saturating_sub(1)) != Some('`') + { in_string = false; default_end = i + 1; chars.next(); @@ -544,7 +617,9 @@ fn parse_powershell_parameters(content: &str) -> anyhow::Result> { chars.next(); } else if ch == ',' { break; - } else if ch.is_whitespace() && chars.clone().skip(1).next().map(|(_, c)| c) == Some(',') { + } else if ch.is_whitespace() + && chars.clone().skip(1).next().map(|(_, c)| c) == Some(',') + { break; } else { default_end = i + 1; @@ -552,12 +627,19 @@ fn parse_powershell_parameters(content: &str) -> anyhow::Result> { } } - default_value = Some(content[default_start..default_end].trim().to_string()); + default_value = + Some(content[default_start..default_end].trim().to_string()); } ',' => { // End of parameter, finalize it if let Some(name) = var_name.take() { - args.push(finalize_parameter(name, type_annotation.take(), default_value.take(), is_mandatory, validate_set.take())?); + args.push(finalize_parameter( + name, + type_annotation.take(), + default_value.take(), + is_mandatory, + validate_set.take(), + )?); } // Reset for next parameter @@ -576,7 +658,13 @@ fn parse_powershell_parameters(content: &str) -> anyhow::Result> { // Finalize last parameter if let Some(name) = var_name { - args.push(finalize_parameter(name, type_annotation, default_value, is_mandatory, validate_set)?); + args.push(finalize_parameter( + name, + type_annotation, + default_value, + is_mandatory, + validate_set, + )?); } Ok(args) @@ -722,7 +810,8 @@ non_required="${5:-}" } ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); @@ -813,14 +902,19 @@ non_required="${5:-}" Arg { otyp: Some("string".to_string()), // [string] (last type bracket with Mandatory and ValidateSet) name: "Message".to_string(), - typ: Typ::Str(Some(vec!["Green".to_string(), "Blue".to_string(), "Red".to_string()])), // ValidateSet enum + typ: Typ::Str(Some(vec![ + "Green".to_string(), + "Blue".to_string(), + "Red".to_string() + ])), // ValidateSet enum default: None, has_default: false, // Required (Mandatory attribute) oidx: None } ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); Ok(()) @@ -970,19 +1064,13 @@ non_required="${5:-}" // Valid: CmdletBinding with comments assert_eq!( - extract_powershell_param_block( - "# My function\n[CmdletBinding()]\nparam($Name)", - false - ), + extract_powershell_param_block("# My function\n[CmdletBinding()]\nparam($Name)", false), Some("$Name") ); // Valid: CmdletBinding with whitespace variations assert_eq!( - extract_powershell_param_block( - "[CmdletBinding()] \n param($Name)", - false - ), + extract_powershell_param_block("[CmdletBinding()] \n param($Name)", false), Some("$Name") ); @@ -1204,17 +1292,32 @@ param( // Test with CmdletBinding with parameters let code3 = "[CmdletBinding(DefaultParameterSetName='ByName')]\nparam($Name, $Id)"; let result3 = extract_powershell_param_block_with_attributes(code3, true); - assert_eq!(result3, Some(("[CmdletBinding(DefaultParameterSetName='ByName')]\nparam($Name, $Id)", ""))); + assert_eq!( + result3, + Some(( + "[CmdletBinding(DefaultParameterSetName='ByName')]\nparam($Name, $Id)", + "" + )) + ); // Test with multiple attributes let code4 = "[CmdletBinding()]\n[OutputType([string])]\nparam($Value)"; let result4 = extract_powershell_param_block_with_attributes(code4, true); - assert_eq!(result4, Some(("[CmdletBinding()]\n[OutputType([string])]\nparam($Value)", ""))); + assert_eq!( + result4, + Some(( + "[CmdletBinding()]\n[OutputType([string])]\nparam($Value)", + "" + )) + ); // Test with comment before attributes let code5 = "# My function\n[CmdletBinding()]\nparam($Name)"; let result5 = extract_powershell_param_block_with_attributes(code5, true); - assert_eq!(result5, Some(("# My function\n[CmdletBinding()]\nparam($Name)", ""))); + assert_eq!( + result5, + Some(("# My function\n[CmdletBinding()]\nparam($Name)", "")) + ); // Test with include_attributes = false (should only get param block, not attributes) let code6 = "[CmdletBinding()]\nparam($Name)"; @@ -1224,7 +1327,10 @@ param( // Test with code after param let code7 = "[CmdletBinding()]\nparam($Name)\nWrite-Host 'Hello'"; let result7 = extract_powershell_param_block_with_attributes(code7, true); - assert_eq!(result7, Some(("[CmdletBinding()]\nparam($Name)", "\nWrite-Host 'Hello'"))); + assert_eq!( + result7, + Some(("[CmdletBinding()]\nparam($Name)", "\nWrite-Host 'Hello'")) + ); // Test with code after param (without attributes) let code8 = "[CmdletBinding()]\nparam($Name)\nWrite-Host 'Hello'"; @@ -1392,10 +1498,85 @@ param( } ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); Ok(()) } + + #[test] + fn test_detect_cmdlet_binding() { + // Basic CmdletBinding + let (has_cb, has_ssp) = detect_cmdlet_binding("[CmdletBinding()]\nparam($Name)"); + assert!(has_cb); + assert!(!has_ssp); + + // CmdletBinding with SupportsShouldProcess + let (has_cb, has_ssp) = + detect_cmdlet_binding("[CmdletBinding(SupportsShouldProcess=$true)]\nparam($Path)"); + assert!(has_cb); + assert!(has_ssp); + + // CmdletBinding with SupportsShouldProcess=false + let (has_cb, has_ssp) = + detect_cmdlet_binding("[CmdletBinding(SupportsShouldProcess=$false)]\nparam($Path)"); + assert!(has_cb); + assert!(!has_ssp); + + // No CmdletBinding + let (has_cb, has_ssp) = detect_cmdlet_binding("param($Name)"); + assert!(!has_cb); + assert!(!has_ssp); + + // Case insensitive + let (has_cb, has_ssp) = + detect_cmdlet_binding("[cmdletbinding(supportsshouldprocess=$true)]\nparam($X)"); + assert!(has_cb); + assert!(has_ssp); + + // Commented out CmdletBinding should NOT be detected + let (has_cb, has_ssp) = + detect_cmdlet_binding("# [CmdletBinding(SupportsShouldProcess=$true)]\nparam($Path)"); + assert!(!has_cb); + assert!(!has_ssp); + } + + #[test] + fn test_powershell_common_param_filtering() -> anyhow::Result<()> { + // Common parameters declared in param() should be filtered out + let code = r#"[CmdletBinding()] +param( + [string]$Name, + [switch]$Verbose, + [string]$ErrorAction, + [int]$Age +)"#; + let sig = parse_powershell_sig(code)?; + assert_eq!(sig.args.len(), 2); + assert_eq!(sig.args[0].name, "Name"); + assert_eq!(sig.args[1].name, "Age"); + assert_eq!(sig.has_cmd_binding, Some(true)); + assert_eq!(sig.supports_should_process, None); + Ok(()) + } + + #[test] + fn test_powershell_sig_cmdlet_binding_metadata() -> anyhow::Result<()> { + // Script without CmdletBinding + let code = "param([string]$Name)"; + let sig = parse_powershell_sig(code)?; + assert_eq!(sig.has_cmd_binding, None); + assert_eq!(sig.supports_should_process, None); + + // Script with CmdletBinding + SupportsShouldProcess + let code = "[CmdletBinding(SupportsShouldProcess=$true)]\nparam([string]$Path)"; + let sig = parse_powershell_sig(code)?; + assert_eq!(sig.has_cmd_binding, Some(true)); + assert_eq!(sig.supports_should_process, Some(true)); + assert_eq!(sig.args.len(), 1); + assert_eq!(sig.args[0].name, "Path"); + Ok(()) + } } diff --git a/backend/parsers/windmill-parser-csharp/src/lib.rs b/backend/parsers/windmill-parser-csharp/src/lib.rs index 26c2607563..254fc3ab50 100644 --- a/backend/parsers/windmill-parser-csharp/src/lib.rs +++ b/backend/parsers/windmill-parser-csharp/src/lib.rs @@ -89,6 +89,7 @@ pub fn parse_csharp_sig_meta(code: &str) -> anyhow::Result { args, has_preprocessor: None, auto_kind, + ..Default::default() }; Ok(CsharpMainSigMeta { is_async, returns_void, class_name, main_sig, is_public }) diff --git a/backend/parsers/windmill-parser-go/src/lib.rs b/backend/parsers/windmill-parser-go/src/lib.rs index 19f1f52b38..cfb73abdae 100644 --- a/backend/parsers/windmill-parser-go/src/lib.rs +++ b/backend/parsers/windmill-parser-go/src/lib.rs @@ -43,6 +43,7 @@ pub fn parse_go_sig(code: &str) -> anyhow::Result { args, auto_kind: None, has_preprocessor: None, + ..Default::default() }) } else { Ok(MainArgSignature { @@ -51,6 +52,7 @@ pub fn parse_go_sig(code: &str) -> anyhow::Result { args: vec![], auto_kind: Some("lib".to_string()), has_preprocessor: None, + ..Default::default() }) } } @@ -244,7 +246,8 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam }, ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); diff --git a/backend/parsers/windmill-parser-graphql/src/lib.rs b/backend/parsers/windmill-parser-graphql/src/lib.rs index 255bb3df34..f7a62bdddb 100644 --- a/backend/parsers/windmill-parser-graphql/src/lib.rs +++ b/backend/parsers/windmill-parser-graphql/src/lib.rs @@ -21,6 +21,7 @@ pub fn parse_graphql_sig(code: &str) -> anyhow::Result { args, auto_kind: None, has_preprocessor: None, + ..Default::default() }) } else { Err(anyhow!("Error parsing sql".to_string())) @@ -126,7 +127,8 @@ query($i: Int, $arr: [String]!, $wahoo: String = "wahoo") { } ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); diff --git a/backend/parsers/windmill-parser-java/src/lib.rs b/backend/parsers/windmill-parser-java/src/lib.rs index 6d3eeb6f18..f7347caabc 100644 --- a/backend/parsers/windmill-parser-java/src/lib.rs +++ b/backend/parsers/windmill-parser-java/src/lib.rs @@ -81,6 +81,7 @@ pub fn parse_java_sig_meta(code: &str) -> anyhow::Result { args, has_preprocessor: None, auto_kind, + ..Default::default() }; Ok(JavaMainSigMeta { returns_void, class_name, main_sig, is_public }) diff --git a/backend/parsers/windmill-parser-nu/tests/tests.rs b/backend/parsers/windmill-parser-nu/tests/tests.rs index 6b16864c26..00c0865229 100644 --- a/backend/parsers/windmill-parser-nu/tests/tests.rs +++ b/backend/parsers/windmill-parser-nu/tests/tests.rs @@ -56,6 +56,7 @@ mod test { ], auto_kind: None, has_preprocessor: None, + ..Default::default() }, sig ); @@ -83,6 +84,7 @@ mod test { },], auto_kind: None, has_preprocessor: None, + ..Default::default() }, sig ); @@ -120,6 +122,7 @@ mod test { ], auto_kind: None, has_preprocessor: None, + ..Default::default() }, sig ); @@ -232,6 +235,7 @@ mod test { ], auto_kind: None, has_preprocessor: None, + ..Default::default() }, sig ); @@ -279,6 +283,7 @@ mod test { ], auto_kind: None, has_preprocessor: None, + ..Default::default() }, sig ); @@ -343,6 +348,7 @@ mod test { // },], // auto_kind: None, // has_preprocessor: None, + // ..Default::default() // }, // sig // ); @@ -373,6 +379,7 @@ mod test { },], auto_kind: None, has_preprocessor: None, + ..Default::default() }, sig ); @@ -420,6 +427,7 @@ mod test { ], auto_kind: None, has_preprocessor: None, + ..Default::default() }, sig ); @@ -448,6 +456,7 @@ mod test { },], auto_kind: None, has_preprocessor: None, + ..Default::default() }, sig ); @@ -480,6 +489,7 @@ mod test { // },], // auto_kind: None, // has_preprocessor: None, + // ..Default::default() // }, // sig // ); @@ -542,6 +552,7 @@ mod test { ], auto_kind: None, has_preprocessor: None, + ..Default::default() }, sig ); @@ -635,6 +646,7 @@ mod test { // ], // auto_kind: None, // has_preprocessor: None, + // ..Default::default() // }, // sig // ); diff --git a/backend/parsers/windmill-parser-php/src/lib.rs b/backend/parsers/windmill-parser-php/src/lib.rs index a7cb928bc3..f5d4c000c6 100644 --- a/backend/parsers/windmill-parser-php/src/lib.rs +++ b/backend/parsers/windmill-parser-php/src/lib.rs @@ -101,6 +101,7 @@ pub fn parse_php_signature( args, auto_kind: None, has_preprocessor, + ..Default::default() }) } else { Ok(MainArgSignature { @@ -109,6 +110,7 @@ pub fn parse_php_signature( args: vec![], auto_kind: Some("lib".to_string()), has_preprocessor, + ..Default::default() }) } } @@ -180,7 +182,8 @@ function main(string $input1 = \"hey\", bool $input2 = false, int $input3 = 3, f } ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); diff --git a/backend/parsers/windmill-parser-py/src/lib.rs b/backend/parsers/windmill-parser-py/src/lib.rs index 87848e9c0e..712fbb68ed 100644 --- a/backend/parsers/windmill-parser-py/src/lib.rs +++ b/backend/parsers/windmill-parser-py/src/lib.rs @@ -366,6 +366,7 @@ pub fn parse_python_signature( Some("lib".to_string()) }, has_preprocessor: Some(has_preprocessor), + ..Default::default() }); } @@ -477,6 +478,7 @@ pub fn parse_python_signature( .collect(), auto_kind: None, has_preprocessor: Some(has_preprocessor), + ..Default::default() }) } else { Ok(MainArgSignature { @@ -489,6 +491,7 @@ pub fn parse_python_signature( None }, has_preprocessor: Some(has_preprocessor), + ..Default::default() }) } } @@ -755,7 +758,8 @@ def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = byt }, ], auto_kind: None, - has_preprocessor: Some(false) + has_preprocessor: Some(false), + ..Default::default() } ); @@ -820,7 +824,8 @@ def main(test1: str, } ], auto_kind: None, - has_preprocessor: Some(false) + has_preprocessor: Some(false), + ..Default::default() } ); @@ -880,7 +885,8 @@ def main(test1: str, } ], auto_kind: None, - has_preprocessor: Some(false) + has_preprocessor: Some(false), + ..Default::default() } ); @@ -924,7 +930,8 @@ def main(test1: Literal["foo", "bar"], test2: List[Literal["foo", "bar"]]): retu } ], auto_kind: None, - has_preprocessor: Some(false) + has_preprocessor: Some(false), + ..Default::default() } ); @@ -955,7 +962,8 @@ def main(test1: DynSelect_foo): return oidx: None }], auto_kind: None, - has_preprocessor: Some(false) + has_preprocessor: Some(false), + ..Default::default() } ); @@ -979,7 +987,8 @@ def hello(): return star_kwargs: false, args: vec![], auto_kind: Some("lib".to_string()), - has_preprocessor: Some(false) + has_preprocessor: Some(false), + ..Default::default() } ); @@ -1007,7 +1016,8 @@ def main(): return star_kwargs: false, args: vec![], auto_kind: None, - has_preprocessor: Some(true) + has_preprocessor: Some(true), + ..Default::default() } ); @@ -1072,7 +1082,8 @@ def main(a: list, e: List[int], b: list = [1,2,3,4], c = [1,2,3,4], d = ["a", "b } ], auto_kind: None, - has_preprocessor: Some(false) + has_preprocessor: Some(false), + ..Default::default() } ); @@ -1121,7 +1132,8 @@ def main(a: str, b: Optional[str], c: str | None): return }, ], auto_kind: None, - has_preprocessor: Some(false) + has_preprocessor: Some(false), + ..Default::default() } ); diff --git a/backend/parsers/windmill-parser-r/src/lib.rs b/backend/parsers/windmill-parser-r/src/lib.rs index 4d018fa8cc..e73d358a60 100644 --- a/backend/parsers/windmill-parser-r/src/lib.rs +++ b/backend/parsers/windmill-parser-r/src/lib.rs @@ -30,6 +30,7 @@ pub fn parse_r_sig_meta(code: &str) -> anyhow::Result { args: args.unwrap_or_default(), has_preprocessor: None, auto_kind: None, + ..Default::default() }; Ok(main_sig) diff --git a/backend/parsers/windmill-parser-ruby/src/lib.rs b/backend/parsers/windmill-parser-ruby/src/lib.rs index 805d2f9933..8443fede70 100644 --- a/backend/parsers/windmill-parser-ruby/src/lib.rs +++ b/backend/parsers/windmill-parser-ruby/src/lib.rs @@ -41,6 +41,7 @@ pub fn parse_ruby_sig_meta(code: &str) -> anyhow::Result { args: args.unwrap_or_default(), has_preprocessor: None, auto_kind, + ..Default::default() }; Ok(main_sig) diff --git a/backend/parsers/windmill-parser-rust/src/lib.rs b/backend/parsers/windmill-parser-rust/src/lib.rs index 0d72ad4893..74a7e15de6 100644 --- a/backend/parsers/windmill-parser-rust/src/lib.rs +++ b/backend/parsers/windmill-parser-rust/src/lib.rs @@ -30,6 +30,7 @@ pub fn parse_rust_signature(code: &str) -> anyhow::Result { args, auto_kind: None, has_preprocessor: None, + ..Default::default() }) } else { Ok(MainArgSignature { @@ -38,6 +39,7 @@ pub fn parse_rust_signature(code: &str) -> anyhow::Result { args: vec![], auto_kind: Some("lib".to_string()), has_preprocessor: None, + ..Default::default() }) } } diff --git a/backend/parsers/windmill-parser-sql/src/lib.rs b/backend/parsers/windmill-parser-sql/src/lib.rs index c8aef3eed5..d10cbc7125 100644 --- a/backend/parsers/windmill-parser-sql/src/lib.rs +++ b/backend/parsers/windmill-parser-sql/src/lib.rs @@ -30,6 +30,7 @@ pub fn parse_mysql_sig(code: &str) -> anyhow::Result { args, auto_kind: None, has_preprocessor: None, + ..Default::default() }) } else { Err(anyhow!("Error parsing sql".to_string())) @@ -46,6 +47,7 @@ pub fn parse_oracledb_sig(code: &str) -> anyhow::Result { args, auto_kind: None, has_preprocessor: None, + ..Default::default() }) } else { Err(anyhow!("Error parsing sql".to_string())) @@ -67,6 +69,7 @@ pub fn parse_pgsql_sig_with_typed_schema(code: &str) -> anyhow::Result<(MainArgS args, auto_kind: None, has_preprocessor: None, + ..Default::default() }, typed_schema, )) @@ -85,6 +88,7 @@ pub fn parse_bigquery_sig(code: &str) -> anyhow::Result { args, auto_kind: None, has_preprocessor: None, + ..Default::default() }) } else { Err(anyhow!("Error parsing sql".to_string())) @@ -100,6 +104,7 @@ pub fn parse_duckdb_sig(code: &str) -> anyhow::Result { args, auto_kind: None, has_preprocessor: None, + ..Default::default() }) } else { Err(anyhow!("Error parsing sql".to_string())) @@ -116,6 +121,7 @@ pub fn parse_snowflake_sig(code: &str) -> anyhow::Result { args, auto_kind: None, has_preprocessor: None, + ..Default::default() }) } else { Err(anyhow!("Error parsing sql".to_string())) @@ -132,6 +138,7 @@ pub fn parse_mssql_sig(code: &str) -> anyhow::Result { args, auto_kind: None, has_preprocessor: None, + ..Default::default() }) } else { Err(anyhow!("Error parsing sql".to_string())) @@ -944,7 +951,8 @@ SELECT * FROM table WHERE token=$1::TEXT AND image=$2::BIGINT }, ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); @@ -993,7 +1001,8 @@ SELECT $2::TEXT; }, ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); @@ -1120,7 +1129,8 @@ SELECT ?, ?; }, ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); @@ -1168,7 +1178,8 @@ SELECT :param2; }, ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); @@ -1208,7 +1219,8 @@ SELECT @token; }, ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); @@ -1256,7 +1268,8 @@ SELECT ?; } ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); @@ -1304,7 +1317,8 @@ SELECT @P2; }, ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); @@ -1353,7 +1367,8 @@ SELECT * FROM table_name WHERE thing = :name4; }, ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); @@ -1391,7 +1406,8 @@ SELECT * FROM users WHERE id = $1 AND email = $2::text; }, ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); @@ -1429,7 +1445,8 @@ SELECT * FROM users LIMIT $1 OFFSET $2; }, ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); @@ -1479,7 +1496,8 @@ WHERE id = $1 }, ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); @@ -1506,7 +1524,8 @@ SELECT * FROM users WHERE id = ANY($1); oidx: Some(1), },], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); @@ -1535,7 +1554,8 @@ SELECT $1::integer; oidx: Some(1), },], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); @@ -1567,7 +1587,8 @@ SELECT x oidx: None, },], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); diff --git a/backend/parsers/windmill-parser-ts/src/lib.rs b/backend/parsers/windmill-parser-ts/src/lib.rs index 125834a3ac..0ac3df6ec4 100644 --- a/backend/parsers/windmill-parser-ts/src/lib.rs +++ b/backend/parsers/windmill-parser-ts/src/lib.rs @@ -462,6 +462,7 @@ pub fn parse_deno_signature( }, auto_kind, has_preprocessor: Some(has_preprocessor), + ..Default::default() }; Ok(r) } diff --git a/backend/parsers/windmill-parser-ts/tests/tests.rs b/backend/parsers/windmill-parser-ts/tests/tests.rs index b50169ca68..643b2ba554 100644 --- a/backend/parsers/windmill-parser-ts/tests/tests.rs +++ b/backend/parsers/windmill-parser-ts/tests/tests.rs @@ -47,6 +47,7 @@ mod tests { args: vec![], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -105,6 +106,7 @@ mod tests { ], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -154,6 +156,7 @@ mod tests { ], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -203,6 +206,7 @@ mod tests { ], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -234,6 +238,7 @@ mod tests { },], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -265,6 +270,7 @@ mod tests { },], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -305,6 +311,7 @@ mod tests { }], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -343,6 +350,7 @@ mod tests { }], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -401,6 +409,7 @@ mod tests { }], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -459,6 +468,7 @@ mod tests { ], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -508,6 +518,7 @@ mod tests { ], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -557,6 +568,7 @@ mod tests { args: vec![], auto_kind: Some("lib".to_string()), has_preprocessor: Some(false), + ..Default::default() } ); } @@ -584,6 +596,7 @@ mod tests { }], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -613,6 +626,7 @@ mod tests { }], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -642,6 +656,7 @@ mod tests { }], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -691,6 +706,7 @@ mod tests { ], auto_kind: None, has_preprocessor: Some(false), + ..Default::default() } ); } @@ -731,6 +747,7 @@ mod tests { }], auto_kind: None, has_preprocessor: Some(true), + ..Default::default() } ); } @@ -761,6 +778,7 @@ mod tests { }], auto_kind: None, has_preprocessor: Some(true), + ..Default::default() } ); } diff --git a/backend/parsers/windmill-parser-wasm/tests/wasm.rs b/backend/parsers/windmill-parser-wasm/tests/wasm.rs index 14a72b7e2a..bbb37ba468 100644 --- a/backend/parsers/windmill-parser-wasm/tests/wasm.rs +++ b/backend/parsers/windmill-parser-wasm/tests/wasm.rs @@ -141,7 +141,8 @@ export function main(test1?: string, test2: string = \"burkina\", } ], auto_kind: None, - has_preprocessor: Some(false) + has_preprocessor: Some(false), + ..Default::default() } ); @@ -220,7 +221,8 @@ export function main(test2 = \"burkina\", } ], auto_kind: None, - has_preprocessor: Some(false) + has_preprocessor: Some(false), + ..Default::default() } ); @@ -271,7 +273,8 @@ export function main(foo: FooBar, {a, b}: FooBar, {c, d}: FooBar = {a: \"foo\", } ], auto_kind: None, - has_preprocessor: Some(false) + has_preprocessor: Some(false), + ..Default::default() } ); @@ -303,7 +306,8 @@ export function main(foo: (\"foo\" | \"bar\")[]) { oidx: None }], auto_kind: None, - has_preprocessor: Some(false) + has_preprocessor: Some(false), + ..Default::default() } ); @@ -447,7 +451,8 @@ Write-Output 'Testing...' } ], auto_kind: None, - has_preprocessor: None + has_preprocessor: None, + ..Default::default() } ); diff --git a/backend/parsers/windmill-parser-yaml/src/lib.rs b/backend/parsers/windmill-parser-yaml/src/lib.rs index f010d21ab4..887f94fa38 100644 --- a/backend/parsers/windmill-parser-yaml/src/lib.rs +++ b/backend/parsers/windmill-parser-yaml/src/lib.rs @@ -27,6 +27,7 @@ pub fn parse_ansible_sig(inner_content: &str) -> anyhow::Result anyhow::Result, pub auto_kind: Option, pub has_preprocessor: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub has_cmd_binding: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_should_process: Option, } #[derive(Serialize, Clone, Debug, PartialEq)] diff --git a/backend/windmill-worker/src/pwsh_executor.rs b/backend/windmill-worker/src/pwsh_executor.rs index 6084ce3cce..9924913ad0 100644 --- a/backend/windmill-worker/src/pwsh_executor.rs +++ b/backend/windmill-worker/src/pwsh_executor.rs @@ -337,7 +337,7 @@ pub async fn handle_powershell_job( envs: HashMap, occupancy_metrics: &mut OccupancyMetrics, ) -> Result, Error> { - let pwsh_args = { + let (pwsh_args, ps_preferences) = { let args = build_args_map(job, client, &db).await?.map(Json); let job_args = if args.is_some() { args.as_ref() @@ -347,7 +347,7 @@ pub async fn handle_powershell_job( let parsed_sig = windmill_parser_bash::parse_powershell_sig(&content)?; - parsed_sig + let user_args = parsed_sig .args .iter() .filter_map(|arg| { @@ -384,7 +384,34 @@ pub async fn handle_powershell_job( } }) .collect::>() - .join(" ") + .join(" "); + + // Extract PowerShell common parameters (_wm_ps_* keys) + // All common params are injected as preference variables into main.ps1 + // (not CLI args) so they only affect user code, not module loading. + let mut preference_lines: Vec = Vec::new(); + if let Some(args_map) = job_args { + if let Some(v) = args_map.get("_wm_ps_verbose") { + if serde_json::from_str::(v.get()).unwrap_or(false) { + preference_lines.push("$VerbosePreference = 'Continue'".to_string()); + } + } + if let Some(v) = args_map.get("_wm_ps_debug") { + if serde_json::from_str::(v.get()).unwrap_or(false) { + preference_lines.push("$DebugPreference = 'Continue'".to_string()); + } + } + if let Some(v) = args_map.get("_wm_ps_error_action") { + if let Ok(action) = serde_json::from_str::(v.get()) { + if matches!(action.as_str(), "Stop" | "Continue" | "SilentlyContinue") { + preference_lines.push(format!("$ErrorActionPreference = '{action}'")); + } + } + } + } + let ps_preferences = preference_lines.join("\n"); + + (user_args, ps_preferences) }; // Resolve modules from workspace dependencies and/or script imports @@ -570,12 +597,22 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"", }\n"; // make sure param() with its attributes is first + let preferences_section = if ps_preferences.is_empty() { + String::new() + } else { + format!("{}\n", ps_preferences) + }; let content: String = if let Some((param_block, remaining_code)) = windmill_parser_bash::extract_powershell_param_block_with_attributes(&content, true) { format!( - "{}\n{}\n{}\n{}\n{}", - param_block, profile, strict_termination_start, remaining_code, strict_termination_end + "{}\n{}\n{}\n{}{}\n{}", + param_block, + profile, + strict_termination_start, + preferences_section, + remaining_code, + strict_termination_end ) } else { format!("{}\n{}", profile, content) @@ -583,18 +620,29 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"", write_file(job_dir, "main.ps1", content.as_str())?; - write_file( - job_dir, - "wrapper.ps1", - &format!( + let has_common_params = !ps_preferences.is_empty(); + let wrapper_content = if has_common_params { + format!( + "$ErrorActionPreference = 'Stop'\n\ + $pipe = New-TemporaryFile\n\ + ./main.ps1 {pwsh_args} 4>verbose.log 5>debug.log 2>&1 | Tee-Object -FilePath $pipe\n\ + Get-Content -Path $pipe | Select-Object -Last 1 | Set-Content -Path './result2.out'\n\ + if (Test-Path verbose.log) {{ Get-Content verbose.log | ForEach-Object {{ Write-Output \"VERBOSE: $_\" }} }}\n\ + if (Test-Path debug.log) {{ Get-Content debug.log | ForEach-Object {{ Write-Output \"DEBUG: $_\" }} }}\n\ + Remove-Item $pipe\n\ + exit $LASTEXITCODE\n" + ) + } else { + format!( "$ErrorActionPreference = 'Stop'\n\ $pipe = New-TemporaryFile\n\ ./main.ps1 {pwsh_args} 2>&1 | Tee-Object -FilePath $pipe\n\ Get-Content -Path $pipe | Select-Object -Last 1 | Set-Content -Path './result2.out'\n\ Remove-Item $pipe\n\ exit $LASTEXITCODE\n" - ), - )?; + ) + }; + write_file(job_dir, "wrapper.ps1", &wrapper_content)?; let mut reserved_variables = get_reserved_variables(job, &client.token, db, parent_runnable_path).await?; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 64f640a1e3..76a706bcf7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -84,7 +84,7 @@ "windmill-parser-wasm-php": "1.647.1", "windmill-parser-wasm-py": "1.657.2", "windmill-parser-wasm-r": "1.668.1", - "windmill-parser-wasm-regex": "1.653.0", + "windmill-parser-wasm-regex": "^1.670.0", "windmill-parser-wasm-ruby": "1.526.1", "windmill-parser-wasm-rust": "1.647.1", "windmill-parser-wasm-ts": "1.657.2", @@ -844,7 +844,6 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -856,7 +855,6 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -867,7 +865,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1357,7 +1354,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1514,7 +1510,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1531,7 +1526,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1548,7 +1542,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1565,7 +1558,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1582,7 +1574,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1599,7 +1590,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1616,7 +1606,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1633,7 +1622,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1650,7 +1638,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1667,7 +1654,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1684,7 +1670,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1701,7 +1686,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1718,7 +1702,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1735,7 +1718,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1752,7 +1734,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2058,7 +2039,6 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6867,7 +6847,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -7366,7 +7346,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7387,7 +7366,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7408,7 +7386,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7429,7 +7406,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7450,7 +7426,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7471,7 +7446,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7492,7 +7466,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7513,7 +7486,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7534,7 +7506,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7555,7 +7526,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7576,7 +7546,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12152,21 +12121,6 @@ } } }, - "node_modules/svelte-check/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", @@ -12897,7 +12851,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -13692,9 +13646,9 @@ "integrity": "sha512-5YNeUibxpNBvYrxCgQcz1PxGhTFx2CyEpg2udtIhq7bx0d4gF/KDZVupMeQmAObmrEtTSFGUWNRJ4zXSWNrSpQ==" }, "node_modules/windmill-parser-wasm-regex": { - "version": "1.653.0", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.653.0.tgz", - "integrity": "sha512-LEvLhb8uCb/jGMzd+lwP886LcwxuTE+W04wdyqmdqy1JD9FBToKeiqW7CtZWqbWr1oI8yK9U3jmqSqyOcUCzRw==" + "version": "1.670.0", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.670.0.tgz", + "integrity": "sha512-foQBwLn7L3JpmPSzMYmFhpVvUnXB1KX4v1rGBsm5W6KircwrjW3VXl/Ih9Izjyju2RRK+tYzjN8vvd2Zo7ulKQ==" }, "node_modules/windmill-parser-wasm-ruby": { "version": "1.526.1", diff --git a/frontend/package.json b/frontend/package.json index 1c15acc3bf..e43122784c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -157,7 +157,7 @@ "windmill-parser-wasm-php": "1.647.1", "windmill-parser-wasm-py": "1.657.2", "windmill-parser-wasm-r": "1.668.1", - "windmill-parser-wasm-regex": "1.653.0", + "windmill-parser-wasm-regex": "^1.670.0", "windmill-parser-wasm-ruby": "1.526.1", "windmill-parser-wasm-rust": "1.647.1", "windmill-parser-wasm-ts": "1.657.2", diff --git a/frontend/src/lib/components/PowerShellCommonParams.svelte b/frontend/src/lib/components/PowerShellCommonParams.svelte new file mode 100644 index 0000000000..75a8d3a862 --- /dev/null +++ b/frontend/src/lib/components/PowerShellCommonParams.svelte @@ -0,0 +1,89 @@ + + +
+ + {#if !collapsed} +
+ + +
+ ErrorAction +
+