Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -93,35 +93,39 @@ internal void BindParameters(Collection<CommandParameterInternal> parameters)

if (parameter.ArgumentSpecified)
{
// If this is the verbatim argument marker, we don't pass it on to the native command.
// We do need to remember it though - we'll expand environment variables in subsequent args.
object argValue = parameter.ArgumentValue;
if (string.Equals("--%", argValue as string, StringComparison.OrdinalIgnoreCase))
{
sawVerbatimArgumentMarker = true;
continue;
}

if (argValue != AutomationNull.Value && argValue != UnboundParameter.Value)
{
// ArrayLiteralAst is used to reconstruct the correct argument, e.g.
// windbg -k com:port=\\devbox\pipe\debug,pipe,resets=0,reconnect
// The parser produced an array of strings but marked the parameter so we
// can properly reconstruct the correct command line.
bool usedQuotes = false;
ArrayLiteralAst arrayLiteralAst = null;
switch (parameter?.ArgumentAst)
bool usedQuotes;
if (argValue is PSObject { BaseObject: string baseString } psObject && psObject.Properties[ParserOps.StringConstantType]?.Value is StringConstantType stp)
{
case StringConstantExpressionAst sce:
usedQuotes = sce.StringConstantType != StringConstantType.BareWord;
break;
case ExpandableStringExpressionAst ese:
usedQuotes = ese.StringConstantType != StringConstantType.BareWord;
break;
case ArrayLiteralAst ala:
arrayLiteralAst = ala;
break;
usedQuotes = stp != StringConstantType.BareWord;
argValue = baseString;
}
else
{
usedQuotes = parameter.ArgumentAst switch
{
StringConstantExpressionAst sce => sce.StringConstantType != StringConstantType.BareWord,
ExpandableStringExpressionAst ese => ese.StringConstantType != StringConstantType.BareWord,
_ => false,
};
}

// If this is the verbatim argument marker, we don't pass it on to the native command.
// We do need to remember it though - we'll expand environment variables in subsequent args.
if (string.Equals("--%", argValue as string, StringComparison.OrdinalIgnoreCase))
{
sawVerbatimArgumentMarker = true;
continue;
}

// ArrayLiteralAst is used to reconstruct the correct argument, e.g.
// windbg -k com:port=\\devbox\pipe\debug,pipe,resets=0,reconnect
ArrayLiteralAst arrayLiteralAst = parameter.ArgumentAst as ArrayLiteralAst;

AppendOneNativeArgument(Context, parameter, argValue, arrayLiteralAst, sawVerbatimArgumentMarker, usedQuotes);
}
Expand Down
9 changes: 8 additions & 1 deletion src/System.Management.Automation/engine/lang/parserutils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using System.Management.Automation.Language;
using System.Management.Automation.Runspaces;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;

using Dbg = System.Management.Automation.Diagnostics;
Expand Down Expand Up @@ -232,6 +231,7 @@ public enum SplitOptions
internal static class ParserOps
{
internal const string MethodNotFoundErrorId = "MethodNotFound";
internal const string StringConstantType = "StringConstantType";

/// <summary>
/// Construct the various caching structures used by the runtime routines...
Expand Down Expand Up @@ -291,6 +291,13 @@ internal static PSObject WrappedNumber(object data, string text)
return wrapped;
}

internal static PSObject WrappedString(string text, StringConstantType stringConstantType)
{
var wrapped = new PSObject(text);
wrapped.Properties.Add(new PSNoteProperty(StringConstantType, stringConstantType), true);
return wrapped;
}

/// <summary>
/// A helper routine that turns the argument object into an
/// integer. It handles PSObject and conversions.
Expand Down
12 changes: 12 additions & 0 deletions src/System.Management.Automation/engine/parser/Compiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,9 @@ internal static class CachedReflectionInfo
internal static readonly MethodInfo ParserOps_UnarySplitOperator =
typeof(ParserOps).GetMethod(nameof(ParserOps.UnarySplitOperator), StaticFlags);

internal static readonly MethodInfo ParserOps_WrappedString =
typeof(ParserOps).GetMethod(nameof(ParserOps.WrappedString), StaticFlags);

internal static readonly ConstructorInfo Pipe_ctor =
typeof(Pipe).GetConstructor(InstanceFlags, null, CallingConventions.Standard, new Type[] { typeof(List<object>) }, null);

Expand Down Expand Up @@ -4248,6 +4251,10 @@ private Expression GetCommandArgumentExpression(CommandElementAst element)
return Expression.Constant(ParserOps.WrappedNumber(constElement.Value, commandArgumentText));
}
}
else if (constElement is StringConstantExpressionAst stringConstant)
{
return Expression.Constant(ParserOps.WrappedString(stringConstant.Value, stringConstant.StringConstantType));
}

var result = Compile(element);
if (result.Type == typeof(object[]))
Expand All @@ -4258,6 +4265,11 @@ private Expression GetCommandArgumentExpression(CommandElementAst element)
{
result = Expression.Call(CachedReflectionInfo.PipelineOps_CheckAutomationNullInCommandArgument, result);
}
else if (element is ExpandableStringExpressionAst expandableString)
{
Debug.Assert(result.Type == typeof(string));
result = Expression.Call(CachedReflectionInfo.ParserOps_WrappedString, result, Expression.Constant(expandableString.StringConstantType));
}

return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Management.Automation.Language;
Expand Down Expand Up @@ -811,6 +812,10 @@ private void ConvertCommand(CommandAst commandAst, bool isTrustedInput)
argument = ParserOps.WrappedNumber(argument, commandArgumentText);
}
}
else if (constantExprAst is StringConstantExpressionAst stringConstant)
{
argument = ParserOps.WrappedString(stringConstant.Value, stringConstant.StringConstantType);
}
else
{
if (!isTrustedInput)
Expand All @@ -832,6 +837,12 @@ private void ConvertCommand(CommandAst commandAst, bool isTrustedInput)
{
argument = GetExpressionValue(exprAst, isTrustedInput);
}

if (ast is ExpandableStringExpressionAst expandableStringExpressionAst)
{
Debug.Assert(argument is string);
argument = ParserOps.WrappedString(argument as string, expandableStringExpressionAst.StringConstantType);
}
}

_powershell.AddArgument(argument);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Describe 'Native UNIX globbing tests' -tags "CI" {

$defaultParamValues = $PSDefaultParameterValues.Clone()
$PSDefaultParameterValues["it:skip"] = $IsWindows
$HomeDir = $ExecutionContext.SessionState.Provider.Get("FileSystem").Home
$Tilde = "~"
}

AfterAll {
Expand Down Expand Up @@ -74,39 +76,82 @@ Describe 'Native UNIX globbing tests' -tags "CI" {
/bin/echo $arg | Should -BeExactly $arg
}
$quoteTests = @(
@{arg = '"*"'},
@{arg = "'*'"}
@{arg = '"*"'; expectedArg = "*"}
@{arg = "'*'"; expectedArg = "*"}
@{arg = '"$TESTDRIVE/*"'; expectedArg = "$TESTDRIVE/*"}
)
It 'Should not expand quoted strings: <arg>' -TestCases $quoteTests {
param($arg)
Invoke-Expression "/bin/echo $arg" | Should -BeExactly '*'
param($arg, $expectedArg)
Invoke-Expression "testexe -echoargs $arg" | Should -BeExactly "Arg 0 is <$expectedArg>"
}
# Splat tests are skipped because they should work, but don't.
# Supporting this scenario would require adding a NoteProperty
# to each quoted string argument - maybe not worth it, and maybe
# an argument for another way to suppress globbing.
It 'Should not expand quoted strings via splat array: <arg>' -TestCases $quoteTests -Skip {
param($arg)
It 'Should not expand quoted strings via splat array: <arg>' -TestCases $quoteTests {
param($arg, $expectedArg)

function Invoke-Echo
function Invoke-TestExe
{
/bin/echo @args
testexe @args
}
Invoke-Expression "Invoke-Echo $arg" | Should -BeExactly '*'
Invoke-Expression "Invoke-TestExe -echoargs $arg" | Should -BeExactly "Arg 0 is <$expectedArg>"
}
It 'Should not expand quoted strings via splat hash: <arg>' -TestCases $quoteTests -Skip {
param($arg)
It 'Should not expand quoted strings via splat hash: <arg>' -TestCases $quoteTests {
param($arg, $expectedArg)

function Invoke-Echo($quotedArg)
{
/bin/echo @PSBoundParameters
testexe -echoargs @PSBoundParameters
}
Invoke-Expression "Invoke-Echo -quotedArg:$arg" | Should -BeExactly "-quotedArg:*"
Invoke-Expression "Invoke-Echo -quotedArg:$arg" | Should -BeExactly "Arg 0 is <-quotedArg:$expectedArg>"

# When specifing a space after the parameter, the space is removed when splatting.
# This behavior is debatable, but it's worth adding this test anyway to detect
# a change in behavior.
Invoke-Expression "Invoke-Echo -quotedArg: $arg" | Should -BeExactly "-quotedArg:*"
Invoke-Expression "Invoke-Echo -quotedArg: $arg" | Should -BeExactly "Arg 0 is <-quotedArg:$expectedArg>"
}
It 'Should expand strings via splat array' {
function Invoke-TestExe
{
$args.Length | Should -Be 2
testexe @args
}
Invoke-TestExe -echoargs $TESTDRIVE/*.txt | Should -BeExactly @(
"Arg 0 is <$TESTDRIVE/abc.txt>"
"Arg 1 is <$TESTDRIVE/bbb.txt>"
"Arg 2 is <$TESTDRIVE/cbb.txt>"
)
}
It 'Should keep its literal meaning when splatted' {
function Invoke-TestExe
{
testexe @args
}
Invoke-TestExe -echoargs $TESTDRIVE/*.txt "$TESTDRIVE/*.txt" '$TESTDRIVE/*.txt' | Should -BeExactly @(
"Arg 0 is <$TESTDRIVE/abc.txt>"
"Arg 1 is <$TESTDRIVE/bbb.txt>"
"Arg 2 is <$TESTDRIVE/cbb.txt>"
"Arg 3 is <$TESTDRIVE/*.txt>"
'Arg 4 is <$TESTDRIVE/*.txt>'
)
}
It 'Should not expand quoted strings via splat hash' {
function Invoke-EchoArgs($quotedArg)
{
$PSBoundParameters.Length | Should -Be 1
testexe -echoargs @PSBoundParameters
}
Invoke-EchoArgs -quotedArg:$TESTDRIVE/*.txt | Should -BeExactly @(
"Arg 0 is <-quotedArg:$TESTDRIVE/abc.txt>"
"Arg 1 is <-quotedArg:$TESTDRIVE/bbb.txt>"
"Arg 2 is <-quotedArg:$TESTDRIVE/cbb.txt>"
)

# When specifing a space after the parameter, the space is removed when splatting.
# This behavior is debatable, but it's worth adding this test anyway to detect
# a change in behavior.
Invoke-EchoArgs -quotedArg: $TESTDRIVE/*.txt | Should -BeExactly @(
"Arg 0 is <-quotedArg:$TESTDRIVE/abc.txt>"
"Arg 1 is <-quotedArg:$TESTDRIVE/bbb.txt>"
"Arg 2 is <-quotedArg:$TESTDRIVE/cbb.txt>"
)
}
# Test the behavior in non-filesystem drives
It 'Should not expand patterns on non-filesystem drives' {
Expand All @@ -119,17 +164,45 @@ Describe 'Native UNIX globbing tests' -tags "CI" {
(/bin/ls $TESTDRIVE/foo*.txt).Length | Should -Be 2
}
# Test ~ expansion
It 'Tilde should be replaced by the filesystem provider home directory' {
/bin/echo ~ | Should -BeExactly ($ExecutionContext.SessionState.Provider.Get("FileSystem").Home)
}
# Test ~ expansion with a path fragment (e.g. ~/foo)
It '~/foo should be replaced by the <filesystem provider home directory>/foo' {
/bin/echo ~/foo | Should -BeExactly "$($ExecutionContext.SessionState.Provider.Get("FileSystem").Home)/foo"
}
It '~ should not be replaced when quoted' {
/bin/echo '~' | Should -BeExactly '~'
/bin/echo "~" | Should -BeExactly '~'
/bin/echo '~/foo' | Should -BeExactly '~/foo'
/bin/echo "~/foo" | Should -BeExactly '~/foo'
}
It '~ should be replaced by the filesystem provider home directory <arg>' -testCases @(
@{arg = '~'; Expected = $HomeDir }
@{arg = '$Tilde'; Expected = $HomeDir }
@{arg = '~/foo'; Expected = "$HomeDir/foo" }
@{arg = '$Tilde/foo'; Expected = "$HomeDir/foo" }
) {
param($arg, $Expected)
Invoke-Expression "testexe -echoargs $arg" | Should -BeExactly "Arg 0 is <$Expected>"
}
It '~ should not be replaced when quoted <arg>' -testCases @(
@{arg = "'~'"; Expected = "~" }
@{arg = "'~/foo'"; Expected = "~/foo" }
@{arg = '"~"'; Expected = "~" }
@{arg = '"~/foo"'; Expected = "~/foo" }
@{arg = '"$Tilde"'; Expected = "~" }
@{arg = '"$Tilde/foo"'; Expected = "~/foo" }
) {
param($arg, $Expected)
Invoke-Expression "testexe -echoargs $arg" | Should -BeExactly "Arg 0 is <$Expected>"
}
It '~ should keep its literal meaning when splatted <splattingArgs>'-testCases @(
@{
splattingArgs = @'
~ ~/foo '~' "~" '~/foo' "~/foo"
'@;
Expected = @("$HomeDir", "$HomeDir/foo", "~", "~", "~/foo", "~/foo")
}
@{
splattingArgs = @'
$Tilde $Tilde/foo "$Tilde" "$Tilde/foo"
'@;
Expected = @("$HomeDir", "$HomeDir/foo", "~", "~/foo")
}
) {
param($splattingArgs, $Expected)
function Invoke-TestExe {
testexe @args
}

Invoke-Expression "Invoke-TestExe -echoargs $splattingArgs" | Should -BeExactly @($Expected | ForEach-Object { $i = 0 } { "Arg {0} is <$_>" -f $i++ } )
}
}
Loading
Loading