Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the issue Get-Help does not support string pattern under Unix #3852

Merged
merged 1 commit into from
Jun 7, 2017

Conversation

chunqingchen
Copy link
Contributor

@chunqingchen chunqingchen commented May 24, 2017

Fix issue #2653

Summary of the issue:

Under Unix system, get-help would not support wildcast pattern such as about*

Root cause of the issue:

Under unix, GetFiles() is using filePath.IndexOf() to search string pattern, this api actually doesn't support pattern.

Fix:

Use regex expression to match *, ? if the search string contains such wildcast. which are the only two cases are supported.

#endif
// On Linux, file names are case sensitive, so we need to put both string to lower case.
// We can not use StringComparison.OrdinalIgnoreCase here as api supports OrdinalIgnoreCase won't support pattern
return Directory.GetFiles(path.ToLower(), pattern.ToLower());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToLower() is not required for path, as GetFiles considers it as case insensitive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

{
# Create at least one help file matches "about*" pattern
$null = New-Item -ItemType File -Path $helpFilePath -Value "about_test" -ErrorAction SilentlyContinue
(Get-ChildItem $PSHOME\$helpFileWithPattern -Recurse).Count | Should Be (Get-Help about*).Count
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also verify that the about_testCase.help.txt is one of the files that is returned by Get-Help?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved by only get the specified files start with about_testcase*

{
# Create at least one help file matches "about*" pattern
$null = New-Item -ItemType File -Path $helpFilePath -Value "about_test" -ErrorAction SilentlyContinue
(Get-ChildItem $PSHOME\$helpFileWithPattern -Recurse).Count | Should Be (Get-Help about*).Count
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get-Help can get help content from other paths under PSModulePath. So if there are other about help topics not under $PSHome, the counts would not match.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

@daxian-dbw
Copy link
Member

@chunqingchen The PR title and body should not be a copy of the issue. Please update the PR title and body based on the contribution.md.

General Guidelines

  1. Use a meaningful title for the PR to describe what is changed in this PR.
    • Do not include issue number in the title. Put it in the description instead, like "Fix Fix help alias #5".
    • Do not use the issue title as the PR title.
      An issue title is to briefly describe what went wrong, while a PR title is to briefly describe what is changed.
  2. The PR body shall provide an additional summary of what/why went wrong and how it's fixed in the PR description.

@chunqingchen chunqingchen force-pushed the bugfix0 branch 2 times, most recently from 9881659 to 92a2b51 Compare May 25, 2017 10:01
@chunqingchen chunqingchen changed the title Get-Help does not support string pattern under Unix Fix the issue Get-Help does not support string pattern under Unix May 25, 2017
@chunqingchen
Copy link
Contributor Author

@daxian-dbw your comment is resolved
@adityapatwardhan your comments are resolved

private string[] GetFiles(string path, string pattern)
{
private string[] GetFiles(string path, string pattern)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like a formatting mistake here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatting is fixed. It shows normal under IDE, I have to delete and re tab to fix it.

}
}
return (String[])result.ToArray(typeof(string));
#else
return Directory.GetFiles(path, pattern);
return Directory.GetFiles(path, pattern);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the formatting change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

string regexPattern = pattern.Replace(".", @"\.");
regexPattern = regexPattern.Replace("*", ".*");
regexPattern = regexPattern.Replace("?", ".?");
Regex r = new Regex(regexPattern);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this matching is infrequent, you should use an interpreted regex instead of a compiled one

https://msdn.microsoft.com/en-us/library/gg578045(v=vs.110).aspx

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also use RegEx.IsMatch(filePath, regexPattern). This avoids instantiating the an RegEx object and RegEx engine caches the compiled version for reuse.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

# Create at least one help file matches "about*" pattern
$null = New-Item -ItemType File -Path $helpFilePath1 -Value "about_test1" -ErrorAction SilentlyContinue
$null = New-Item -ItemType File -Path $helpFilePath2 -Value "about_test2" -ErrorAction SilentlyContinue
(Get-Help about_testCase*).Count | Should Be 2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case doesn't cover the code change, you explicitly handle *, ?, and .

Seems like you should also have a test case with multiple wildcards

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved.

#if UNIX
// On Linux, file names are case sensitive, so we need to add
// extra logic to select the files that match the given pattern.
ArrayList result = new ArrayList();
string[] files = Directory.GetFiles(path);

string regexPattern = pattern.Replace(".", @"\.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RegEx.Escape(pattern) should be used first, before any replaces.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually should not be used. RegEx.Escape will also escape the '*' and '?' which we want them as normal character to replace later.

string regexPattern = pattern.Replace(".", @"\.");
regexPattern = regexPattern.Replace("*", ".*");
regexPattern = regexPattern.Replace("?", ".?");
Regex r = new Regex(regexPattern);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also use RegEx.IsMatch(filePath, regexPattern). This avoids instantiating the an RegEx object and RegEx engine caches the compiled version for reuse.

regexPattern = regexPattern.Replace("*", ".*");
regexPattern = regexPattern.Replace("?", ".?");
Regex r = new Regex(regexPattern);

foreach (string filePath in files)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can all this code be replaced by:

return Directory.GetFiles(path).Where(f => Regex.IsMatch(f, regExPattern.ToString())).ToArray();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return Directory.GetFiles(path).Where(f => Regex.IsMatch(f, regExPattern.ToString())).ToArray(); is not implementing the logic of string which is not using a pattern

@chunqingchen chunqingchen force-pushed the bugfix0 branch 11 times, most recently from 950584f to b82e76b Compare May 31, 2017 11:02
@chunqingchen
Copy link
Contributor Author

@adityapatwardhan @SteveL-MSFT Thanks Aditya and Steve. Your comments are resolved.

I have spent much time try to figure out why there's time out exception on Unix test runs on my pull request. It ended up not related to my code and also the code and tests get passed on local unix os (both Ubuntu and MacOS). See my comments with the test case. I have to put a skip on the test case if it is not windows os.

@@ -114,17 +115,31 @@ private void SearchForFiles()
}

private string[] GetFiles(string path, string pattern)
{
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra space after {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

@@ -225,4 +225,38 @@ Describe "Get-Help should find help info within help files" -Tags @('CI', 'Requi
Remove-Item $helpFilePath -Force -ErrorAction SilentlyContinue
}
}

# There is a bug specified to Tranvis CI that hangs the test if "get-help" is used to search pattern string. This doesn't repro locally.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typos:
Tranvis -> Travis
specified -> specific

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

$helpFilePath1 = Join-Path $helpFolderPath $helpFile1
$helpFilePath2 = Join-Path $helpFolderPath $helpFile2

if (!(Test-Path $helpFolderPath))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if block can be replace by the following. If it already exists, it is not modified.

$null = New-Item -ItemType Directory -Path $helpFolderPath -Force

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

# Create at least one help file matches "about*" pattern
$null = New-Item -ItemType File -Path $helpFilePath1 -Value "about_test1" -ErrorAction SilentlyContinue
$null = New-Item -ItemType File -Path $helpFilePath2 -Value "about_test2" -ErrorAction SilentlyContinue
Get-Help about_testCas?1 | Should Match "about_test1"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all the Shoulds, use Be or BeExactly. Match uses regular expression comparison. It is not required here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

Get-Help about_testCas?1 | Should Match "about_test1"
Get-Help about_testCase.? | Should Match "about_test2"
(Get-Help about_testCase*).Count | Should Be 2
Get-Help about_testCas?.2* | Should Match "about_test2"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, each It should have only one Should. This ensures that if the first should fails, the rest are not ignored. Take a look at: http://www.powershellmagazine.com/2015/06/04/pester-triangulation-and-reusing-test-cases/
You can pass the values for Get-Help as testcases and values for Should as expected values. Using testcases parameter of It also allows you to give better names for each variation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with @adityapatwardhan, this is a good example to use the -TestCases parameter of Pester. Look at other test code in this repo as a sample.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved with triangulation format. a good way !

@chunqingchen
Copy link
Contributor Author

@adityapatwardhan @SteveL-MSFT your comments are resolved

@{command = "Get-Help about_testCas?.2*"; testname = "test ?, * pattern with dot"; result = "about_test2"}
)

It "Get-Help should find pattern help files - <testname>" -TestCases $testcases -skip: (-not $IsWindows){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unless the testcases are used in other tests, I would prefer to have these inline to the It statement as it makes it easier to associate the two

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good example of using test cases would be:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

@{command = "Get-Help about_testCas?.2*"; testname = "test ?, * pattern with dot"; result = "about_test2"}
)

It "Get-Help should find pattern help files - <testname>" -TestCases $testcases -skip: (-not $IsWindows){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good example of using test cases would be:

try
{
# Create at least one help file matches "about*" pattern
$null = New-Item -ItemType File -Path $helpFilePath1 -Value "about_test1" -ErrorAction SilentlyContinue
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these be done in beforeall and cleanup in afterall?

$command,
$result
)
Invoke-Expression $command | Should Be $result
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use Invoke-Expression instead use splatting instead. Look at: 28ec9a3

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case (Get-Help about_testCase*).Count would needs to be explicitly written in the test case one more time. If u think this is better than invoke-expression i will update later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After talking to Jim, in general Invoke-Expression is not a best practice. An alternative approach would be to make the command as script block. So the test would look like:

$testcases = @( 
                  @{command = {Get-Help about_testCas?1}; testname = "test ? pattern"; result = "about_test1"} 
                  @{command = {Get-Help about_testCase.?}; testname = "test ? pattern with dot"; result = "about_test2"} 
                  @{command = {(Get-Help about_testCase*).Count}; testname = "test * pattern"; result = "2"} 
                  @{command = {Get-Help about_testCas?.2*}; testname = "test ?, * pattern with dot"; result = "about_test2"} 
               ) 

And replace Invoke-Expression with

$command.Invoke() | Should be $result

@{command = "Get-Help about_testCas?.2*"; testname = "test ?, * pattern with dot"; result = "about_test2"}
)

It "Get-Help should find pattern help files - <testname>" -TestCases $testcases -skip: (-not $IsWindows){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be marked as Pending on Non-Windows and not skipped. So we can revisit later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved


# There is a bug specific to Travis CI that hangs the test if "get-help" is used to search pattern string. This doesn't repro locally.
# This occurs even if Unix system just returns "Directory.GetFiles(path, pattern);" as the windows' code does.
# Since there's currently no way to get the vm from Tranvis CI and the test PASSES locally on both Ubuntu and MacOS, excluding pattern test under Unix system.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: Tranvis -> Travis.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

# Remove the test files
AfterAll {
Remove-Item $helpFilePath1 -Force -ErrorAction SilentlyContinue
Remove-Item $helpFilePath2 -Force -ErrorAction SilentlyContinue
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$helpFolderPath does not seem to be cleaned up? Maybe you can just delete $helpFolderPath recursively.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$helpFolderPath is an existed folder contains other help files.

$command,
$result
)
Invoke-Expression $command | Should Be $result
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After talking to Jim, in general Invoke-Expression is not a best practice. An alternative approach would be to make the command as script block. So the test would look like:

$testcases = @( 
                  @{command = {Get-Help about_testCas?1}; testname = "test ? pattern"; result = "about_test1"} 
                  @{command = {Get-Help about_testCase.?}; testname = "test ? pattern with dot"; result = "about_test2"} 
                  @{command = {(Get-Help about_testCase*).Count}; testname = "test * pattern"; result = "2"} 
                  @{command = {Get-Help about_testCas?.2*}; testname = "test ?, * pattern with dot"; result = "about_test2"} 
               ) 

And replace Invoke-Expression with

$command.Invoke() | Should be $result

@chunqingchen
Copy link
Contributor Author

@adityapatwardhan your comment has been resolved

@mirichmo mirichmo merged commit 751dada into PowerShell:master Jun 7, 2017
// If the input is pattern instead of string, we need to use Regex expression.
if (pattern.Contains("*") || pattern.Contains("?"))
{
if (Regex.IsMatch(filePath, regexPattern))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using regular expression matching instead of wildcard matching?

FWIW - wildcard matching already converts the wildcard to a regex - so this code is probably doing something similar but subtly different.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. We basically need to implementation Directory.GetFiles(path, pattern); in a case-insensitive way, and the code at https://github.com/PowerShell/PowerShell/blob/master/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs#L5961 is doing the same thing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#4053 is opened for this.
Never mind, it's a duplicate of #3967

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants