8000 Glob native command args only when not quoted · lzybkr/PowerShell@2bcea1d · GitHub
[go: up one dir, main page]

Skip to content

Commit 2bcea1d

Browse files
committed
Glob native command args only when not quoted
Also fix some minor issues with exceptions being raised when resolving the path - falling back to no glob. Fix: PowerShell#3931 PowerShell#4971
1 parent 7aa52e0 commit 2bcea1d

File tree

2 files changed

+181
-95
lines changed

2 files changed

+181
-95
lines changed

src/System.Management.Automation/engine/NativeCommandParameterBinder.cs

Lines changed: 111 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22
Copyright (c) Microsoft Corporation. All rights reserved.
33
--********************************************************************/
44

5+
using Microsoft.PowerShell.Commands;
56
using System.Collections;
67
using System.Collections.ObjectModel;
8+
using System.IO;
9+
using System.Linq;
710
using System.Management.Automation.Internal;
811
using System.Text;
912

1013
namespace System.Management.Automation
1114
{
15+
using Language;
16+
1217
/// <summary>
1318
/// The parameter binder for native commands.
1419
/// </summary>
@@ -79,7 +84,7 @@ internal void BindParameters(Collection<CommandParameterInternal> parameters)
7984
if (parameter.ParameterNameSpecified)
8085
{
8186
Diagnostics.Assert(parameter.ParameterText.IndexOf(' ') == -1, "Parameters cannot have whitespace");
82-
_arguments.Append(parameter.ParameterText);
87+
PossiblyGlobArg(parameter.ParameterText, usedQuotes: false);
8388

8489
if (parameter.SpaceAfterParameter)
8590
{
@@ -105,9 +110,23 @@ internal void BindParameters(Collection<CommandParameterInternal> parameters)
105110
// windbg -k com:port=\\devbox\pipe\debug,pipe,resets=0,reconnect
106111
// The parser produced an array of strings but marked the parameter so we
107112
// can properly reconstruct the correct command line.
113+
bool usedQuotes = false;
114+
var pAst = parameter.ArgumentAst;
115+
if (pAst != null)
116+
{
117+
if (pAst is StringConstantExpressionAst sce)
118+
{
119+
usedQuotes = sce.StringConstantType != StringConstantType.BareWord;
120+
}
121+
else if (pAst is ExpandableStringExpressionAst ese)
122+
{
123+
usedQuotes = ese.StringConstantType != StringConstantType.BareWord;
124+
}
125+
}
108126
appendOneNativeArgument(Context, argValue,
109127
parameter.ArrayIsSingleArgumentForNativeCommand ? ',' : ' ',
110-
sawVerbatimArgumentMarker);
128+
sawVerbatimArgumentMarker,
129+
usedQuotes);
111130
}
112131
}
113132
}
@@ -141,7 +160,8 @@ internal String Arguments
141160
/// <param name="obj">The object to append</param>
142161
/// <param name="separator">A space or comma used when obj is enumerable</param>
143162
/// <param name="sawVerbatimArgumentMarker">true if the argument occurs after --%</param>
144-
private void appendOneNativeArgument(ExecutionContext context, object obj, char separator, bool sawVerbatimArgumentMarker)
163+
/// <param name="usedQuotes">True if the argument was a quoted string (single or double)</param>
164+
private void appendOneNativeArgument(ExecutionContext context, object obj, char separator, bool sawVerbatimArgumentMarker, bool usedQuotes)
145165
{
146166
IEnumerator list = LanguagePrimitives.GetEnumerator(obj);
147167
bool needSeparator = false;
@@ -207,105 +227,103 @@ private void appendOneNativeArgument(ExecutionContext context, object obj, char
207227
}
208228
else
209229
{
230+
PossiblyGlobArg(arg, usedQuotes);
231+
}
232+
}
233+
}
234+
} while (list != null);
235+
}
236+
237+
/// <summary>
238+
/// On Windows, just append <paramref name="arg"/>.
239+
/// On Unix, do globbing as appropriate, otherwise just append <paramref name="arg"/>.
240+
/// </summary>
241+
/// <param name="arg">The argument that possibly needs expansion</param>
242+
/// <param name="usedQuotes">True if the argument was a quoted string (single or double)</param>
243+
private void PossiblyGlobArg(string arg, bool usedQuotes)
244+
{
245+
var argExpanded = false;
246+
210247
#if UNIX
211-
// On UNIX systems, we expand arguments containing wildcard expressions against
212-
// the file system just like bash, etc.
213-
if (System.Management.Automation.WildcardPattern.ContainsWildcardCharacters(arg))
214-
{
215-
// See if the current working directory is a filesystem provider location
216-
// We won't do the expansion if it isn't since native commands can only access the file system.
217-
var cwdinfo = Context.EngineSessionState.CurrentLocation;
248+
// On UNIX systems, we expand arguments containing wildcard expressions against
249+
// the file system just like bash, etc.
250+
if (!usedQuotes && WildcardPattern.ContainsWildcardCharacters(arg))
251+
{
252+
// See if the current working directory is a filesystem provider location
253+
// We won't do the expansion if it isn't since native commands can only access the file system.
254+
var cwdinfo = Context.EngineSessionState.CurrentLocation;
255+
256+
// If it's a filesystem location then expand the wildcards
257+
if (cwdinfo.Provider.Name.Equals(FileSystemProvider.ProviderName, StringComparison.OrdinalIgnoreCase))
258+
{
259+
// On UNIX, paths starting with ~ are not normalized
260+
bool normalizePath = arg.Length == 0 || arg[0] != '~';
218261

219-
// If it's a filesystem location then expand the wildcards
220-
if (string.Equals(cwdinfo.Provider.Name, Microsoft.PowerShell.Commands.FileSystemProvider.ProviderName,
221-
StringComparison.OrdinalIgnoreCase))
222-
{
223-
bool normalizePath = true;
224-
// On UNIX, paths starting with ~ are not normalized
225-
if (arg.Length > 0 && arg[0] == '~')
226-
{
227-
normalizePath = false;
228-
}
262+
// See if there are any matching paths otherwise just add the pattern as the argument
263+
Collection<PSObject> paths = null;
264+
try
265+
{
266+
paths = Context.EngineSessionState.InvokeProvider.ChildItem.Get(arg, false);
267+
}
268+
catch
269+
{
270+
// Fallthrough will append the pattern unchanged.
271+
}
229272

230-
// See if there are any matching paths otherwise just add the pattern as the argument
231-
var paths = Context.EngineSessionState.InvokeProvider.ChildItem.Get(arg, false);
232-
if (paths.Count > 0)
233-
{
234-
bool first = true;
235-
foreach (var path in paths)
236-
{
237-
object pbo = path.BaseObject;
238-
if (! first)
239-
{
240-
_arguments.Append(" ");
241-
}
242-
else
243-
{
244-
if (! (pbo is System.IO.FileSystemInfo))
245-
{
246-
// If the object is not a filesystem object, then just append
247-
// the pattern unchanged
248-
_arguments.Append(arg);
249-
break;
250-
}
251-
first = false;
252-
}
253-
var expandedPath = (pbo as System.IO.FileSystemInfo).FullName;
254-
if (normalizePath)
255-
{
256-
expandedPath = Context.SessionState.Path.NormalizeRelativePath(expandedPath, cwdinfo.ProviderPath);
257-
}
258-
// If the path contains spaces, then add quotes around it.
259-
if (NeedQuotes(expandedPath))
260-
{
261-
_arguments.Append("\"");
262-
_arguments.Append(expandedPath);
263-
_arguments.Append("\"");
264-
}
265-
else
266-
{
267-
_arguments.Append(expandedPath);
268-
}
269-
}
270-
}
271-
else
272-
{
273-
_arguments.Append(arg);
274-
}
275-
}
276-
else
277-
{
278-
_arguments.Append(arg);
279-
}
273+
// Expand paths, but only from the file system.
274+
if (paths?.Count > 0 && paths.All(p => p.BaseObject is FileSystemInfo))
275+
{
276+
var sep = "";
277+
foreach (var path in paths)
278+
{
279+
_arguments.Append(sep);
280+
sep = " ";
281+
var expandedPath = (path.BaseObject as FileSystemInfo).FullName;
282+
if (normalizePath)
283+
{
284+
expandedPath =
285+
Context.SessionState.Path.NormalizeRelativePath(expandedPath, cwdinfo.ProviderPath);
286+
}
287+
// If the path contains spaces, then add quotes around it.
288+
if (NeedQuotes(expandedPath))
289+
{
290+
_arguments.Append("\"");
291+
_arguments.Append(expandedPath);
292+
_arguments.Append("\"");
280293
}
281294
else
282295
{
283-
// Even if there are no wildcards, we still need to possibly
284-
// expand ~ into the filesystem provider home directory path
285-
ProviderInfo fileSystemProvider = Context.EngineSessionState.GetSingleProvider(
286-
Microsoft.PowerShell.Commands.FileSystemProvider.ProviderName);
287-
string home = fileSystemProvider.Home;
288-
if (string.Equals(arg, "~"))
289-
{
290-
_arguments.Append(home);
291-
}
292-
else if (arg.StartsWith("~/", StringComparison.OrdinalIgnoreCase))
293-
{
294-
var replacementString = home + arg.Substring(1);
295-
_arguments.Append(replacementString);
296-
}
297-
else
298-
{
299-
_arguments.Append(arg);
300-
}
296+
_arguments.Append(expandedPath);
301297
}
302-
#else
303-
_arguments.Append(arg);
304-
#endif
298+
argExpanded = true;
305299
}
306300
}
307301
}
308-
} while (list != null);
302+
}
303+
else if (!usedQuotes)
304+
{
305+
// Even if there are no wildcards, we still need to possibly
306+
// expand ~ into the filesystem provider home directory path
307+
ProviderInfo fileSystemProvider = Context.EngineSessionState.GetSingleProvider(FileSystemProvider.ProviderName);
308+
string home = fileSystemProvider.Home;
309+
if (string.Equals(arg, "~"))
310+
{
311+
_arguments.Append(home);
312+
argExpanded = true;
313+
}
314+
else if (arg.StartsWith("~/", StringComparison.OrdinalIgnoreCase))
315+
{
316+
var replacementString = home + arg.Substring(1);
317+
_arguments.Append(replacementString);
318+
argExpanded = true;
319+
}
320+
}
321+
#endif // UNIX
322+
323+
if (!argExpanded)
324+
{
325+
_arguments.Append(arg);
326+
}
309327
}
310328

311329
/// <summary>
@@ -336,6 +354,6 @@ internal static bool NeedQuotes(string stringToCheck)
336354
/// The native command to bind to
337355
/// </summary>
338356
private NativeCommand _nativeCommand;
339-
#endregion private members
357+
#endregion private members
340358
}
341359
} // namespace System.Management.Automation

test/powershell/Language/Scripting/NativeExecution/NativeUnixGlobbing.Tests.ps1

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,70 @@ Describe 'Native UNIX globbing tests' -tags "CI" {
3535
It 'Globbing [cde]b?.* should return one file name "cbb.txt"' {
3636
/bin/ls $TESTDRIVE/[cde]b?.* | Should Match "cbb.txt"
3737
}
38-
It 'Should return the original pattern if there are no matches' {
39-
/bin/echo $TESTDRIVE/*.nosuchfile | Should Match "\*\.nosuchfile$"
38+
# Test globbing with expressions
39+
It 'Globbing should work with unquoted expressions' {
40+
$v = "$TESTDRIVE/abc*"
41+
/bin/ls $v | Should Match "abc.txt"
42+
43+
$h = [pscustomobject]@{P=$v}
44+
/bin/ls $h.P | Should Match "abc.txt"
45+
46+
$a = $v,$v
47+
/bin/ls $a[1] | Should Match "abc.txt"
48+
}
49+
It 'Globbing should not happen with quoted expressions' {
50+
$v = "$TESTDRIVE/abc*"
51+
/bin/echo "$v" | Should BeExactly $v
52+
/bin/echo '$v' | Should BeExactly '$v'
53+
}
54+
It 'Should return the original pattern (<arg>) if there are no matches' -TestCases @(
55+
@{arg = '/nOSuCH*file'}, # No matching file
56+
@{arg = '/bin/nOSuCHdir/*'}, # Directory doesn't exist
57+
@{arg = '-NosUch*fIle'}, # Parameter syntax but could be file
58+
@{arg = '-nOsuCh*drive:nosUch*fIle'}, # Parameter w/ arg syntax, could specify drive
59+
@{arg = '-nOs[u]ChdrIve:nosUch*fIle'}, # Parameter w/ arg syntax, could specify drive
60+
@{arg = '-nOsuChdRive:nosUch*fIle'}, # Parameter w/ arg syntax, could specify drive
61+
@{arg = '-nOsuChdRive: nosUch*fIle'}, # Parameter w/ arg syntax, could specify drive
62+
@{arg = '/no[suchFilE'}, # Invalid wildcard (no closing ']')
63+
@{arg = '[]'} # Invalid wildcard
64+
) {
65+
param($arg)
66+
/bin/echo $arg | Should BeExactly $arg
67+
}
68+
$quoteTests = @(
69+
@{arg = '"*"'},
70+
@{arg = "'*'"}
71+
)
72+
It 'Should not expand quoted strings: <arg>' -TestCases $quoteTests {
73+
param($arg)
74+
Invoke-Expression "/bin/echo $arg" | Should BeExactly '*'
75+
}
76+
# Splat tests are skipped because they should work, but don't.
77+
# Supporting this scenario would require adding a NoteProperty
78+
# to each quoted string argument - maybe not worth it, and maybe
79+
# an argument for another way to suppress globbing.
80+
It 'Should not expand quoted strings via splat array: <arg>' -TestCases $quoteTests -Skip {
81+
param($arg)
82+
83+
function Invoke-Echo
84+
{
85+
/bin/echo @args
86+
}
87+
Invoke-Expression "Invoke-Echo $arg" | Should BeExactly '*'
88+
}
89+
It 'Should not expand quoted strings via splat hash: <arg>' -TestCases $quoteTests -Skip {
90+
param($arg)
91+
92+
function Invoke-Echo($quotedArg)
93+
{
94+
/bin/echo @PSBoundParameters
95+
}
96+
Invoke-Expression "Invoke-Echo -quotedArg:$arg" | Should BeExactly "-quotedArg:*"
97+
98+
# When specifing a space after the parameter, the space is removed when splatting.
99+
# This behavior is debatable, but it's worth adding this test anyway to detect
100+
# a change in behavior.
101+
Invoke-Expression "Invoke-Echo -quotedArg: $arg" | Should BeExactly "-quotedArg:*"
40102
}
41103
# Test the behavior in non-filesystem drives
42104
It 'Should not expand patterns on non-filesystem drives' {
@@ -56,4 +118,10 @@ Describe 'Native UNIX globbing tests' -tags "CI" {
56118
It '~/foo should be replaced by the <filesystem provider home directory>/foo' {
57119
/bin/echo ~/foo | Should BeExactly "$($executioncontext.SessionState.Provider.Get("FileSystem").Home)/foo"
58120
}
121+
It '~ should not be replaced when quoted' {
122+
/bin/echo '~' | Should BeExactly '~'
123+
/bin/echo "~" | Should BeExactly '~'
124+
/bin/echo '~/foo' | Should BeExactly '~/foo'
125+
/bin/echo "~/foo" | Should BeExactly '~/foo'
126+
}
59127
}

0 commit comments

Comments
 (0)
0