From 11ad02a15a562aa080f8e41dbfed8dc02588c4a5 Mon Sep 17 00:00:00 2001 From: Bruce Payette Date: Mon, 1 May 2017 12:23:36 -0700 Subject: [PATCH] Adding support for native command globbing on UNIX (#3643) This change enables globbing (wildcard expansion) against the file system for native commands like '/bin/ls'. The expansion is only done in the file system. In non-filesystem drives expansion is not done and the pattern is returned unchanged. Limitations of the fix: Currently quoting is not honored so for a command like /bin/ls "*.txt", wildcard expansion will still be done. Adding support for bare word detection will come in a future PR. Use --% to suppress wildcard expansion e.g. git add --% * --- .../engine/NativeCommandParameterBinder.cs | 139 +++++++++++++++--- .../NativeUnixGlobbing.Tests.ps1 | 59 ++++++++ 2 files changed, 179 insertions(+), 19 deletions(-) create mode 100644 test/powershell/Language/Scripting/NativeExecution/NativeUnixGlobbing.Tests.ps1 diff --git a/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs b/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs index d6cfdb7065..3794f41c43 100644 --- a/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs +++ b/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs @@ -192,25 +192,8 @@ namespace System.Management.Automation // // We need to check quotes that the win32 argument parser checks which is currently // just the normal double quotes, no other special quotes. Also note that mismatched - // quotes are supported. - - bool needQuotes = false, followingBackslash = false; - int quoteCount = 0; - for (int i = 0; i < arg.Length; i++) - { - if (arg[i] == '"' && !followingBackslash) - { - quoteCount += 1; - } - else if (char.IsWhiteSpace(arg[i]) && (quoteCount % 2 == 0)) - { - needQuotes = true; - } - - followingBackslash = arg[i] == '\\'; - } - - if (needQuotes) + // quotes are supported + if (NeedQuotes(arg)) { _arguments.Append('"'); _arguments.Append(arg); @@ -218,13 +201,131 @@ namespace System.Management.Automation } else { +#if UNIX + // On UNIX systems, we expand arguments containing wildcard expressions against + // the file system just like bash, etc. + if (System.Management.Automation.WildcardPattern.ContainsWildcardCharacters(arg)) + { + // See if the current working directory is a filesystem provider location + // We won't do the expansion if it isn't since native commands can only access the file system. + var cwdinfo = Context.EngineSessionState.CurrentLocation; + + // If it's a filesystem location then expand the wildcards + if (string.Equals(cwdinfo.Provider.Name, Microsoft.PowerShell.Commands.FileSystemProvider.ProviderName, + StringComparison.OrdinalIgnoreCase)) + { + bool normalizePath = true; + // On UNIX, paths starting with ~ are not normalized + if (arg.Length > 0 && arg[0] == '~') + { + normalizePath = false; + } + + // See if there are any matching paths otherwise just add the pattern as the argument + var paths = Context.EngineSessionState.InvokeProvider.ChildItem.Get(arg, false); + if (paths.Count > 0) + { + bool first = true; + foreach (var path in paths) + { + object pbo = path.BaseObject; + if (! first) + { + _arguments.Append(" "); + } + else + { + if (! (pbo is System.IO.FileSystemInfo)) + { + // If the object is not a filesystem object, then just append + // the pattern unchanged + _arguments.Append(arg); + break; + } + first = false; + } + var expandedPath = (pbo as System.IO.FileSystemInfo).FullName; + if (normalizePath) + { + expandedPath = Context.SessionState.Path.NormalizeRelativePath(expandedPath, cwdinfo.ProviderPath); + } + // If the path contains spaces, then add quotes around it. + if (NeedQuotes(expandedPath)) + { + _arguments.Append("\""); + _arguments.Append(expandedPath); + _arguments.Append("\""); + } + else + { + _arguments.Append(expandedPath); + } + } + } + else + { + _arguments.Append(arg); + } + } + else + { + _arguments.Append(arg); + } + } + else + { + // Even if there are no wildcards, we still need to possibly + // expand ~ into the filesystem provider home directory path + ProviderInfo fileSystemProvider = Context.EngineSessionState.GetSingleProvider( + Microsoft.PowerShell.Commands.FileSystemProvider.ProviderName); + string home = fileSystemProvider.Home; + if (string.Equals(arg, "~")) + { + _arguments.Append(home); + } + else if (arg.StartsWith("~/", StringComparison.OrdinalIgnoreCase)) + { + var replacementString = home + arg.Substring(1); + _arguments.Append(replacementString); + } + else + { + _arguments.Append(arg); + } + } +#else _arguments.Append(arg); +#endif } } } } while (list != null); } + /// + /// Check to see if the string contains spaces and therefore must be quoted. + /// + /// The string to check for spaces + private bool NeedQuotes(string stringToCheck) + { + bool needQuotes = false, followingBackslash = false; + int quoteCount = 0; + for (int i = 0; i < stringToCheck.Length; i++) + { + if (stringToCheck[i] == '"' && !followingBackslash) + { + quoteCount += 1; + } + else if (char.IsWhiteSpace(stringToCheck[i]) && (quoteCount % 2 == 0)) + { + needQuotes = true; + } + followingBackslash = stringToCheck[i] == '\\'; + } + return needQuotes; + } + + /// /// The native command to bind to /// diff --git a/test/powershell/Language/Scripting/NativeExecution/NativeUnixGlobbing.Tests.ps1 b/test/powershell/Language/Scripting/NativeExecution/NativeUnixGlobbing.Tests.ps1 new file mode 100644 index 0000000000..c18d75c464 --- /dev/null +++ b/test/powershell/Language/Scripting/NativeExecution/NativeUnixGlobbing.Tests.ps1 @@ -0,0 +1,59 @@ + +Describe 'Native UNIX globbing tests' -tags "CI" { + + BeforeAll { + if (-not $IsWindows ) + { + "" > "$TESTDRIVE/abc.txt" + "" > "$TESTDRIVE/bbb.txt" + "" > "$TESTDRIVE/cbb.txt" + } + + $defaultParamValues = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues["it:skip"] = $IsWindows + } + + AfterAll { + $global:PSDefaultParameterValues = $defaultParamValues + } + + # Test * expansion + It 'The globbing pattern *.txt should match 3 files' { + (/bin/ls $TESTDRIVE/*.txt).Length | Should Be 3 + } + It 'The globbing pattern *b.txt should match 2 files whose basenames end in "b"' { + (/bin/ls $TESTDRIVE/*b.txt).Length | Should Be 2 + } + # Test character classes + It 'The globbing pattern should match 2 files whose names start with either "a" or "b"' { + (/bin/ls $TESTDRIVE/[ab]*.txt).Length | Should Be 2 + } + It 'Globbing abc.* should return one file name "abc.txt"' { + /bin/ls $TESTDRIVE/abc.* | Should Match "abc.txt" + } + # Test that ? matches any single character + It 'Globbing [cde]b?.* should return one file name "cbb.txt"' { + /bin/ls $TESTDRIVE/[cde]b?.* | Should Match "cbb.txt" + } + It 'Should return the original pattern if there are no matches' { + /bin/echo $TESTDRIVE/*.nosuchfile | Should Match "\*\.nosuchfile$" + } + # Test the behavior in non-filesystem drives + It 'Should not expand patterns on non-filesystem drives' { + /bin/echo env:ps* | Should BeExactly "env:ps*" + } + # Test the behavior for files with spaces in the names + It 'Globbing filenames with spaces should match 2 files' { + "" > "$TESTDRIVE/foo bar.txt" + "" > "$TESTDRIVE/foo baz.txt" + (/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 /foo' { + /bin/echo ~/foo | Should BeExactly "$($executioncontext.SessionState.Provider.Get("FileSystem").Home)/foo" + } +}