Reputation: 333
I have a Powershell script that (as one of its options) reads a user-defined pre-execution command from a file and needs to execute it. The user-defined pre-execution command is expected to be an ordinary DOS-style command. I can split the command by spaces and then feed it to the PowerShell "&" to get it executed:
$preExecutionCommand = "dir D:\Test"
$preExecutionArgs = $preExecutionCommand -split '\s+'
$preExecutionCmd = $preExecutionArgs[0]
$preExecutionNumArgs = $preExecutionArgs.Length - 1
if ($preExecutionNumArgs -gt 0) {
$preExecutionArgs = $preExecutionArgs[1..$preExecutionNumArgs]
& $preExecutionCmd $preExecutionArgs
} else {
& $preExecutionCmd
}
But if the user-defined command string has spaces that need to go in the arguments, or the path to the command has spaces, then I need to be much smarter at parsing the user-defined string.
To the naked eye it is obvious that the following string has a command at the front followed by 2 parameters:
"C:\Program Files\Tool\program1" 25 "the quick brown fox"
Has anyone already got a function that will parse strings like this and give back an array or list of the DOS-style command and each of the parameters?
Upvotes: 2
Views: 2795
Reputation: 450
function ParseCommandLine($commandLine)
{
return Invoke-Expression ".{`$args} $commandLine"
}
Upvotes: 1
Reputation: 1082
There is a very simple solution for that. You can misuse the Powershell paramater parsing mechanism for it:
> $paramString = '1 blah "bluh" "ding dong" """foo"""'
> $paramArray = iex "echo $paramString"
> $paramArray
1
blah
bluh
ding dong
"foo"
Upvotes: 5
Reputation: 575
I have put together the following that seems to do what you require.
$parsercode = @"
using System;
using System.Linq;
using System.Collections.Generic;
public static class CommandLineParser
{
public static List<String> Parse(string commandLine)
{
var result = commandLine.Split('"')
.Select((element, index) => index % 2 == 0 // If even index
? element.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) // Split the item
: new string[] { String.Format("\"{0}\"", element) }) // Keep the entire item
.SelectMany(element => element).ToList();
return result;
}
}
"@
Add-Type -TypeDefinition $parsercode -Language CSharp
$commands = Get-Content .\commands.txt
$commands | % {
$tokens = [CommandLineParser]::Parse($_)
$command = $tokens[0]
$arguments = $tokens[1..($tokens.Count-1)]
echo ("Command:{0}, ArgCount:{1}, Arguments:{2}" -f $command, $arguments.Count, ([string]::Join(" ", $arguments)))
Start-Process -FilePath ($command) -ArgumentList $arguments
}
I have used some c# code posted by @Cédric Bignon in 2013 which shows a very nice C# Linq solution to your parser problem to create a parser method in [CommandLineParser]::Parse. That is then used to parse the command and Arguments to send to Start-Process.
Try it out and see if it does what you want.
Upvotes: 1
Reputation: 333
In the end I am using CommandLineToArgvW() to parse the command line. With this I can pass double quotes literally into parameters when needed, as well as have spaces in double-quoted parameters. e.g.:
dir "abc def" 23 """z"""
becomes a directory command with 3 parameters:
abc def
23
"z"
The code is:
function Split-CommandLine
{
<#
.Synopsis
Parse command-line arguments using Win32 API CommandLineToArgvW function.
.Link
https://github.com/beatcracker/Powershell-Misc/blob/master/Split-CommandLine.ps1
http://edgylogic.com/blog/powershell-and-external-commands-done-right/
.Description
This is the Cmdlet version of the code from the article http://edgylogic.com/blog/powershell-and-external-commands-done-right.
It can parse command-line arguments using Win32 API function CommandLineToArgvW .
.Parameter CommandLine
A string representing the command-line to parse. If not specified, the command-line of the current PowerShell host is used.
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$CommandLine
)
Begin
{
$Kernel32Definition = @'
[DllImport("kernel32")]
public static extern IntPtr LocalFree(IntPtr hMem);
'@
$Kernel32 = Add-Type -MemberDefinition $Kernel32Definition -Name 'Kernel32' -Namespace 'Win32' -PassThru
$Shell32Definition = @'
[DllImport("shell32.dll", SetLastError = true)]
public static extern IntPtr CommandLineToArgvW(
[MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine,
out int pNumArgs);
'@
$Shell32 = Add-Type -MemberDefinition $Shell32Definition -Name 'Shell32' -Namespace 'Win32' -PassThru
}
Process
{
$ParsedArgCount = 0
$ParsedArgsPtr = $Shell32::CommandLineToArgvW($CommandLine, [ref]$ParsedArgCount)
Try
{
$ParsedArgs = @();
0..$ParsedArgCount | ForEach-Object {
$ParsedArgs += [System.Runtime.InteropServices.Marshal]::PtrToStringUni(
[System.Runtime.InteropServices.Marshal]::ReadIntPtr($ParsedArgsPtr, $_ * [IntPtr]::Size)
)
}
}
Finally
{
$Kernel32::LocalFree($ParsedArgsPtr) | Out-Null
}
$ret = @()
# -lt to skip the last item, which is a NULL ptr
for ($i = 0; $i -lt $ParsedArgCount; $i += 1) {
$ret += $ParsedArgs[$i]
}
return $ret
}
}
$executionCommand = Get-Content .\commands.txt
$executionArgs = Split-CommandLine $executionCommand
$executionCmd = $executionArgs[0]
$executionNumArgs = $executionArgs.Length - 1
if ($executionNumArgs -gt 0) {
$executionArgs = $executionArgs[1..$executionNumArgs]
echo $executionCmd $executionArgs
& $executionCmd $executionArgs
} else {
echo $executionCmd
& $executionCmd
}
Upvotes: 1