8000 Implement UNIX-like globbing for native commands · PowerShell/PowerShell@f451da9 · GitHub
[go: up one dir, main page]

Skip to content 8000

Commit f451da9

Browse files
author
Bruce Payette
committed
Implement UNIX-like globbing for native commands
1 parent f8f603a commit f451da9

File tree

2 files changed

+173
-19
lines changed

2 files changed

+173
-19
lines changed

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

Lines changed: 120 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -192,39 +192,140 @@ private void appendOneNativeArgument(ExecutionContext context, object obj, char
192192
//
193193
// We need to check quotes that the win32 argument parser checks which is currently
194194
// just the normal double quotes, no other special quotes. Also note that mismatched
195-
// quotes are supported.
196-
197-
bool needQuotes = false, followingBackslash = false;
198-
int quoteCount = 0;
199-
for (int i = 0; i < arg.Length; i++)
200-
{
201-
if (arg[i] == '"' && !followingBackslash)
202-
{
203-
quoteCount += 1;
204-
}
205-
else if (char.IsWhiteSpace(arg[i]) && (quoteCount % 2 == 0))
206-
{
207-
needQuotes = true;
208-
}
209-
210-
followingBackslash = arg[i] == '\\';
211-
}
212-
213-
if (needQuotes)
195+
// quotes are supported
196+
if (NeedQuotes(arg))
214197
{
215198
_arguments.Append('"');
216199
_arg 10000 uments.Append(arg);
217200
_arguments.Append('"');
218201
}
219202
else
220203
{
204+
#if UNIX
205+
// On UNIX systems, we expand arguments containing wildcard expressions against
206+
// the file system just like bash, etc.
207+
if (System.Management.Automation.WildcardPattern.ContainsWildcardCharacters(arg))
208+
{
209+
// See if the current working directory is a filesystem provider location
210+
// We won't do the expansion if it isn't since native commands can only access the file system.
211+
var cwdinfo = Context.EngineSessionState.CurrentLocation;
212+
213+
// If it's a filesystem location then expand the wildcards
214+
if (string.Equals(cwdinfo.Provider.Name, Microsoft.PowerShell.Commands.FileSystemProvider.ProviderName,
215+
StringComparison.OrdinalIgnoreCase))
216+
{
217+
bool normalizePath = true;
218+
// On UNIX, paths starting with ~ are not normalized
219+
if (arg.Length > 0 && arg[0] == '~')
220+
{
221+
normalizePath = false;
222+
}
223+
224+
// See if there are any matching paths otherwise just add the pattern as the argument
225+
var paths = Context.EngineSessionState.InvokeProvider.ChildItem.Get(arg, false);
226+
if (paths.Count > 0)
227+
{
228+
bool first = true;
229+
foreach (var path in paths)
230+
{
231+
object pbo = path.BaseObject;
232+
if (! first)
233+
{
234+
_arguments.Append(" ");
235+
}
236+
else
237+
{
238+
if (! (pbo is System.IO.FileSystemInfo))
239+
{
240+
// If the object is not a filesystem object, then just append
241+
// the pattern unchanged
242+
_arguments.Append(arg);
243+
break;
244+
}
245+
first = false;
246+
}
247+
var expandedPath = (pbo as System.IO.FileSystemInfo).FullName;
248+
if (normalizePath)
249+
{
250+
expandedPath = Context.SessionState.Path.NormalizeRelativePath(expandedPath, cwdinfo.ProviderPath);
251+
}
252+
// If the path contains spaces, then add quotes around it.
253+
if (NeedQuotes(expandedPath))
254+
{
255+
_arguments.Append("\"");
256+
_arguments.Append(expandedPath);
257+
_arguments.Append("\"");
258+
}
259+
else
260+
{
261+
_arguments.Append(expandedPath);
262+
}
263+
}
264+
}
265+
else
266+
{
267+
_arguments.Append(arg);
268+
}
269+
}
270+
else
271+
{
272+
_arguments.Append(arg);
273+
}
274+
}
275+
else
276+
{
277+
// Even if there are no wildcards, we still need to possibly
278+
// expand ~ into the filesystem provider home directory path
279+
ProviderInfo fileSystemProvider = Context.EngineSessionState.GetSingleProvider(
280+
Microsoft.PowerShell.Commands.FileSystemProvider.ProviderName);
281+
string home = fileSystemProvider.Home;
282+
if (string.Equals(arg, "~"))
283+
{
284+
_arguments.Append(home);
285+
}
286+
else if (arg.StartsWith("~/", StringComparison.OrdinalIgnoreCase))
287+
{
288+
var replacementString = home + arg.Substring(1);
289+
_arguments.Append(replacementString);
290+
}
291+
else
292+
{
293+
_arguments.Append(arg);
294+
}
295+
}
296+
#else
221297
_arguments.Append(arg);
298+
#endif
222299
}
223300
}
224301
}
225302
} while (list != null);
226303
}
227304

305+
/// <summary>
306+
/// Check to see if the string contains spaces and therefore must be quoted.
307+
/// </summary>
308+
/// <param name="stringToCheck">The string to check for spaces</param>
309+
private bool NeedQuotes(string stringToCheck)
310+
{
311+
bool needQuotes = false, followingBackslash = false;
312+
int quoteCount = 0;
313+
for (int i = 0; i < stringToCheck.Length; i++)
314+
{
315+
if (stringToCheck[i] == '"' && !followingBackslash)
316+
{
317+
quoteCount += 1;
318+
}
319+
else if (char.IsWhiteSpace(stringToCheck[i]) && (quoteCount % 2 == 0))
320+
{
321+
needQuotes = true;
322+
}
323+
followingBackslash = stringToCheck[i] == '\\';
324+
}
325+
return needQuotes;
326+
}
327+
328+
228329
/// <summary>
229330
/// The native command to bind to
230331
/// </summary>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
$PesterSkip = if ( $IsWindows ) { @{ Skip = $true } } else { @{} }
2+
3+
Describe 'Native UNIX globbing tests' -tags "CI" {
4+
5+
BeforeAll {
6+
if (-not $IsWindows )
7+
{
8+
"" > "$TESTDRIVE/abc.txt"
9+
"" > "$TESTDRIVE/bbb.txt"
10+
"" > "$TESTDRIVE/cbb.txt"
11+
}
12+
}
13+
14+
# Test * expansion
15+
It 'The globbing pattern *.txt should match 3 files' @PesterSkip {
16+
(/bin/ls $TESTDRIVE/*.txt).Length | Should Be 3
17+
}
18+
It 'The globbing pattern *b.txt should match 2 files whose basenames end in "b"' @PesterSkip {
19+
(/bin/ls $TESTDRIVE/*b.txt).Length | Should Be 2
20+
}
21+
# Test character classes
22+
It 'The globbing pattern should match 2 files whose names start with either "a" or "b"' @PesterSkip {
23+
(/bin/ls $TESTDRIVE/[ab]*.txt).Length | Should Be 2
24+
}
25+
It 'Globbing abc.* should return one file name "abc.txt"' @PesterSkip {
26+
/bin/ls $TESTDRIVE/abc.* | Should Match "abc.txt"
27+
}
28+
# Test that ? matches any single character
29+
It 'Globbing [cde]b?.* should return one file name "cbb.txt"' @PesterSkip {
30+
/bin/ls $TESTDRIVE/[cde]b?.* | Should Match "cbb.txt"
31+
}
32+
It 'Should return the original pattern if there are no matches' @PesterSkip {
33+
/bin/echo $TESTDRIVE/*.nosuchfile | Should Match "\*\.nosuchfile$"
34+
}
35+
# Test the behavior in non-filesystem drives
36+
It 'Should not expand patterns on non-filesystem drives' @PesterSkip {
37+
/bin/echo env:ps* | Should BeExactly "env:ps*"
38+
}
39+
# Test the behavior for files with spaces in the names
40+
It 'Globbing filenames with spaces should match 2 files' @PesterSkip {
41+
"" > "$TESTDRIVE/foo bar.txt"
42+
"" > "$TESTDRIVE/foo baz.txt"
43+
(/bin/ls $TESTDRIVE/foo*.txt).Length | Should Be 2
44+
}
45+
# Test ~ expansion
46+
It 'Tilde should be replaced by the filesystem provider home directory' @PesterSkip {
47+
/bin/echo ~ | Should BeExactly ($executioncontext.SessionState.Provider.Get("FileSystem").Home)
48+
}
49+
# Test ~ expansion with a path fragment (e.g. ~/foo)
50+
It '~/foo should be replaced by the <filesystem provider home directory>/foo' @PesterSkip {
51+
/bin/echo ~/foo | Should BeExactly "$($executioncontext.SessionState.Provider.Get("FileSystem").Home)/foo"
52+
}
53+
}

0 commit comments

Comments
 (0)
0