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() {}