diff --git a/Class/ParameterParser.vb b/Class/ParameterParser.vb
index 779fa98..9dee7a5 100644
--- a/Class/ParameterParser.vb
+++ b/Class/ParameterParser.vb
@@ -1,92 +1,131 @@
-' -----------------------------------------------------------------------------
-' Copyright (c) svtica. All rights reserved.
-' File: ParameterParser.vb
-' Author: LiteTask contributors
-' Date: 2026-04-25
-' Purpose: Parse "key=value" parameter strings used by TaskRunner. Supports
-' cmd-style quoting (key="value with spaces") while remaining
-' backward-compatible with the original whitespace-delimited form.
-' -----------------------------------------------------------------------------
-Namespace LiteTask
- Public Module ParameterParser
-
- '''
- ''' Parses a parameter string of the form `key=value [key2=value2 ...]`.
- ''' Values may be quoted with double quotes to include spaces:
- ''' key="value with spaces"
- ''' Tokens without an `=` are skipped, as are `=value` fragments.
- ''' Last occurrence wins for duplicate keys.
- '''
- Public Function Parse(parameters As String) As Dictionary(Of String, String)
- Dim result As New Dictionary(Of String, String)
- If String.IsNullOrEmpty(parameters) Then Return result
-
- Dim i As Integer = 0
- Dim len As Integer = parameters.Length
-
- While i < len
- ' Skip leading whitespace
- While i < len AndAlso Char.IsWhiteSpace(parameters(i))
- i += 1
- End While
- If i >= len Then Exit While
-
- ' Read key: up to '=' or whitespace
- Dim keyStart As Integer = i
- While i < len AndAlso parameters(i) <> "="c AndAlso Not Char.IsWhiteSpace(parameters(i))
- i += 1
- End While
-
- ' No '=' for this token: skip token entirely (preserves original behavior
- ' where bare words without '=' were dropped).
- If i >= len OrElse parameters(i) <> "="c Then
- Continue While
- End If
-
- Dim key As String = parameters.Substring(keyStart, i - keyStart)
- i += 1 ' consume '='
-
- ' Empty key (`=value`) is skipped to match original behavior.
- If String.IsNullOrEmpty(key) Then
- ' Advance past the value so we don't reparse it as a new token
- SkipValue(parameters, i)
- Continue While
- End If
-
- Dim value As String = ReadValue(parameters, i)
- result(key) = value
- End While
-
- Return result
- End Function
-
- Private Function ReadValue(s As String, ByRef i As Integer) As String
- Dim len As Integer = s.Length
- If i >= len Then Return String.Empty
-
- If s(i) = """"c Then
- ' Quoted value: read until next '"' or end of string.
- i += 1
- Dim valueStart As Integer = i
- While i < len AndAlso s(i) <> """"c
- i += 1
- End While
- Dim value As String = s.Substring(valueStart, i - valueStart)
- If i < len Then i += 1 ' consume closing quote
- Return value
- End If
-
- ' Unquoted value: read until whitespace.
- Dim unquotedStart As Integer = i
- While i < len AndAlso Not Char.IsWhiteSpace(s(i))
- i += 1
- End While
- Return s.Substring(unquotedStart, i - unquotedStart)
- End Function
-
- Private Sub SkipValue(s As String, ByRef i As Integer)
- ReadValue(s, i)
- End Sub
-
- End Module
-End Namespace
+' -----------------------------------------------------------------------------
+' Copyright (c) svtica. All rights reserved.
+' File: ParameterParser.vb
+' Author: LiteTask contributors
+' Date: 2026-04-25
+' Purpose: Parse parameter strings used by TaskRunner. Supports three forms
+' that can be mixed in the same string:
+' key=value
+' key="value with spaces"
+' -Name Value (PowerShell-style)
+' -Name "Value with spaces" (PowerShell-style)
+' -Switch (no value -> stored as Nothing)
+' -----------------------------------------------------------------------------
+Namespace LiteTask
+ Public Module ParameterParser
+
+ '''
+ ''' Parses a parameter string. Supports both `key=value` and
+ ''' PowerShell-style `-Name Value` / `-Switch` syntax in the same
+ ''' input. Returns an ordered dictionary mapping each parameter name
+ ''' to its value. A switch parameter (no value provided) is stored
+ ''' as Nothing so callers can distinguish it from an empty string.
+ ''' Tokens without an `=` and that don't start with `-Letter` are
+ ''' skipped to preserve the original lenient behavior.
+ ''' Last occurrence wins for duplicate keys.
+ '''
+ Public Function Parse(parameters As String) As Dictionary(Of String, Object)
+ Dim result As New Dictionary(Of String, Object)(StringComparer.OrdinalIgnoreCase)
+ If String.IsNullOrEmpty(parameters) Then Return result
+
+ Dim i As Integer = 0
+ Dim len As Integer = parameters.Length
+
+ While i < len
+ ' Skip leading whitespace
+ While i < len AndAlso Char.IsWhiteSpace(parameters(i))
+ i += 1
+ End While
+ If i >= len Then Exit While
+
+ ' PowerShell-style: -Name [Value] (a dash followed by a letter)
+ If parameters(i) = "-"c AndAlso i + 1 < len AndAlso IsNameStart(parameters(i + 1)) Then
+ i += 1 ' consume '-'
+ Dim keyStart As Integer = i
+ While i < len AndAlso Not Char.IsWhiteSpace(parameters(i)) AndAlso parameters(i) <> "="c
+ i += 1
+ End While
+ Dim psKey As String = parameters.Substring(keyStart, i - keyStart)
+
+ ' Allow `-Name=Value` as a convenience
+ If i < len AndAlso parameters(i) = "="c Then
+ i += 1
+ result(psKey) = ReadValue(parameters, i)
+ Continue While
+ End If
+
+ ' Skip whitespace between name and value
+ Dim valueScan As Integer = i
+ While valueScan < len AndAlso Char.IsWhiteSpace(parameters(valueScan))
+ valueScan += 1
+ End While
+
+ ' If next non-whitespace is another -Name, this is a switch.
+ If valueScan >= len OrElse
+ (parameters(valueScan) = "-"c AndAlso valueScan + 1 < len AndAlso IsNameStart(parameters(valueScan + 1))) Then
+ result(psKey) = Nothing
+ ' Don't consume the next token; loop will pick it up.
+ Else
+ i = valueScan
+ result(psKey) = ReadValue(parameters, i)
+ End If
+ Continue While
+ End If
+
+ ' key=value form
+ Dim keyStartEq As Integer = i
+ While i < len AndAlso parameters(i) <> "="c AndAlso Not Char.IsWhiteSpace(parameters(i))
+ i += 1
+ End While
+
+ If i >= len OrElse parameters(i) <> "="c Then
+ ' Bare token without '=': skip to preserve original lenient behavior.
+ Continue While
+ End If
+
+ Dim key As String = parameters.Substring(keyStartEq, i - keyStartEq)
+ i += 1 ' consume '='
+
+ If String.IsNullOrEmpty(key) Then
+ SkipValue(parameters, i)
+ Continue While
+ End If
+
+ result(key) = ReadValue(parameters, i)
+ End While
+
+ Return result
+ End Function
+
+ Private Function IsNameStart(c As Char) As Boolean
+ Return Char.IsLetter(c) OrElse c = "_"c
+ End Function
+
+ Private Function ReadValue(s As String, ByRef i As Integer) As String
+ Dim len As Integer = s.Length
+ If i >= len Then Return String.Empty
+
+ If s(i) = """"c Then
+ i += 1
+ Dim valueStart As Integer = i
+ While i < len AndAlso s(i) <> """"c
+ i += 1
+ End While
+ Dim value As String = s.Substring(valueStart, i - valueStart)
+ If i < len Then i += 1 ' consume closing quote
+ Return value
+ End If
+
+ Dim unquotedStart As Integer = i
+ While i < len AndAlso Not Char.IsWhiteSpace(s(i))
+ i += 1
+ End While
+ Return s.Substring(unquotedStart, i - unquotedStart)
+ End Function
+
+ Private Sub SkipValue(s As String, ByRef i As Integer)
+ ReadValue(s, i)
+ End Sub
+
+ End Module
+End Namespace
diff --git a/Class/TaskRunner.vb b/Class/TaskRunner.vb
index 4c509f1..1fe090b 100644
--- a/Class/TaskRunner.vb
+++ b/Class/TaskRunner.vb
@@ -96,12 +96,27 @@ Namespace LiteTask
Try
_logger.LogInfo($"Executing Batch task: {taskAction.Name}")
+ ' Trailing args appended to `cmd.exe /c "target.bat" ` so the
+ ' script receives them as %1..%* (works for both code paths).
+ Dim batchArgs As String = If(String.IsNullOrWhiteSpace(taskAction.Parameters),
+ String.Empty,
+ " " & taskAction.Parameters.Trim())
+
If credential IsNot Nothing Then
' Parse UNC paths from batch content
Dim batchContent = File.ReadAllText(taskAction.Target)
Dim uncPaths = ParseUNCPaths(batchContent)
Dim envPassVar = $"BATCH_PASS_{Guid.NewGuid().ToString("N")}"
+ ' Write the augmented script (net use prelude + original body) to a
+ ' temp .bat so we can invoke it as `cmd.exe /c "tempfile" `.
+ ' The previous implementation streamed the script via stdin to
+ ' `cmd /c -`, which cannot receive %1..%* — task parameters were
+ ' silently dropped.
+ Dim tempDir = Path.Combine(Application.StartupPath, "LiteTaskData", "temp")
+ Directory.CreateDirectory(tempDir)
+ Dim tempBatFile = Path.Combine(tempDir, $"batch_{Guid.NewGuid():N}.bat")
+
Try
' Set up credentials
Using securePass As New SecureString()
@@ -135,22 +150,16 @@ Namespace LiteTask
modifiedContent.AppendLine($"net use {uncPath} /delete")
Next
- ' Execute using PsExec
- Using ms As New MemoryStream()
- Using writer As New StreamWriter(ms)
- Await writer.WriteAsync(modifiedContent.ToString())
- Await writer.FlushAsync()
- ms.Position = 0
+ Await File.WriteAllTextAsync(tempBatFile, modifiedContent.ToString())
- Dim psExecPath = Path.Combine(_toolManager._toolsPath, "PsExec64.exe")
- Dim fullCommand = $"{baseArgs} -p ""%{envPassVar}%"" cmd.exe /c -"
+ Dim psExecPath = Path.Combine(_toolManager._toolsPath, "PsExec64.exe")
+ Dim fullCommand = $"{baseArgs} -p ""%{envPassVar}%"" cmd.exe /c ""{tempBatFile}""{batchArgs}"
- Using process As New Process With {
+ Using process As New Process With {
.StartInfo = New ProcessStartInfo With {
.FileName = psExecPath,
.Arguments = fullCommand,
.UseShellExecute = False,
- .RedirectStandardInput = True,
.RedirectStandardOutput = True,
.RedirectStandardError = True,
.CreateNoWindow = True,
@@ -158,20 +167,25 @@ Namespace LiteTask
.StandardErrorEncoding = Encoding.UTF8
}
}
- Return Await RunProcess(process)
- End Using
- End Using
+ Return Await RunProcess(process)
End Using
Finally
Environment.SetEnvironmentVariable(envPassVar, Nothing, EnvironmentVariableTarget.Process)
+ If File.Exists(tempBatFile) Then
+ Try
+ File.Delete(tempBatFile)
+ Catch ex As Exception
+ _logger.LogWarning($"Failed to delete temp batch file {tempBatFile}: {ex.Message}")
+ End Try
+ End If
End Try
Else
' Execute without credentials
Using process As New Process With {
.StartInfo = New ProcessStartInfo With {
.FileName = "cmd.exe",
- .Arguments = $"/c ""{taskAction.Target}""",
+ .Arguments = $"/c ""{taskAction.Target}""{batchArgs}",
.UseShellExecute = False,
.RedirectStandardOutput = True,
.RedirectStandardError = True,
@@ -535,15 +549,18 @@ Namespace LiteTask
Dim sqlContent = File.ReadAllText(taskAction.Target)
Dim sqlInfo = ExtractSqlInfo(sqlContent)
Dim sqlConfig = _xmlManager.GetSqlConfiguration()
+ ' The task's Parameters field uses the same `key=value` / `-Name value`
+ ' grammar as PowerShell tasks. ParseSqlParameters parses T-SQL EXEC
+ ' syntax inside the .sql file content, not the form field.
Dim parameters = If(Not String.IsNullOrEmpty(taskAction.Parameters),
- ParseSqlParameters(taskAction.Parameters),
- New Dictionary(Of String, String))
+ ParameterParser.Parse(taskAction.Parameters),
+ New Dictionary(Of String, Object)(StringComparer.OrdinalIgnoreCase))
- Dim server = If(parameters.ContainsKey("server"), parameters("server"),
+ Dim server = If(parameters.ContainsKey("server"), CStr(parameters("server")),
If(Not String.IsNullOrEmpty(sqlInfo.ServerName), sqlInfo.ServerName,
sqlConfig("DefaultServer")))
- Dim database = If(parameters.ContainsKey("database"), parameters("database"),
+ Dim database = If(parameters.ContainsKey("database"), CStr(parameters("database")),
If(Not String.IsNullOrEmpty(sqlInfo.DatabaseName), sqlInfo.DatabaseName,
sqlConfig("DefaultDatabase")))
@@ -659,7 +676,11 @@ Namespace LiteTask
If Not String.IsNullOrEmpty(taskAction.Parameters) Then
_logger.LogInfo($"Adding parameters: {taskAction.Parameters}")
For Each param In ParseParameters(taskAction.Parameters)
- powerShell.AddParameter(param.Key, param.Value)
+ If param.Value Is Nothing Then
+ powerShell.AddParameter(param.Key)
+ Else
+ powerShell.AddParameter(param.Key, param.Value)
+ End If
Next
End If
@@ -802,14 +823,14 @@ Namespace LiteTask
- Private Function ParseParameters(parameters As String) As Dictionary(Of String, String)
+ Private Function ParseParameters(parameters As String) As Dictionary(Of String, Object)
Try
Dim result = ParameterParser.Parse(parameters)
_logger.LogInfo($"Parsed {result.Count} parameters successfully")
Return result
Catch ex As Exception
_logger.LogError($"Error parsing parameters: {ex.Message}")
- Return New Dictionary(Of String, String)
+ Return New Dictionary(Of String, Object)
End Try
End Function
diff --git a/Tests/ParameterParserTests.vb b/Tests/ParameterParserTests.vb
index 7cc1748..9a34410 100644
--- a/Tests/ParameterParserTests.vb
+++ b/Tests/ParameterParserTests.vb
@@ -1,101 +1,151 @@
-' -----------------------------------------------------------------------------
-' Copyright (c) svtica. All rights reserved.
-' File: ParameterParserTests.vb
-' Author: LiteTask contributors
-' Date: 2026-04-25
-' Purpose: Unit tests for LiteTask.ParameterParser covering the original
-' unquoted form and the new cmd-style quoting.
-' -----------------------------------------------------------------------------
-Imports Microsoft.VisualStudio.TestTools.UnitTesting
-Imports LiteTask.LiteTask
-
-
-Public Class ParameterParserTests
-
-
- Public Sub Parse_SinglePair_ReturnsKeyAndValue()
- Dim result = ParameterParser.Parse("key=value")
- Assert.AreEqual(1, result.Count)
- Assert.AreEqual("value", result("key"))
- End Sub
-
-
- Public Sub Parse_QuotedValueWithSpaces_KeepsValueIntact()
- Dim result = ParameterParser.Parse("key=""value with spaces""")
- Assert.AreEqual(1, result.Count)
- Assert.AreEqual("value with spaces", result("key"))
- End Sub
-
-
- Public Sub Parse_MultiplePairs_AllParsed()
- Dim result = ParameterParser.Parse("a=1 b=2 c=3")
- Assert.AreEqual(3, result.Count)
- Assert.AreEqual("1", result("a"))
- Assert.AreEqual("2", result("b"))
- Assert.AreEqual("3", result("c"))
- End Sub
-
-
- Public Sub Parse_MixedQuotedAndUnquoted_BothHandled()
- Dim result = ParameterParser.Parse("a=1 b=""hello world"" c=3")
- Assert.AreEqual(3, result.Count)
- Assert.AreEqual("1", result("a"))
- Assert.AreEqual("hello world", result("b"))
- Assert.AreEqual("3", result("c"))
- End Sub
-
-
- Public Sub Parse_EmptyQuotedValue_YieldsEmptyString()
- Dim result = ParameterParser.Parse("a=1 b="""" c=3")
- Assert.AreEqual(3, result.Count)
- Assert.AreEqual("", result("b"))
- End Sub
-
-
- Public Sub Parse_EmptyUnquotedValue_YieldsEmptyString()
- Dim result = ParameterParser.Parse("a= b=2")
- Assert.AreEqual(2, result.Count)
- Assert.AreEqual("", result("a"))
- Assert.AreEqual("2", result("b"))
- End Sub
-
-
- Public Sub Parse_NullOrEmpty_ReturnsEmptyDictionary()
- Assert.AreEqual(0, ParameterParser.Parse(Nothing).Count)
- Assert.AreEqual(0, ParameterParser.Parse("").Count)
- Assert.AreEqual(0, ParameterParser.Parse(" ").Count)
- End Sub
-
-
- Public Sub Parse_TokenWithoutEquals_IsSkipped()
- Dim result = ParameterParser.Parse("a=1 garbage b=2")
- Assert.AreEqual(2, result.Count)
- Assert.AreEqual("1", result("a"))
- Assert.AreEqual("2", result("b"))
- Assert.IsFalse(result.ContainsKey("garbage"))
- End Sub
-
-
- Public Sub Parse_QuotedValueFollowedByMore_ContinuesParsing()
- Dim result = ParameterParser.Parse("path=""C:\Program Files\App"" mode=fast")
- Assert.AreEqual(2, result.Count)
- Assert.AreEqual("C:\Program Files\App", result("path"))
- Assert.AreEqual("fast", result("mode"))
- End Sub
-
-
- Public Sub Parse_DuplicateKey_LastWriteWins()
- Dim result = ParameterParser.Parse("a=1 a=2 a=3")
- Assert.AreEqual(1, result.Count)
- Assert.AreEqual("3", result("a"))
- End Sub
-
-
- Public Sub Parse_LeadingTrailingWhitespace_Tolerated()
- Dim result = ParameterParser.Parse(" a=1 b=2 ")
- Assert.AreEqual(2, result.Count)
- Assert.AreEqual("1", result("a"))
- Assert.AreEqual("2", result("b"))
- End Sub
-
-End Class
+' -----------------------------------------------------------------------------
+' Copyright (c) svtica. All rights reserved.
+' File: ParameterParserTests.vb
+' Author: LiteTask contributors
+' Date: 2026-04-25
+' Purpose: Unit tests for LiteTask.ParameterParser covering the historical
+' `key=value` form, cmd-style quoting, and PowerShell-style
+' `-Name Value` / switch syntax.
+' -----------------------------------------------------------------------------
+Imports Microsoft.VisualStudio.TestTools.UnitTesting
+Imports LiteTask.LiteTask
+
+
+Public Class ParameterParserTests
+
+
+ Public Sub Parse_SinglePair_ReturnsKeyAndValue()
+ Dim result = ParameterParser.Parse("key=value")
+ Assert.AreEqual(1, result.Count)
+ Assert.AreEqual("value", result("key"))
+ End Sub
+
+
+ Public Sub Parse_QuotedValueWithSpaces_KeepsValueIntact()
+ Dim result = ParameterParser.Parse("key=""value with spaces""")
+ Assert.AreEqual(1, result.Count)
+ Assert.AreEqual("value with spaces", result("key"))
+ End Sub
+
+
+ Public Sub Parse_MultiplePairs_AllParsed()
+ Dim result = ParameterParser.Parse("a=1 b=2 c=3")
+ Assert.AreEqual(3, result.Count)
+ Assert.AreEqual("1", result("a"))
+ Assert.AreEqual("2", result("b"))
+ Assert.AreEqual("3", result("c"))
+ End Sub
+
+
+ Public Sub Parse_MixedQuotedAndUnquoted_BothHandled()
+ Dim result = ParameterParser.Parse("a=1 b=""hello world"" c=3")
+ Assert.AreEqual(3, result.Count)
+ Assert.AreEqual("1", result("a"))
+ Assert.AreEqual("hello world", result("b"))
+ Assert.AreEqual("3", result("c"))
+ End Sub
+
+
+ Public Sub Parse_EmptyQuotedValue_YieldsEmptyString()
+ Dim result = ParameterParser.Parse("a=1 b="""" c=3")
+ Assert.AreEqual(3, result.Count)
+ Assert.AreEqual("", result("b"))
+ End Sub
+
+
+ Public Sub Parse_EmptyUnquotedValue_YieldsEmptyString()
+ Dim result = ParameterParser.Parse("a= b=2")
+ Assert.AreEqual(2, result.Count)
+ Assert.AreEqual("", result("a"))
+ Assert.AreEqual("2", result("b"))
+ End Sub
+
+
+ Public Sub Parse_NullOrEmpty_ReturnsEmptyDictionary()
+ Assert.AreEqual(0, ParameterParser.Parse(Nothing).Count)
+ Assert.AreEqual(0, ParameterParser.Parse("").Count)
+ Assert.AreEqual(0, ParameterParser.Parse(" ").Count)
+ End Sub
+
+
+ Public Sub Parse_TokenWithoutEquals_IsSkipped()
+ Dim result = ParameterParser.Parse("a=1 garbage b=2")
+ Assert.AreEqual(2, result.Count)
+ Assert.AreEqual("1", result("a"))
+ Assert.AreEqual("2", result("b"))
+ Assert.IsFalse(result.ContainsKey("garbage"))
+ End Sub
+
+
+ Public Sub Parse_QuotedValueFollowedByMore_ContinuesParsing()
+ Dim result = ParameterParser.Parse("path=""C:\Program Files\App"" mode=fast")
+ Assert.AreEqual(2, result.Count)
+ Assert.AreEqual("C:\Program Files\App", result("path"))
+ Assert.AreEqual("fast", result("mode"))
+ End Sub
+
+
+ Public Sub Parse_DuplicateKey_LastWriteWins()
+ Dim result = ParameterParser.Parse("a=1 a=2 a=3")
+ Assert.AreEqual(1, result.Count)
+ Assert.AreEqual("3", result("a"))
+ End Sub
+
+
+ Public Sub Parse_LeadingTrailingWhitespace_Tolerated()
+ Dim result = ParameterParser.Parse(" a=1 b=2 ")
+ Assert.AreEqual(2, result.Count)
+ Assert.AreEqual("1", result("a"))
+ Assert.AreEqual("2", result("b"))
+ End Sub
+
+
+ Public Sub Parse_PowerShellStyle_NameValue_Pairs()
+ Dim result = ParameterParser.Parse("-WindowHours 96 -EmailTo ""user@example.com""")
+ Assert.AreEqual(2, result.Count)
+ Assert.AreEqual("96", result("WindowHours"))
+ Assert.AreEqual("user@example.com", result("EmailTo"))
+ End Sub
+
+
+ Public Sub Parse_PowerShellStyle_SwitchBetweenParams_StoredAsNothing()
+ Dim result = ParameterParser.Parse("-Force -WindowHours 96")
+ Assert.AreEqual(2, result.Count)
+ Assert.IsTrue(result.ContainsKey("Force"))
+ Assert.IsNull(result("Force"))
+ Assert.AreEqual("96", result("WindowHours"))
+ End Sub
+
+
+ Public Sub Parse_PowerShellStyle_TrailingSwitch_StoredAsNothing()
+ Dim result = ParameterParser.Parse("-WindowHours 96 -Verbose")
+ Assert.AreEqual(2, result.Count)
+ Assert.AreEqual("96", result("WindowHours"))
+ Assert.IsNull(result("Verbose"))
+ End Sub
+
+
+ Public Sub Parse_PowerShellStyle_NameEqualsValue_Accepted()
+ Dim result = ParameterParser.Parse("-Mode=fast -Count=3")
+ Assert.AreEqual(2, result.Count)
+ Assert.AreEqual("fast", result("Mode"))
+ Assert.AreEqual("3", result("Count"))
+ End Sub
+
+
+ Public Sub Parse_MixedStyles_BothParsed()
+ Dim result = ParameterParser.Parse("alpha=one -Force -Beta ""two three""")
+ Assert.AreEqual(3, result.Count)
+ Assert.AreEqual("one", result("alpha"))
+ Assert.IsNull(result("Force"))
+ Assert.AreEqual("two three", result("Beta"))
+ End Sub
+
+
+ Public Sub Parse_KeyLookup_IsCaseInsensitive()
+ Dim result = ParameterParser.Parse("-WindowHours 96")
+ Assert.AreEqual("96", result("windowhours"))
+ Assert.AreEqual("96", result("WINDOWHOURS"))
+ End Sub
+
+End Class