Skip to content

Commit

Permalink
✨ Windows Compatibility-Add PowerShell Script as an Alternative to Gr…
Browse files Browse the repository at this point in the history
…ep on Linux (#690)

PR Summary
This pull request introduces a PowerShell-based alternative to the grep
command, ensuring that the built-in File Searching feature is also
compatible with Windows systems.

service_client.go: Added runOSSpecificGrepCommand to handle grep
functionality for both Windows (using PowerShell) and Unix-based
systems. This function checks for the OS platform and invokes
powershell(windows) and grep(linux). Replaced direct grep command
execution with runOSSpecificGrepCommand to support cross-platform
compatibility.

service_client.go: Implemented parseGrepOutputForFileContent to parse
the output from both PowerShell and grep. This enhancement not only
refines the existing parsing mechanism for Linux but also accommodates
Windows file path patterns and addresses issues with the split function
when multiple ':' appear in the input.

WIP: Testing

---------

Signed-off-by: kthatipally <[email protected]>
  • Loading branch information
kthatipally authored Nov 26, 2024
1 parent 8c930fc commit 80d7fb0
Showing 1 changed file with 77 additions and 28 deletions.
105 changes: 77 additions & 28 deletions provider/internal/builtin/service_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,29 +106,12 @@ func (p *builtinServiceClient) Evaluate(ctx context.Context, cap string, conditi
if c.Pattern == "" {
return response, fmt.Errorf("could not parse provided regex pattern as string: %v", conditionInfo)
}

var outputBytes []byte
if runtime.GOOS == "darwin" {
cmd := fmt.Sprintf(
`find %v -type f | \
while read file; do perl -ne '/(%v)/ && print "$ARGV:$.:$1\n";' "$file"; done`,
p.config.Location, c.Pattern,
)
findstr := exec.Command("/bin/sh", "-c", cmd)
outputBytes, err = findstr.Output()
} else {
grep := exec.Command("grep", "-o", "-n", "-R", "-P", c.Pattern)
if ok, paths := cond.ProviderContext.GetScopedFilepaths(); ok {
grep.Args = append(grep.Args, paths...)
} else {
grep.Args = append(grep.Args, p.config.Location)
}
outputBytes, err = grep.Output()
}
//Runs on Windows using PowerShell.exe and Unix based systems using grep
outputBytes, err := runOSSpecificGrepCommand(c.Pattern, p.config.Location, cond.ProviderContext)
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() == 1 {
return response, nil
}
return response, fmt.Errorf("could not run grep with provided pattern %+v", err)
return response, err
}
matches := []string{}
outputString := strings.TrimSpace(string(outputBytes))
Expand All @@ -137,13 +120,10 @@ func (p *builtinServiceClient) Evaluate(ctx context.Context, cap string, conditi
}

for _, match := range matches {
//TODO(fabianvf): This will not work if there is a `:` in the filename, do we care?
pieces := strings.SplitN(match, ":", 3)
if len(pieces) != 3 {
//TODO(fabianvf): Just log or return?
//(shawn-hurley): I think the return is good personally
return response, fmt.Errorf(
"malformed response from grep, cannot parse grep output '%s' with pattern {filepath}:{lineNumber}:{matchingText}", match)
var pieces []string
pieces, err := parseGrepOutputForFileContent(match)
if err != nil {
return response, fmt.Errorf("could not parse grep output '%s' for the Pattern '%v': %v ", match, c.Pattern, err)
}

containsFile, err := provider.FilterFilePattern(c.FilePattern, pieces[0])
Expand All @@ -167,6 +147,7 @@ func (p *builtinServiceClient) Evaluate(ctx context.Context, cap string, conditi
if err != nil {
return response, fmt.Errorf("cannot convert line number string to integer")
}

response.Incidents = append(response.Incidents, provider.IncidentContext{
FileURI: uri.File(absPath),
LineNumber: &lineNumber,
Expand Down Expand Up @@ -593,3 +574,71 @@ func (b *builtinServiceClient) isFileIncluded(absolutePath string) bool {
b.log.V(7).Info("excluding file from search", "file", absolutePath)
return false
}

func parseGrepOutputForFileContent(match string) ([]string, error) {
// This will parse the output of the PowerShell/grep in the form
// "Filepath:Linenumber:Matchingtext" to return string array of path, line number and matching text
// works with handling both windows and unix based file paths eg: "C:\path\to\file" and "/path/to/file"
re, err := regexp.Compile(`^(.*?):(\d+):(.*)$`)
if err != nil {
return nil, fmt.Errorf("failed to compile regular expression: %v", err)
}
submatches := re.FindStringSubmatch(match)
if len(submatches) != 4 {
return nil, fmt.Errorf(
"malformed response from file search, cannot parse result '%s' with pattern %#q", match, re)
}
return submatches[1:], nil
}

func runOSSpecificGrepCommand(pattern string, location string, providerContext provider.ProviderContext) ([]byte, error) {
var outputBytes []byte
var err error
var utilName string

if runtime.GOOS == "windows" {
utilName = "powershell.exe"
// Windows does not have grep, so we use PowerShell.exe's Select-String instead
// This is a workaround until we can find a better solution
psScript := `
$pattern = $env:PATTERN
$location = $env:FILEPATH
Get-ChildItem -Path $location -Recurse -File | ForEach-Object {
$file = $_
# Search for the pattern in the file
Select-String -Path $file.FullName -Pattern $pattern -AllMatches | ForEach-Object {
foreach ($match in $_.Matches) {
"{0}:{1}:{2}" -f $file.FullName, $_.LineNumber, $match.Value
}
}
}`
findstr := exec.Command(utilName, "-Command", psScript)
findstr.Env = append(os.Environ(), "PATTERN="+pattern, "FILEPATH="+location)
outputBytes, err = findstr.Output()

} else if runtime.GOOS == "darwin" {
cmd := fmt.Sprintf(
`find %v -type f | \
while read file; do perl -ne '/(%v)/ && print "$ARGV:$.:$1\n";' "$file"; done`,
location, pattern,
)
findstr := exec.Command("/bin/sh", "-c", cmd)
outputBytes, err = findstr.Output()
} else {
grep := exec.Command("grep", "-o", "-n", "-R", "-P", pattern)
if ok, paths := providerContext.GetScopedFilepaths(); ok {
grep.Args = append(grep.Args, paths...)
} else {
grep.Args = append(grep.Args, location)
}
outputBytes, err = grep.Output()
}
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() == 1 {
return nil, nil
}
return nil, fmt.Errorf("could not run '%s' with provided pattern %+v", utilName, err)
}

return outputBytes, nil
}

0 comments on commit 80d7fb0

Please sign in to comment.