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