windows argv to string module utility (#28970)

* windows argv to string module utility

* rebased PR with latest devel branch
This commit is contained in:
Jordan Borean 2017-10-23 09:49:40 +10:00 committed by GitHub
parent ed342e8ce3
commit b663f602bc
4 changed files with 191 additions and 1 deletions

View file

@ -0,0 +1,75 @@
# Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# The rules used in these functions are derived from the below
# https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments
# https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
Function Escape-Argument($argument, $force_quote=$false) {
# this converts a single argument to an escaped version, use Join-Arguments
# instead of this function as this only escapes a single string.
# check if argument contains a space, \n, \t, \v or "
if ($force_quote -eq $false -and $argument.Length -gt 0 -and $argument -notmatch "[ \n\t\v`"]") {
# argument does not need escaping (and we don't want to force it),
# return as is
return $argument
} else {
# we need to quote the arg so start with "
$new_argument = '"'
for ($i = 0; $i -lt $argument.Length; $i++) {
$num_backslashes = 0
# get the number of \ from current char until end or not a \
while ($i -ne ($argument.Length - 1) -and $argument[$i] -eq "\") {
$num_backslashes++
$i++
}
$current_char = $argument[$i]
if ($i -eq ($argument.Length -1) -and $current_char -eq "\") {
# We are at the end of the string so we need to add the same \
# * 2 as the end char would be a "
$new_argument += ("\" * ($num_backslashes + 1) * 2)
} elseif ($current_char -eq '"') {
# we have a inline ", we need to add the existing \ but * by 2
# plus another 1
$new_argument += ("\" * (($num_backslashes * 2) + 1))
$new_argument += $current_char
} else {
# normal character so no need to escape the \ we have counted
$new_argument += ("\" * $num_backslashes)
$new_argument += $current_char
}
}
# we need to close the special arg with a "
$new_argument += '"'
return $new_argument
}
}
Function Argv-ToString($arguments, $force_quote=$false) {
# Takes in a list of un escaped arguments and convert it to a single string
# that can be used when starting a new process. It will escape the
# characters as necessary in the list.
# While there is a CommandLineToArgvW function there is a no
# ArgvToCommandLineW that we can call to convert a list to an escaped
# string.
# You can also pass in force_quote so that each argument is quoted even
# when not necessary, by default only arguments with certain characters are
# quoted.
# TODO: add in another switch which will escape the args for cmd.exe
$escaped_arguments = @()
foreach ($argument in $arguments) {
$escaped_argument = Escape-Argument -argument $argument -force_quote $force_quote
$escaped_arguments += $escaped_argument
}
return ($escaped_arguments -join ' ')
}
# this line must stay at the bottom to ensure all defined module parts are exported
Export-ModuleMember -Alias * -Function * -Cmdlet *

View file

@ -0,0 +1,13 @@
using System;
// This has been compiled to an exe and uploaded to S3 bucket for argv test
namespace PrintArgv
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(string.Join(System.Environment.NewLine, args));
}
}
}

View file

@ -0,0 +1,93 @@
#!powershell
#Requires -Module Ansible.ModuleUtils.Legacy
#Requires -Module Ansible.ModuleUtils.ArgvParser
$ErrorActionPreference = 'Continue'
$params = Parse-Args $args
$exe = Get-AnsibleParam -obj $params -name "exe" -type "path" -failifempty $true
Add-Type -TypeDefinition @'
using System.IO;
using System.Threading;
namespace Ansible.Command
{
public static class NativeUtil
{
public static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)
{
var sowait = new EventWaitHandle(false, EventResetMode.ManualReset);
var sewait = new EventWaitHandle(false, EventResetMode.ManualReset);
string so = null, se = null;
ThreadPool.QueueUserWorkItem((s)=>
{
so = stdoutStream.ReadToEnd();
sowait.Set();
});
ThreadPool.QueueUserWorkItem((s) =>
{
se = stderrStream.ReadToEnd();
sewait.Set();
});
foreach(var wh in new WaitHandle[] { sowait, sewait })
wh.WaitOne();
stdout = so;
stderr = se;
}
}
}
'@
Function Run-Process($executable, $arguments) {
$proc = New-Object System.Diagnostics.Process
$psi = $proc.StartInfo
$psi.FileName = $executable
$psi.Arguments = $arguments
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.UseShellExecute = $false
$proc.Start() | Out-Null # will always return $true for non shell-exec cases
$stdout = $stderr = [string] $null
[Ansible.Command.NativeUtil]::GetProcessOutput($proc.StandardOutput, $proc.StandardError, [ref] $stdout, [ref] $stderr) | Out-Null
$proc.WaitForExit() | Out-Null
$actual_args = $stdout.Substring(0, $stdout.Length - 2) -split "`r`n"
return $actual_args
}
$tests = @(
@('abc', 'd', 'e'),
@('a\\b', 'de fg', 'h'),
@('a\"b', 'c', 'd'),
@('a\\b c', 'd', 'e'),
@('C:\Program Files\file\', 'arg with " quote'),
@('ADDLOCAL="a,b,c"', '/s', 'C:\\Double\\Backslash')
)
foreach ($expected in $tests) {
$joined_string = Argv-ToString -arguments $expected
# We can't used CommandLineToArgvW to test this out as it seems to mangle
# \, might be something to do with unicode but not sure...
$actual = Run-Process -executable $exe -arguments $joined_string
if ($expected.Count -ne $actual.Count) {
$result.actual = $actual -join "`n"
$result.expected = $expected -join "`n"
Fail-Json -obj $result -message "Actual arg count: $($actual.Count) != Expected arg count: $($expected.Count)"
}
for ($i = 0; $i -lt $expected.Count; $i++) {
$expected_arg = $expected[$i]
$actual_arg = $actual[$i]
if ($expected_arg -cne $actual_arg) {
$result.actual = $actual -join "`n"
$result.expected = $expected -join "`n"
Fail-Json -obj $result -message "Actual arg: '$actual_arg' != Expected arg: '$expected_arg'"
}
}
}
Exit-Json @{ data = 'success' }

View file

@ -48,7 +48,7 @@
that:
- sid_test.data == 'success'
- name: create testing folder for argv binary
- name: create temp testing folder
win_file:
path: C:\ansible testing
state: directory
@ -67,6 +67,15 @@
that:
- command_util.data == 'success'
- name: call module with ArgvParser tests
argv_parser_test:
exe: C:\ansible testing\PrintArgv.exe
register: argv_test
- assert:
that:
- argv_test.data == 'success'
- name: remove testing folder
win_file:
path: C:\ansible testing