Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 1 | // This app provides a simple scripted environment for running common veyron |
| 2 | // services as subprocesses and testing interactions between them. It is |
| 3 | // structured as an interpreter, with global variables and variable |
| 4 | // expansion, but no control flow. The command set that it supports is |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 5 | // extendable by adding new 'commands' that implement the API defined |
| 6 | // by veyron/lib/modules. |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 7 | package main |
| 8 | |
| 9 | import ( |
| 10 | "bufio" |
| 11 | "flag" |
| 12 | "fmt" |
| 13 | "os" |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 14 | "strconv" |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 15 | "strings" |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 16 | "time" |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 17 | "unicode" |
| 18 | |
Jiri Simsa | 764efb7 | 2014-12-25 20:57:03 -0800 | [diff] [blame] | 19 | "v.io/core/veyron2" |
Matt Rosencrantz | d599e38 | 2015-01-12 11:13:32 -0800 | [diff] [blame] | 20 | "v.io/core/veyron2/context" |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 21 | |
Jiri Simsa | 764efb7 | 2014-12-25 20:57:03 -0800 | [diff] [blame] | 22 | "v.io/core/veyron/lib/expect" |
| 23 | "v.io/core/veyron/lib/modules" |
| 24 | _ "v.io/core/veyron/lib/modules/core" |
| 25 | _ "v.io/core/veyron/profiles" |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 26 | ) |
| 27 | |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 28 | type cmdState struct { |
| 29 | modules.Handle |
| 30 | *expect.Session |
| 31 | line string |
| 32 | } |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 33 | |
| 34 | var ( |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 35 | interactive bool |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 36 | handles map[string]*cmdState |
Cosmos Nicolaou | b44f239 | 2014-10-28 22:41:49 -0700 | [diff] [blame] | 37 | jsonDict map[string]string |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 38 | ) |
| 39 | |
| 40 | func init() { |
| 41 | flag.BoolVar(&interactive, "interactive", true, "set interactive/batch mode") |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 42 | handles = make(map[string]*cmdState) |
Cosmos Nicolaou | b44f239 | 2014-10-28 22:41:49 -0700 | [diff] [blame] | 43 | jsonDict = make(map[string]string) |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 44 | flag.Usage = usage |
| 45 | } |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 46 | |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 47 | var usage = func() { |
| 48 | fmt.Println( |
| 49 | `Welcome to this simple shell that lets you run mount tables, a simple server |
| 50 | and sundry other commands from an interactive command line or as scripts. Type |
| 51 | 'help' at the prompt to see a list of available commands, or 'help command' to |
| 52 | get specific help about that command. The shell provides environment variables |
| 53 | with expansion and intrinsic support for managing subprocess, but it does not |
| 54 | provide any flow control commands. |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 55 | |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 56 | All commands, except builtin ones (such as help, set, eval etc) are run |
| 57 | asynchronously in background. That is, the prompt returns as soon as they are |
| 58 | started and no output is displayed from them unless an error is encountered |
| 59 | when they are being started. Each input line is numbered and that number is |
| 60 | used to refer to the standard output of previous started commands. The variable |
| 61 | _ always contains the number of the immediately preceeding line. It is |
| 62 | possible to read the output of a command (using the 'read' builtin) and assign |
| 63 | it that output to an environment variable. The 'eval' builtin parses output of |
| 64 | the form <var>=<val>. In this way subproccess may be started, their output |
| 65 | read and used to configure subsequent subprocesses. For example: |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 66 | |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 67 | 1> time |
| 68 | 2> read 1 t |
| 69 | 3> print $t |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 70 | |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 71 | will print the first line of output from the time command, as will the |
| 72 | following: |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 73 | |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 74 | or: |
| 75 | time |
| 76 | read $_ t |
| 77 | print $t |
| 78 | |
| 79 | The eval builtin is used to directly to assign to variables specified |
| 80 | in the output of the command. For example, if the root command |
| 81 | prints out MT_NAME=foo then eval will set MT_NAME to foo as follows: |
| 82 | |
| 83 | root |
| 84 | eval $_ |
| 85 | print $MT_NAME |
| 86 | |
| 87 | will print the value of MT_NAME that is output by the root command. |
| 88 | `) |
| 89 | flag.PrintDefaults() |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 90 | } |
| 91 | |
| 92 | func prompt(lineno int) { |
| 93 | if interactive { |
| 94 | fmt.Printf("%d> ", lineno) |
| 95 | } |
| 96 | } |
| 97 | |
Matt Rosencrantz | d599e38 | 2015-01-12 11:13:32 -0800 | [diff] [blame] | 98 | var ctx *context.T |
Matt Rosencrantz | c13446b | 2014-12-03 10:37:00 -0800 | [diff] [blame] | 99 | |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 100 | func main() { |
Suharsh Sivakumar | 19fbf99 | 2015-01-23 11:02:27 -0800 | [diff] [blame] | 101 | var shutdown veyron2.Shutdown |
| 102 | ctx, shutdown = veyron2.Init() |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 103 | |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 104 | // Subprocesses commands are run by fork/execing this binary |
| 105 | // so we must test to see if this instance is a subprocess or the |
| 106 | // the original command line instance. |
Cosmos Nicolaou | d4f0056 | 2014-11-17 20:35:48 -0800 | [diff] [blame] | 107 | if modules.IsModulesProcess() { |
Suharsh Sivakumar | 19fbf99 | 2015-01-23 11:02:27 -0800 | [diff] [blame] | 108 | shutdown() |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 109 | // Subprocess, run the requested command. |
| 110 | if err := modules.Dispatch(); err != nil { |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 111 | fmt.Fprintf(os.Stderr, "failed: %v\n", err) |
Cosmos Nicolaou | 4e213d7 | 2014-10-26 22:21:52 -0700 | [diff] [blame] | 112 | os.Exit(1) |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 113 | } |
| 114 | return |
| 115 | } |
Suharsh Sivakumar | 19fbf99 | 2015-01-23 11:02:27 -0800 | [diff] [blame] | 116 | defer shutdown() |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 117 | |
Ryan Brown | a08a221 | 2015-01-15 15:40:10 -0800 | [diff] [blame] | 118 | shell, err := modules.NewShell(ctx, nil) |
Cosmos Nicolaou | 344cc4a | 2014-11-26 15:38:43 -0800 | [diff] [blame] | 119 | if err != nil { |
| 120 | fmt.Fprintf(os.Stderr, "unexpected error: %s\n", err) |
| 121 | os.Exit(1) |
| 122 | } |
Cosmos Nicolaou | bbae388 | 2014-10-02 22:58:19 -0700 | [diff] [blame] | 123 | defer shell.Cleanup(os.Stderr, os.Stderr) |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 124 | |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 125 | scanner := bufio.NewScanner(os.Stdin) |
| 126 | lineno := 1 |
| 127 | prompt(lineno) |
| 128 | for scanner.Scan() { |
| 129 | line := scanner.Text() |
| 130 | if !strings.HasPrefix(line, "#") && len(line) > 0 { |
Cosmos Nicolaou | 3574f12 | 2014-06-11 23:07:15 -0700 | [diff] [blame] | 131 | if line == "eof" { |
| 132 | break |
| 133 | } |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 134 | if err := process(shell, line, lineno); err != nil { |
| 135 | fmt.Printf("ERROR: %d> %q: %v\n", lineno, line, err) |
Cosmos Nicolaou | b44f239 | 2014-10-28 22:41:49 -0700 | [diff] [blame] | 136 | if !interactive { |
| 137 | os.Exit(1) |
| 138 | } |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 139 | } |
| 140 | } |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 141 | shell.SetVar("_", strconv.Itoa(lineno)) |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 142 | lineno++ |
| 143 | prompt(lineno) |
| 144 | } |
| 145 | if err := scanner.Err(); err != nil { |
| 146 | fmt.Printf("error reading input: %v\n", err) |
| 147 | } |
| 148 | |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 149 | } |
| 150 | |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 151 | func output(lineno int, line string) { |
| 152 | if len(line) > 0 { |
| 153 | if !interactive { |
| 154 | fmt.Printf("%d> ", lineno) |
| 155 | } |
| 156 | line = strings.TrimSuffix(line, "\n") |
| 157 | fmt.Printf("%s\n", line) |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | func process(sh *modules.Shell, line string, lineno int) error { |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 162 | fields, err := splitQuotedFields(line) |
| 163 | if err != nil { |
| 164 | return err |
| 165 | } |
| 166 | if len(fields) == 0 { |
| 167 | return fmt.Errorf("no input") |
| 168 | } |
| 169 | name := fields[0] |
| 170 | |
| 171 | var args []string |
| 172 | if len(fields) > 1 { |
| 173 | args = fields[1:] |
| 174 | } else { |
| 175 | args = []string{} |
| 176 | } |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 177 | sub, err := subVariables(sh, args) |
Cosmos Nicolaou | 4e213d7 | 2014-10-26 22:21:52 -0700 | [diff] [blame] | 178 | if err != nil { |
| 179 | return err |
| 180 | } |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 181 | if cmd := builtins[name]; cmd != nil { |
| 182 | if cmd.nargs >= 0 && len(sub) != cmd.nargs { |
| 183 | return fmt.Errorf("wrong (%d) # args for %q: usage %s", len(sub), name, cmd.usage) |
| 184 | } |
| 185 | l := "" |
| 186 | var err error |
| 187 | if cmd.needsHandle { |
| 188 | l, err = handleWrapper(sh, cmd.fn, sub...) |
| 189 | } else { |
| 190 | l, err = cmd.fn(sh, nil, sub...) |
| 191 | } |
| 192 | if err != nil { |
Cosmos Nicolaou | 4e213d7 | 2014-10-26 22:21:52 -0700 | [diff] [blame] | 193 | return fmt.Errorf("%s : %s", err, l) |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 194 | } |
| 195 | output(lineno, l) |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 196 | } else { |
Cosmos Nicolaou | 612ad38 | 2014-10-29 19:41:35 -0700 | [diff] [blame] | 197 | handle, err := sh.Start(name, nil, sub...) |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 198 | if err != nil { |
| 199 | return err |
| 200 | } |
| 201 | handles[strconv.Itoa(lineno)] = &cmdState{ |
| 202 | handle, |
| 203 | expect.NewSession(nil, handle.Stdout(), time.Minute), |
| 204 | line, |
| 205 | } |
Cosmos Nicolaou | b44f239 | 2014-10-28 22:41:49 -0700 | [diff] [blame] | 206 | output(lineno, line) |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 207 | } |
| 208 | return nil |
| 209 | } |
| 210 | |
| 211 | // splitQuotedFields a line into fields, allowing for quoted strings. |
| 212 | func splitQuotedFields(line string) ([]string, error) { |
| 213 | fields := []string{} |
| 214 | inquote := false |
| 215 | var field []rune |
| 216 | for _, c := range line { |
| 217 | switch { |
| 218 | case c == '"': |
| 219 | if inquote { |
| 220 | fields = append(fields, string(field)) |
| 221 | field = nil |
| 222 | inquote = false |
| 223 | } else { |
| 224 | inquote = true |
| 225 | } |
| 226 | case unicode.IsSpace(c): |
| 227 | if inquote { |
| 228 | field = append(field, c) |
| 229 | } else { |
| 230 | if len(field) > 0 { |
| 231 | fields = append(fields, string(field)) |
| 232 | } |
| 233 | field = nil |
| 234 | } |
| 235 | default: |
| 236 | field = append(field, c) |
| 237 | } |
| 238 | } |
| 239 | if inquote { |
| 240 | return nil, fmt.Errorf("unterminated quoted input") |
| 241 | } |
| 242 | |
| 243 | if len(field) > 0 { |
| 244 | fields = append(fields, string(field)) |
| 245 | } |
| 246 | return fields, nil |
| 247 | } |
| 248 | |
| 249 | // subVariables substitutes variables that occur in the string slice |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 250 | // args with values from the Shell. |
| 251 | func subVariables(sh *modules.Shell, args []string) ([]string, error) { |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 252 | var results []string |
| 253 | for _, a := range args { |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 254 | if r, err := subVariablesInArgument(sh, a); err != nil { |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 255 | return results, err |
| 256 | } else { |
| 257 | results = append(results, r) |
| 258 | } |
| 259 | } |
| 260 | return results, nil |
| 261 | } |
| 262 | |
| 263 | // subVariablesInArgument substitutes variables that occur in the string |
| 264 | // parameter with values from vars. |
| 265 | // |
| 266 | // A variable, is introduced by $, terminated by \t, space, / , : or !. |
| 267 | // Variables may also be enclosed by {} (as in ${VAR}) to allow for embedding |
| 268 | // within strings. |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 269 | func subVariablesInArgument(sh *modules.Shell, a string) (string, error) { |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 270 | first := strings.Index(a, "$") |
| 271 | if first < 0 { |
| 272 | return a, nil |
| 273 | } |
| 274 | parts := strings.Split(a, "$") |
| 275 | result := parts[0] |
| 276 | vn := "" |
| 277 | rem := 0 |
| 278 | for _, p := range parts[1:] { |
| 279 | start := 0 |
| 280 | end := -1 |
| 281 | if strings.HasPrefix(p, "{") { |
| 282 | start = 1 |
| 283 | end = strings.Index(p, "}") |
| 284 | if end < 0 { |
| 285 | return "", fmt.Errorf("unterminated variable: %q", p) |
| 286 | } |
| 287 | rem = end + 1 |
| 288 | } else { |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 289 | end = strings.IndexAny(p, "\t/,:!= ") |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 290 | if end < 0 { |
| 291 | end = len(p) |
| 292 | } |
| 293 | rem = end |
| 294 | } |
| 295 | vn = p[start:end] |
| 296 | r := p[rem:] |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 297 | v, present := sh.GetVar(vn) |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 298 | if !present { |
Matt Rosencrantz | 0dd7ce0 | 2014-09-23 11:34:26 -0700 | [diff] [blame] | 299 | return a, fmt.Errorf("unknown variable: %q", vn) |
Cosmos Nicolaou | 7c659ac | 2014-06-09 22:47:04 -0700 | [diff] [blame] | 300 | } |
| 301 | result += v |
| 302 | result += r |
| 303 | } |
| 304 | return result, nil |
| 305 | } |