go.fuchsia.dev/jiri@v0.0.0-20240502161911-b66513b29486/cmd/jiri/grep.go (about)

     1  // Copyright 2016 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  
    12  	"go.fuchsia.dev/jiri"
    13  	"go.fuchsia.dev/jiri/cmdline"
    14  	"go.fuchsia.dev/jiri/gitutil"
    15  	"go.fuchsia.dev/jiri/project"
    16  )
    17  
    18  var cmdGrep = &cmdline.Command{
    19  	Runner: jiri.RunnerFunc(runGrep),
    20  	Name:   "grep",
    21  	Short:  "Search across projects.",
    22  	Long: `
    23  Run git grep across all projects.
    24  `,
    25  	ArgsName: "<query> [--] [<pathspec>...]",
    26  }
    27  
    28  var grepFlags struct {
    29  	cwdRel bool
    30  	n      bool
    31  	h      bool
    32  	i      bool
    33  	e      string
    34  	l      bool
    35  	L      bool
    36  	w      bool
    37  }
    38  
    39  func init() {
    40  	flags := &cmdGrep.Flags
    41  	flags.BoolVar(&grepFlags.n, "n", false, "Prefix the line number to matching lines")
    42  	flags.StringVar(&grepFlags.e, "e", "", "The next parameter is the pattern. This option has to be used for patterns starting with -")
    43  	flags.BoolVar(&grepFlags.h, "H", true, "Does nothing. Just makes this git grep compatible")
    44  	flags.BoolVar(&grepFlags.i, "i", false, "Ignore case differences between the patterns and the files")
    45  	flags.BoolVar(&grepFlags.l, "l", false, "Instead of showing every matched line, show only the names of files that contain matches")
    46  	flags.BoolVar(&grepFlags.w, "w", false, "Match the pattern only at word boundary")
    47  	flags.BoolVar(&grepFlags.l, "name-only", false, "same as -l")
    48  	flags.BoolVar(&grepFlags.l, "files-with-matches", false, "same as -l")
    49  	flags.BoolVar(&grepFlags.L, "L", false, "Instead of showing every matched line, show only the names of files that do not contain matches")
    50  	flags.BoolVar(&grepFlags.L, "files-without-match", false, "same as -L")
    51  	flags.BoolVar(&grepFlags.cwdRel, "cwd-rel", false, "Output paths relative to the current working directory (if available)")
    52  }
    53  
    54  func buildFlags() []string {
    55  	var args []string
    56  	if grepFlags.n {
    57  		args = append(args, "-n")
    58  	}
    59  	if grepFlags.e != "" {
    60  		args = append(args, "-e", grepFlags.e)
    61  	}
    62  	if grepFlags.i {
    63  		args = append(args, "-i")
    64  	}
    65  	if grepFlags.l {
    66  		args = append(args, "-l")
    67  	}
    68  	if grepFlags.L {
    69  		args = append(args, "-L")
    70  	}
    71  	if grepFlags.w {
    72  		args = append(args, "-w")
    73  	}
    74  	return args
    75  }
    76  
    77  func doGrep(jirix *jiri.X, args []string) ([]string, error) {
    78  	var pathSpecs []string
    79  	lenArgs := len(args)
    80  	if lenArgs > 0 {
    81  		for i, a := range os.Args {
    82  			if a == "--" {
    83  				pathSpecs = os.Args[i+1:]
    84  				break
    85  			}
    86  		}
    87  		// we will not find -- if user uses something like jiri grep -- a b,
    88  		// as flag.Parse() removes '--' in that case, so set args length
    89  		lenArgs = len(args) - len(pathSpecs)
    90  		for i, a := range args {
    91  
    92  			if a == "--" {
    93  				args = args[0:i]
    94  				// reset length
    95  				lenArgs = len(args)
    96  				break
    97  			}
    98  		}
    99  	}
   100  
   101  	if grepFlags.e != "" && lenArgs > 0 {
   102  		return nil, jirix.UsageErrorf("No additional argument allowed with flag -e")
   103  	} else if grepFlags.e == "" && lenArgs != 1 {
   104  		return nil, jirix.UsageErrorf("grep requires one argument")
   105  	}
   106  
   107  	projects, err := project.LocalProjects(jirix, project.FastScan)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	// TODO(ianloic): run in parallel rather than serially.
   113  	// TODO(ianloic): only run grep on projects under the cwd.
   114  	var results []string
   115  	flags := buildFlags()
   116  	if jirix.Color.Enabled() {
   117  		flags = append(flags, "--color=always")
   118  	}
   119  	query := ""
   120  	if lenArgs == 1 {
   121  		query = args[0]
   122  	}
   123  
   124  	cwd := jirix.Root
   125  	if grepFlags.cwdRel {
   126  		if wd, err := os.Getwd(); err == nil {
   127  			cwd = wd
   128  		}
   129  	}
   130  
   131  	for _, project := range projects {
   132  		relpath, err := filepath.Rel(cwd, project.Path)
   133  		if err != nil {
   134  			return nil, err
   135  		}
   136  		git := gitutil.New(jirix, gitutil.RootDirOpt(project.Path))
   137  		lines, err := git.Grep(query, pathSpecs, flags...)
   138  		if err != nil {
   139  			continue
   140  		}
   141  		for _, line := range lines {
   142  			// TODO(ianloic): higlight the project path part like `repo grep`.
   143  			results = append(results, relpath+"/"+line)
   144  		}
   145  	}
   146  
   147  	// TODO(ianloic): fail if all of the sub-greps fail
   148  	return results, nil
   149  }
   150  
   151  func runGrep(jirix *jiri.X, args []string) error {
   152  	lines, err := doGrep(jirix, args)
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	for _, line := range lines {
   158  		fmt.Println(line)
   159  	}
   160  	return nil
   161  }