github.com/bazelbuild/bazel-watcher@v0.25.2/internal/ibazel/output_runner/output_runner.go (about)

     1  // Copyright 2017 The Bazel Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package output_runner
    16  
    17  import (
    18  	"bufio"
    19  	"bytes"
    20  	"encoding/json"
    21  	"flag"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"os"
    25  	"path/filepath"
    26  	"regexp"
    27  	"strconv"
    28  	"strings"
    29  
    30  	"github.com/bazelbuild/bazel-watcher/internal/ibazel/log"
    31  	"github.com/bazelbuild/bazel-watcher/internal/ibazel/workspace"
    32  	"github.com/bazelbuild/bazel-watcher/third_party/bazel/master/src/main/protobuf/blaze_query"
    33  )
    34  
    35  var (
    36  	runOutput = flag.Bool(
    37  		"run_output",
    38  		true,
    39  		"Search for commands in Bazel output that match a regex and execute them, the default path of file should be in the workspace root .bazel_fix_commands.json")
    40  	runOutputInteractive = flag.Bool(
    41  		"run_output_interactive",
    42  		true,
    43  		"Use an interactive prompt when executing commands in Bazel output")
    44  	notifiedUser = false
    45  )
    46  
    47  // This RegExp will match ANSI escape codes.
    48  var escapeCodeCleanerRegex = regexp.MustCompile("\\x1B\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x7E]")
    49  
    50  type OutputRunner struct {
    51  	w workspace.Workspace
    52  }
    53  
    54  type Optcmd struct {
    55  	Regex   string   `json:"regex"`
    56  	Command string   `json:"command"`
    57  	Args    []string `json:"args"`
    58  }
    59  
    60  func New() *OutputRunner {
    61  	i := &OutputRunner{
    62  		w: &workspace.MainWorkspace{},
    63  	}
    64  	return i
    65  }
    66  
    67  func (i *OutputRunner) Initialize(info *map[string]string) {}
    68  
    69  func (i *OutputRunner) TargetDecider(rule *blaze_query.Rule) {}
    70  
    71  func (i *OutputRunner) ChangeDetected(targets []string, changeType string, change string) {}
    72  
    73  func (i *OutputRunner) BeforeCommand(targets []string, command string) {}
    74  
    75  func (i *OutputRunner) AfterCommand(targets []string, command string, success bool, output *bytes.Buffer) {
    76  	if *runOutput == false || output == nil {
    77  		return
    78  	}
    79  
    80  	jsonCommandPath := ".bazel_fix_commands.json"
    81  	defaultRegex := Optcmd{
    82  		Regex:   "^buildozer '(.*)'\\s+(.*)$",
    83  		Command: "buildozer",
    84  		Args:    []string{"$1", "$2"},
    85  	}
    86  
    87  	optcmd := i.readConfigs(jsonCommandPath)
    88  	if optcmd == nil {
    89  		optcmd = []Optcmd{defaultRegex}
    90  	}
    91  	commandLines, commands, args := matchRegex(optcmd, output)
    92  	for idx, _ := range commandLines {
    93  		if *runOutputInteractive {
    94  			if i.promptCommand(commands[idx], args[idx]) {
    95  				i.executeCommand(commands[idx], args[idx])
    96  			}
    97  		} else {
    98  			i.executeCommand(commands[idx], args[idx])
    99  		}
   100  	}
   101  }
   102  
   103  func (o *OutputRunner) readConfigs(configPath string) []Optcmd {
   104  	workspacePath, err := o.w.FindWorkspace()
   105  	if err != nil {
   106  		log.Fatalf("Error finding workspace: %v", err)
   107  		os.Exit(5)
   108  	}
   109  
   110  	jsonFile, err := os.Open(filepath.Join(workspacePath, configPath))
   111  	if os.IsNotExist(err) {
   112  		// Note this is not attached to the os.IsNotExist because we don't want the
   113  		// other error handler to catch if we hav already notified.
   114  		if !notifiedUser {
   115  			log.Banner(
   116  				"Did you know iBazel can invoke programs like Gazelle, buildozer, and",
   117  				"other BUILD file generators for you automatically based on bazel output?",
   118  				"Documentation at: https://github.com/bazelbuild/bazel-watcher#output-runner")
   119  		}
   120  		notifiedUser = true
   121  		return nil
   122  	} else if err != nil {
   123  		log.Errorf("Error reading config: %s", err)
   124  		return nil
   125  	}
   126  	defer jsonFile.Close()
   127  
   128  	byteValue, _ := ioutil.ReadAll(jsonFile)
   129  	var optcmd []Optcmd
   130  	err = json.Unmarshal(byteValue, &optcmd)
   131  	if err != nil {
   132  		log.Errorf("Error in .bazel_fix_commands.json: %s", err)
   133  	}
   134  
   135  	return optcmd
   136  }
   137  
   138  func matchRegex(optcmd []Optcmd, output *bytes.Buffer) ([]string, []string, [][]string) {
   139  	var commandLines, commands []string
   140  	var args [][]string
   141  	distinctCommands := map[string]bool{}
   142  	scanner := bufio.NewScanner(output)
   143  	for scanner.Scan() {
   144  		line := escapeCodeCleanerRegex.ReplaceAllLiteralString(scanner.Text(), "")
   145  		for _, oc := range optcmd {
   146  			re := regexp.MustCompile(oc.Regex)
   147  			matches := re.FindStringSubmatch(line)
   148  			if matches != nil && len(matches) >= 0 {
   149  				command := convertArg(matches, oc.Command)
   150  				cmdArgs := convertArgs(matches, oc.Args)
   151  				fullCmd := strings.Join(append([]string{command}, cmdArgs...), " ")
   152  				if _, found := distinctCommands[fullCmd]; !found {
   153  					commandLines = append(commandLines, matches[0])
   154  					commands = append(commands, command)
   155  					args = append(args, cmdArgs)
   156  					distinctCommands[fullCmd] = true
   157  				}
   158  			}
   159  		}
   160  	}
   161  	return commandLines, commands, args
   162  }
   163  
   164  func convertArg(matches []string, arg string) string {
   165  	if strings.HasPrefix(arg, "$") {
   166  		val, _ := strconv.Atoi(arg[1:])
   167  		return matches[val]
   168  	}
   169  	return arg
   170  }
   171  
   172  func convertArgs(matches []string, args []string) []string {
   173  	var rst []string
   174  	for i, _ := range args {
   175  		var converted strings.Builder
   176  		converted.Grow(len(args[i]))
   177  
   178  		matchIndex := 0
   179  		matching := false
   180  
   181  		writeMatch := func() {
   182  			if matching {
   183  				if matchIndex < len(matches) {
   184  					converted.WriteString(matches[matchIndex])
   185  				} else {
   186  					converted.WriteRune('$')
   187  					converted.WriteString(strconv.Itoa(matchIndex))
   188  				}
   189  				matchIndex = 0
   190  				matching = false
   191  			}
   192  		}
   193  
   194  		for _, c := range args[i] {
   195  			if c == '$' {
   196  				if matching {
   197  					converted.WriteRune(c)
   198  				}
   199  				matching = !matching
   200  			} else if matching && c >= '0' && c <= '9' {
   201  				matchIndex = matchIndex*10 + int(c-'0')
   202  			} else {
   203  				writeMatch()
   204  				converted.WriteRune(c)
   205  			}
   206  		}
   207  		writeMatch()
   208  
   209  		rst = append(rst, converted.String())
   210  	}
   211  	return rst
   212  }
   213  
   214  func (_ *OutputRunner) promptCommand(command string, args []string) bool {
   215  	reader := bufio.NewReader(os.Stdin)
   216  	fmt.Fprintf(os.Stderr, "Do you want to execute this command?\n%s %s\n[y/N]", command, strings.Join(args, " "))
   217  	text, _ := reader.ReadString('\n')
   218  	text = strings.ToLower(text)
   219  	text = strings.TrimSpace(text)
   220  	text = strings.TrimRight(text, "\n")
   221  	if text == "y" {
   222  		return true
   223  	} else {
   224  		return false
   225  	}
   226  }
   227  
   228  func (o *OutputRunner) executeCommand(command string, args []string) {
   229  	o.w.ExecuteCommand(command, args)
   230  }
   231  
   232  func (i *OutputRunner) Cleanup() {}