├── .gitignore ├── LICENSE ├── README.md ├── cmd.go ├── example └── main.go ├── go.mod ├── go.sum ├── internal └── internal.go └── plugins ├── README.md ├── controlflow └── controlflow.go ├── json ├── README.md └── json.go └── stats └── stats.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) {{year}} {{fullname}} 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cmd 2 | === 3 | 4 | A library to create shell-like command processors, slightly inspired by the Python cmd/cmd2 package 5 | 6 | ## Installation 7 | $ go get github.com/gobs/cmd 8 | 9 | ## Documentation 10 | http://godoc.org/github.com/gobs/cmd 11 | 12 | ## Example 13 | 14 | import "github.com/gobs/cmd" 15 | 16 | // return true to stop command loop 17 | func Exit(line string) (stop bool) { 18 | fmt.Println("goodbye!") 19 | return true 20 | } 21 | 22 | // change the prompt 23 | func (cmd *Cmd) SetPrompt(line string) (stop bool) { 24 | cmd.Prompt = line 25 | return 26 | } 27 | 28 | // initialize Cmd structure 29 | commander := &cmd.Cmd{Prompt: "> ",} 30 | commander.Init() 31 | 32 | // add inline method 33 | commander.Add(cmd.Command{ 34 | "ls", 35 | `list stuff`, 36 | func(line string) (stop bool) { 37 | fmt.Println("listing stuff") 38 | return 39 | }}) 40 | 41 | // add another command 42 | commander.Add(cmd.Command{ 43 | Name: "prompt", 44 | Help: `Set prompt`, 45 | Call: commander.SetPrompt 46 | }) 47 | 48 | // and one more 49 | commander.Add(cmd.Command{ 50 | "exit", 51 | `terminate example`, 52 | Exit 53 | }) 54 | 55 | // start command loop 56 | commander.CmdLoop() 57 | 58 | ## Available commands 59 | 60 | The command processor predefines a few useful commands, including function definitions and conditionals. 61 | 62 | Use the `help` command to see the list of available commands. 63 | 64 | Function definition is similart to bash functions: 65 | 66 | function test { 67 | echo Function name: $0 68 | echo Number of arguments: $# 69 | echo First argument: $1 70 | echo All arguments: $* 71 | } 72 | 73 | but you can also define a very short function (one-liner): 74 | 75 | function oneliner echo "very short function" 76 | 77 | Variables can be set/listed using the `var` command: 78 | 79 | > var catch 22 80 | 81 | > var catch 82 | catch: 22 83 | 84 | > echo $catch 85 | 22 86 | 87 | To unset/remove a variable use: 88 | 89 | > var -r catch 90 | > var -rm catch 91 | > var --remove catch 92 | 93 | note that currently only "string" values are supported (i.e. `var x 1` is the same as `var x "1"1) 94 | 95 | Conditional flow with `if` and `else` commands: 96 | 97 | if (condition) { 98 | # true path 99 | } else { 100 | # false path 101 | } 102 | 103 | The `else` block is optional: 104 | 105 | if (condition) { 106 | # only the truth 107 | } 108 | 109 | And the short test: 110 | 111 | if (condition) echo "yes!" 112 | 113 | ## Conditions: 114 | 115 | The simplest condition is the "non empty argument": 116 | 117 | if true echo "yes, it's true" 118 | 119 | But if you are using a variable you need to quote it: 120 | 121 | if "$nonempty" echo "nonempty is not empty" 122 | 123 | All other conditionals are in the form: `(cond arguments...)`: 124 | 125 | (z $var) # $var is empty 126 | 127 | (n $var) # $var is not empty 128 | 129 | (t $var) # $var is true (true, 1, not-empty var) 130 | 131 | (f $var) # $var is false (false, 0, empty empty) 132 | 133 | (eq $var val) # $var == val 134 | 135 | (ne $var val) # $var != val 136 | 137 | (gt $var val) # $var > val 138 | 139 | (gte $var val) # $var >= val 140 | 141 | (lt $var val) # $var < val 142 | 143 | (lte $var val) # $var <= val 144 | 145 | (startswith $var val) # $var starts with val 146 | (endswith $var val) # $var ends with val 147 | (contains $var val) # $var contains val 148 | 149 | Conditions can also be negated with the "!" operator: 150 | 151 | if !true echo "not true" 152 | if !(contains $var val) echo val not in $var 153 | 154 | As for variables, for now only string comparisons are supported. 155 | -------------------------------------------------------------------------------- /cmd.go: -------------------------------------------------------------------------------- 1 | /* 2 | This package is used to implement a "line oriented command interpreter", inspired by the python package with 3 | the same name http://docs.python.org/2/library/cmd.html 4 | 5 | Usage: 6 | 7 | commander := &Cmd{...} 8 | commander.Init() 9 | 10 | commander.Add(Command{...}) 11 | commander.Add(Command{...}) 12 | 13 | commander.CmdLoop() 14 | */ 15 | package cmd 16 | 17 | import ( 18 | "github.com/alitto/pond" 19 | "github.com/gobs/args" 20 | "github.com/gobs/cmd/internal" 21 | "github.com/gobs/pretty" 22 | "golang.org/x/sync/errgroup" 23 | 24 | "fmt" 25 | "io" 26 | "os" 27 | "os/exec" 28 | "os/signal" 29 | "regexp" 30 | "sort" 31 | "strconv" 32 | "strings" 33 | "sync" 34 | "syscall" 35 | "time" 36 | 37 | "log" 38 | ) 39 | 40 | var ( 41 | reArg = regexp.MustCompile(`\$(\w+|\(\w+\)|\(env.\w+\)|[\*#]|\([\*#]\))`) // $var or $(var) 42 | reVarAssign = regexp.MustCompile(`([\d\w]+)(=(.*))?`) // name=value 43 | sep = string(0xFFFD) // unicode replacement char 44 | 45 | // NoVar is passed to Command.OnChange to indicate that the variable is not set or needs to be deleted 46 | NoVar = &struct{}{} 47 | ) 48 | 49 | type arguments = map[string]string 50 | 51 | // This is used to describe a new command 52 | type Command struct { 53 | // command name 54 | Name string 55 | // command description 56 | Help string 57 | // the function to call to execute the command 58 | Call func(string) bool 59 | // the function to call to print the help string 60 | HelpFunc func() 61 | } 62 | 63 | func (c *Command) DefaultHelp() { 64 | if len(c.Help) > 0 { 65 | fmt.Println(c.Help) 66 | } else { 67 | fmt.Println("No help for ", c.Name) 68 | } 69 | } 70 | 71 | type Completer interface { 72 | Complete(string, string) []string // Complete(start, full-line) returns matches 73 | } 74 | 75 | type linkedCompleter struct { 76 | name string 77 | completer Completer 78 | next *linkedCompleter 79 | } 80 | 81 | type CompleterWords func() []string 82 | type CompleterCond func(start, line string) bool 83 | 84 | // The context for command completion 85 | type WordCompleter struct { 86 | // a function that returns the list of words to match on 87 | Words CompleterWords 88 | // a function that returns true if this completer should be executed 89 | Cond CompleterCond 90 | } 91 | 92 | func (c *WordCompleter) Complete(start, line string) (matches []string) { 93 | if c.Cond != nil && c.Cond(start, line) == false { 94 | return 95 | } 96 | 97 | for _, w := range c.Words() { 98 | if strings.HasPrefix(w, start) { 99 | matches = append(matches, w) 100 | } 101 | } 102 | 103 | return 104 | } 105 | 106 | // Create a WordCompleter and initialize with list of words 107 | func NewWordCompleter(words CompleterWords, cond CompleterCond) *WordCompleter { 108 | return &WordCompleter{Words: words, Cond: cond} 109 | } 110 | 111 | // 112 | // A "context" for the "go" command 113 | // 114 | 115 | type GoRunner interface { 116 | Run(task func()) 117 | Wait() 118 | } 119 | 120 | // This is a runner where you can wait for completions of all tasks 121 | // with the option to specify the maximum number of tasks to run in parallel. 122 | type groupRunner struct { 123 | waitGroup errgroup.Group 124 | } 125 | 126 | func GroupRunner(workers int) GoRunner { 127 | b := &groupRunner{} 128 | if workers >= 0 { 129 | b.waitGroup.SetLimit(workers) 130 | } 131 | 132 | return b 133 | } 134 | 135 | func (r *groupRunner) Run(task func()) { 136 | r.waitGroup.Go(func() error { 137 | task() 138 | return nil 139 | }) 140 | } 141 | 142 | func (r *groupRunner) Wait() { 143 | log.Println("wait") 144 | r.waitGroup.Wait() 145 | } 146 | 147 | // 148 | // This is a runner based on a goroutine pool. 149 | // 150 | 151 | type pooled struct { 152 | pool *pond.WorkerPool 153 | } 154 | 155 | func PoolRunner(workers, capacity int, options ...pond.Option) GoRunner { 156 | return &pooled{pool: pond.New(workers, capacity, options...)} 157 | } 158 | 159 | func (r *pooled) Run(task func()) { 160 | r.pool.Submit(task) 161 | } 162 | 163 | func (r *pooled) Wait() { 164 | r.pool.StopAndWait() 165 | } 166 | 167 | // This the the "context" for the command interpreter 168 | type Cmd struct { 169 | // the prompt string 170 | Prompt string 171 | 172 | // the continuation prompt string 173 | ContinuationPrompt string 174 | 175 | // the history file 176 | HistoryFile string 177 | 178 | // this function is called to fetch the current prompt 179 | // so it can be overridden to provide a dynamic prompt 180 | GetPrompt func(bool) string 181 | 182 | // this function is called before starting the command loop 183 | PreLoop func() 184 | 185 | // this function is called before terminating the command loop 186 | PostLoop func() 187 | 188 | // this function is called before executing the selected command 189 | PreCmd func(string) 190 | 191 | // this function is called after a command has been executed 192 | // return true to terminate the interpreter, false to continue 193 | PostCmd func(string, bool) bool 194 | 195 | // this function is called to execute one command 196 | OneCmd func(string) bool 197 | 198 | // this function is called if the last typed command was an empty line 199 | EmptyLine func() 200 | 201 | // this function is called if the command line doesn't match any existing command 202 | // by default it displays an error message 203 | Default func(string) 204 | 205 | // this function is called when the user types the "help" command. 206 | // It is implemented so that it can be overwritten, mainly to support plugins. 207 | Help func(string) bool 208 | 209 | // this function is called to implement command completion. 210 | // it should return a list of words that match the input text 211 | Complete func(string, string) []string 212 | 213 | // this function is called when a variable change (via set/var command). 214 | // it should return the new value to set the variable to (to force type casting) 215 | // 216 | // oldv will be nil if a new varabile is being created 217 | // 218 | // newv will be nil if the variable is being deleted 219 | OnChange func(name string, oldv, newv interface{}) interface{} 220 | 221 | // this function is called when the user tries to interrupt a running 222 | // command. If it returns true, the application will be terminated. 223 | Interrupt func(os.Signal) bool 224 | 225 | // this function is called when recovering from a panic. 226 | // If it returns true, the application will be terminated. 227 | Recover func(interface{}) bool 228 | 229 | // if true, enable shell commands 230 | EnableShell bool 231 | 232 | // if true, print elapsed time 233 | Timing bool 234 | 235 | // if true, print command before executing 236 | Echo bool 237 | 238 | // if true, don't print result of some operations (stored in result variables) 239 | Silent bool 240 | 241 | // if true, a Ctrl-C should return an error 242 | // CtrlCAborts bool 243 | 244 | // this is the list of available commands indexed by command name 245 | Commands map[string]Command 246 | 247 | ///////// private stuff ///////////// 248 | completers *linkedCompleter 249 | 250 | commandNames []string 251 | commandCompleter *WordCompleter 252 | functionCompleter *WordCompleter 253 | 254 | runner GoRunner 255 | 256 | interrupted bool 257 | context *internal.Context 258 | stdout *os.File // original stdout 259 | sync.RWMutex 260 | } 261 | 262 | // Initialize the command interpreter context 263 | func (cmd *Cmd) Init(plugins ...Plugin) { 264 | if cmd.GetPrompt == nil { 265 | cmd.GetPrompt = func(cont bool) string { 266 | if cont { 267 | return cmd.ContinuationPrompt 268 | } 269 | 270 | return cmd.Prompt 271 | } 272 | } 273 | if cmd.PreLoop == nil { 274 | cmd.PreLoop = func() {} 275 | } 276 | if cmd.PostLoop == nil { 277 | cmd.PostLoop = func() {} 278 | } 279 | if cmd.PreCmd == nil { 280 | cmd.PreCmd = func(string) {} 281 | } 282 | if cmd.PostCmd == nil { 283 | cmd.PostCmd = func(line string, stop bool) bool { return stop } 284 | } 285 | if cmd.OneCmd == nil { 286 | cmd.OneCmd = cmd.oneCmd 287 | } 288 | if cmd.EmptyLine == nil { 289 | cmd.EmptyLine = func() {} 290 | } 291 | if cmd.Default == nil { 292 | cmd.Default = func(line string) { fmt.Printf("invalid command: %v\n", line) } 293 | } 294 | if cmd.OnChange == nil { 295 | cmd.OnChange = func(name string, oldv, newv interface{}) interface{} { return newv } 296 | } 297 | if cmd.Interrupt == nil { 298 | cmd.Interrupt = func(sig os.Signal) bool { return true } 299 | } 300 | if cmd.Recover == nil { 301 | cmd.Recover = func(r interface{}) bool { return true } 302 | } 303 | if cmd.Help == nil { 304 | cmd.Help = cmd.help 305 | } 306 | cmd.context = internal.NewContext() 307 | cmd.context.PushScope(nil, nil) 308 | 309 | cmd.stdout = os.Stdout 310 | 311 | cmd.Commands = make(map[string]Command) 312 | cmd.Add(Command{"help", `list available commands`, func(line string) bool { 313 | return cmd.Help(line) 314 | }, nil}) 315 | cmd.Add(Command{"echo", `echo input line`, cmd.command_echo, nil}) 316 | cmd.Add(Command{"go", `go cmd: asynchronous execution of cmd, or 'go [--start [n]|--pool [w [cap]]|--wait]'`, 317 | cmd.command_go, nil}) 318 | cmd.Add(Command{"time", `time [starttime]`, cmd.command_time, nil}) 319 | cmd.Add(Command{"output", `output [filename|--]`, cmd.command_output, nil}) 320 | cmd.Add(Command{"exit", `exit program`, cmd.command_exit, nil}) 321 | 322 | for _, p := range plugins { 323 | if err := p.PluginInit(cmd, cmd.context); err != nil { 324 | panic("plugin initialization failed: " + err.Error()) 325 | } 326 | } 327 | 328 | cmd.SetVar("echo", cmd.Echo) 329 | cmd.SetVar("print", !cmd.Silent) 330 | cmd.SetVar("timing", cmd.Timing) 331 | } 332 | 333 | func (cmd *Cmd) setInterrupted(interrupted bool) { 334 | cmd.Lock() 335 | cmd.interrupted = interrupted 336 | cmd.Unlock() 337 | } 338 | 339 | func (cmd *Cmd) Interrupted() (interrupted bool) { 340 | cmd.RLock() 341 | interrupted = cmd.interrupted 342 | cmd.RUnlock() 343 | return 344 | } 345 | 346 | // Plugin is the interface implemented by plugins 347 | type Plugin interface { 348 | PluginInit(cmd *Cmd, ctx *internal.Context) error 349 | } 350 | 351 | func (cmd *Cmd) SetPrompt(prompt string, max int) { 352 | l := len(prompt) 353 | 354 | if max > 3 && l > max { 355 | max -= 3 // for "..." 356 | prompt = "..." + prompt[l-max:] 357 | } 358 | 359 | cmd.Prompt = prompt 360 | } 361 | 362 | // Update function completer (when function list changes) 363 | func (cmd *Cmd) updateCompleters() { 364 | if c := cmd.GetCompleter(""); c == nil { // default completer 365 | cmd.commandNames = make([]string, 0, len(cmd.Commands)) 366 | for name := range cmd.Commands { 367 | cmd.commandNames = append(cmd.commandNames, name) 368 | } 369 | sort.Strings(cmd.commandNames) // for help listing 370 | 371 | cmd.AddCompleter("", NewWordCompleter(func() []string { 372 | return cmd.commandNames 373 | }, func(s, l string) bool { 374 | return s == l // check if we are at the beginning of the line 375 | })) 376 | 377 | cmd.AddCompleter("help", NewWordCompleter(func() []string { 378 | return cmd.commandNames 379 | }, func(s, l string) bool { 380 | return strings.HasPrefix(l, "help ") 381 | })) 382 | } 383 | } 384 | 385 | func (cmd *Cmd) wordCompleter(line string, pos int) (head string, completions []string, tail string) { 386 | start := strings.LastIndex(line[:pos], " ") 387 | 388 | for c := cmd.completers; c != nil; c = c.next { 389 | if completions = c.completer.Complete(line[start+1:], line); completions != nil { 390 | return line[:start+1], completions, line[pos:] 391 | } 392 | } 393 | 394 | if cmd.Complete != nil { 395 | return line[:start+1], cmd.Complete(line[start+1:], line), line[pos:] 396 | } 397 | 398 | return 399 | } 400 | 401 | func (cmd *Cmd) AddCompleter(name string, c Completer) { 402 | lc := &linkedCompleter{name: name, completer: c, next: cmd.completers} 403 | cmd.completers = lc 404 | } 405 | 406 | func (cmd *Cmd) GetCompleter(name string) Completer { 407 | for c := cmd.completers; c != nil; c = c.next { 408 | if c.name == name { 409 | return c.completer 410 | } 411 | } 412 | 413 | return nil 414 | } 415 | 416 | // execute shell command 417 | func shellExec(command string) { 418 | args := args.GetArgs(command) 419 | if len(args) < 1 { 420 | fmt.Println("No command to exec") 421 | } else { 422 | if strings.ContainsAny(command, "$*~") { 423 | if _, err := exec.LookPath("sh"); err == nil { 424 | args = []string{"sh", "-c", command} 425 | } 426 | } 427 | cmd := exec.Command(args[0]) 428 | cmd.Args = args 429 | cmd.Stdout = os.Stdout 430 | cmd.Stderr = os.Stderr 431 | 432 | if err := cmd.Run(); err != nil { 433 | fmt.Println(err) 434 | } 435 | } 436 | } 437 | 438 | // execute shell command and pipe input and/or output 439 | func pipeExec(command string) *os.File { 440 | args := args.GetArgs(command) 441 | if len(args) < 1 { 442 | fmt.Println("No command to exec") 443 | } else { 444 | if strings.ContainsAny(command, "$*~") { 445 | if _, err := exec.LookPath("sh"); err == nil { 446 | args = []string{"sh", "-c", command} 447 | } 448 | } 449 | cmd := exec.Command(args[0]) 450 | cmd.Args = args 451 | cmd.Stdout = os.Stdout 452 | cmd.Stderr = os.Stderr 453 | 454 | pr, pw, err := os.Pipe() 455 | if err != nil { 456 | fmt.Println("cannot create pipe:", err) 457 | return nil 458 | } 459 | 460 | cmd.Stdin = pr 461 | 462 | go func() { 463 | if err := cmd.Run(); err != nil { 464 | fmt.Println(err) 465 | } 466 | }() 467 | 468 | return pw 469 | } 470 | 471 | return nil 472 | } 473 | 474 | // Add a command to the command interpreter. 475 | // Overrides a command with the same name, if there was one 476 | func (cmd *Cmd) Add(command Command) { 477 | if command.HelpFunc == nil { 478 | command.HelpFunc = command.DefaultHelp 479 | } 480 | 481 | cmd.Commands[command.Name] = command 482 | } 483 | 484 | // Default help command. 485 | // It lists all available commands or it displays the help for the specified command 486 | func (cmd *Cmd) help(line string) (stop bool) { 487 | fmt.Println("") 488 | 489 | if line == "--all" { 490 | fmt.Println("Available commands (use 'help '):") 491 | fmt.Println("================================================================") 492 | for _, c := range cmd.commandNames { 493 | fmt.Printf("%v: ", c) 494 | cmd.Commands[c].HelpFunc() 495 | } 496 | } else if len(line) == 0 { 497 | fmt.Println("Available commands (use 'help '):") 498 | fmt.Println("================================================================") 499 | 500 | max := 0 501 | 502 | for _, c := range cmd.commandNames { 503 | if len(c) > max { 504 | max = len(c) 505 | } 506 | } 507 | 508 | tp := pretty.NewTabPrinter(80 / (max + 1)) 509 | tp.TabWidth(max + 1) 510 | 511 | for _, c := range cmd.commandNames { 512 | tp.Print(c) 513 | } 514 | tp.Println() 515 | } else if c, ok := cmd.Commands[line]; ok { 516 | c.HelpFunc() 517 | } else { 518 | fmt.Println("unknown command or function") 519 | } 520 | 521 | fmt.Println("") 522 | return 523 | } 524 | 525 | func (cmd *Cmd) command_echo(line string) (stop bool) { 526 | if strings.HasPrefix(line, "-n ") { 527 | fmt.Print(strings.TrimSpace(line[3:])) 528 | } else { 529 | fmt.Println(line) 530 | } 531 | return 532 | } 533 | 534 | func (cmd *Cmd) command_go(line string) (stop bool) { 535 | if strings.HasPrefix(line, "-") { 536 | // should be --start, --pool or --wait 537 | 538 | args := args.ParseArgs(line) 539 | 540 | if v, ok := args.Options["start"]; ok { 541 | max := -1 542 | 543 | if v != "" { 544 | max, _ = strconv.Atoi(v) 545 | } 546 | 547 | if len(args.Arguments) > 0 { 548 | max, _ = strconv.Atoi(args.Arguments[0]) 549 | } 550 | 551 | fmt.Println("start with", max, "workers") 552 | cmd.runner = GroupRunner(max) 553 | } else if v, ok := args.Options["pool"]; ok { 554 | pmax := 1 555 | pcap := 10 556 | 557 | if v != "" { 558 | pmax, _ = strconv.Atoi(v) 559 | } 560 | 561 | if len(args.Arguments) > 0 { 562 | pmax, _ = strconv.Atoi(args.Arguments[0]) 563 | } 564 | 565 | if len(args.Arguments) > 1 { 566 | pcap, _ = strconv.Atoi(args.Arguments[1]) 567 | } else if pcap < pmax { 568 | pcap = pmax 569 | } 570 | 571 | fmt.Println("pool with", pmax, "workers", pcap, "capacity") 572 | cmd.runner = PoolRunner(pmax, pcap, pond.PanicHandler(func(p any) { 573 | panic(p) 574 | })) 575 | } else if _, ok := args.Options["wait"]; ok { 576 | if cmd.runner == nil { 577 | fmt.Println("nothing to wait on") 578 | } else { 579 | cmd.runner.Wait() 580 | cmd.runner = nil 581 | } 582 | } else { 583 | fmt.Println("invalid option") 584 | } 585 | 586 | return 587 | } 588 | 589 | if strings.HasPrefix(line, "go ") { 590 | fmt.Println("Don't go go me!") 591 | } else if cmd.runner == nil { 592 | go cmd.OneCmd(line) 593 | } else { 594 | cmd.runner.Run(func() { 595 | fmt.Println("RUN", line) 596 | cmd.OneCmd(line) 597 | }) 598 | } 599 | 600 | return 601 | } 602 | 603 | func (cmd *Cmd) command_time(line string) (stop bool) { 604 | if line == "-m" || line == "--milli" || line == "--millis" { 605 | t := time.Now().UnixNano() / int64(time.Millisecond) 606 | if !cmd.SilentResult() { 607 | fmt.Println(t) 608 | } 609 | 610 | cmd.SetVar("time", t) 611 | } else if line == "" { 612 | t := time.Now().Format(time.RFC3339) 613 | if !cmd.SilentResult() { 614 | fmt.Println(t) 615 | } 616 | 617 | cmd.SetVar("time", t) 618 | } else { 619 | if t, err := time.Parse(time.RFC3339, line); err != nil { 620 | fmt.Println("invalid start time") 621 | } else { 622 | d := time.Since(t).Round(time.Millisecond) 623 | if !cmd.SilentResult() { 624 | fmt.Println(d) 625 | } 626 | cmd.SetVar("elapsed", d.Seconds()) 627 | } 628 | } 629 | 630 | return 631 | } 632 | 633 | func (cmd *Cmd) command_output(line string) (stop bool) { 634 | if line != "" { 635 | if line == "--" { 636 | if cmd.stdout != nil && os.Stdout != cmd.stdout { // default stdout 637 | os.Stdout.Close() 638 | os.Stdout = cmd.stdout 639 | } 640 | } else if strings.HasPrefix(line, "|") { // pipe 641 | line = strings.TrimSpace(line[1:]) 642 | 643 | w := pipeExec(line) 644 | if w == nil { 645 | return 646 | } 647 | 648 | if cmd.stdout == nil { 649 | cmd.stdout = os.Stdout 650 | } else if cmd.stdout != os.Stdout { 651 | os.Stdout.Close() 652 | } 653 | 654 | os.Stdout = w 655 | } else { 656 | f, err := os.Create(line) 657 | if err != nil { 658 | fmt.Fprintln(os.Stderr, err) 659 | return 660 | } 661 | 662 | if cmd.stdout == nil { 663 | cmd.stdout = os.Stdout 664 | } else if cmd.stdout != os.Stdout { 665 | os.Stdout.Close() 666 | } 667 | 668 | os.Stdout = f 669 | } 670 | } 671 | 672 | fmt.Fprintln(os.Stderr, "output:", os.Stdout.Name()) 673 | return 674 | } 675 | 676 | func (cmd *Cmd) command_exit(line string) (stop bool) { 677 | if !cmd.SilentResult() { 678 | fmt.Println("goodbye!") 679 | } 680 | return true 681 | } 682 | 683 | // This method executes one command 684 | func (cmd *Cmd) oneCmd(line string) (stop bool) { 685 | defer func() { 686 | if r := recover(); r != nil { 687 | /* 688 | if !cmd.SilentResult() { 689 | fmt.Println("recovered:", r) 690 | } 691 | */ 692 | 693 | stop = cmd.Recover(r) 694 | } 695 | }() 696 | 697 | if cmd.GetBoolVar("timing") { 698 | start := time.Now() 699 | defer func() { 700 | d := time.Since(start).Truncate(time.Millisecond) 701 | cmd.SetVar("elapsed", d.Seconds()) 702 | 703 | if !cmd.SilentResult() { 704 | fmt.Println("Elapsed:", d) 705 | } 706 | }() 707 | } 708 | 709 | if cmd.GetBoolVar("echo") { 710 | fmt.Println(cmd.GetPrompt(false), line) 711 | } 712 | 713 | if cmd.EnableShell && strings.HasPrefix(line, "!") { 714 | shellExec(line[1:]) 715 | return 716 | } 717 | 718 | var cname, params string 719 | 720 | parts := strings.SplitN(line, " ", 2) 721 | 722 | cname = parts[0] 723 | if len(parts) > 1 { 724 | params = strings.TrimSpace(parts[1]) 725 | } 726 | 727 | if command, ok := cmd.Commands[cname]; ok { 728 | stop = command.Call(params) 729 | } else { 730 | cmd.Default(line) 731 | } 732 | 733 | return 734 | } 735 | 736 | // This is the command interpreter entry point. 737 | // It displays a prompt, waits for a command and executes it until the selected command returns true 738 | func (cmd *Cmd) CmdLoop() { 739 | if len(cmd.Prompt) == 0 { 740 | cmd.Prompt = "> " 741 | } 742 | if len(cmd.ContinuationPrompt) == 0 { 743 | cmd.ContinuationPrompt = ": " 744 | } 745 | 746 | cmd.context.StartLiner(cmd.HistoryFile) 747 | cmd.context.SetWordCompleter(cmd.wordCompleter) 748 | 749 | cmd.updateCompleters() 750 | cmd.PreLoop() 751 | 752 | defer func() { 753 | cmd.context.StopLiner() 754 | cmd.PostLoop() 755 | 756 | if os.Stdout != cmd.stdout { 757 | os.Stdout.Close() 758 | os.Stdout = cmd.stdout 759 | } 760 | }() 761 | 762 | sigc := make(chan os.Signal, 1) 763 | signal.Notify(sigc, os.Interrupt, syscall.SIGTERM) 764 | 765 | go func() { 766 | for sig := range sigc { 767 | cmd.setInterrupted(true) 768 | cmd.context.ResetTerminal() 769 | 770 | if cmd.Interrupt(sig) { 771 | // rethrow signal to kill app 772 | signal.Stop(sigc) 773 | p, _ := os.FindProcess(os.Getpid()) 774 | p.Signal(sig) 775 | } else { 776 | //signal.Notify(sigc, os.Interrupt, syscall.SIGTERM) 777 | } 778 | } 779 | }() 780 | 781 | cmd.runLoop(true) 782 | } 783 | 784 | func (cmd *Cmd) runLoop(mainLoop bool) (stop bool) { 785 | // loop until ReadLine returns nil (signalling EOF) 786 | for { 787 | line, err := cmd.context.ReadLine(cmd.GetPrompt(false), cmd.GetPrompt(true)) 788 | if err != nil { 789 | if err != io.EOF { 790 | fmt.Println(err) 791 | } 792 | break 793 | } 794 | 795 | if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") { 796 | cmd.EmptyLine() 797 | continue 798 | } 799 | 800 | if mainLoop { 801 | cmd.setInterrupted(false) 802 | cmd.context.UpdateHistory(line) // allow user to recall this line 803 | } 804 | 805 | m, _ := cmd.context.TerminalMode() 806 | //interactive := err == nil 807 | 808 | cmd.PreCmd(line) 809 | stop = cmd.OneCmd(line) 810 | stop = cmd.PostCmd(line, stop) || (mainLoop == false && cmd.Interrupted()) 811 | 812 | cmd.context.RestoreMode(m) 813 | if stop { 814 | break 815 | } 816 | } 817 | 818 | return 819 | } 820 | 821 | // RunBlock runs a block of code. 822 | // 823 | // Note: this is public because it's needed by the ControlFlow plugin (and can't be in interal 824 | // because of circular dependencies). It shouldn't be used by end-user applications. 825 | func (cmd *Cmd) RunBlock(name string, body []string, args []string, newscope bool) (stop bool) { 826 | if args != nil { 827 | args = append([]string{name}, args...) 828 | } 829 | 830 | prev := cmd.context.ScanBlock(body) 831 | if newscope { 832 | cmd.context.PushScope(nil, args) 833 | } 834 | shouldStop := cmd.runLoop(false) 835 | if newscope { 836 | cmd.context.PopScope() 837 | } 838 | cmd.context.SetScanner(prev) 839 | 840 | if name == "" { // if stop is called in an unamed block (i.e. not a function) we should really stop 841 | stop = shouldStop 842 | } 843 | 844 | return 845 | } 846 | 847 | // SetVar sets a variable in the current scope 848 | func (cmd *Cmd) SetVar(k string, v interface{}) { 849 | cmd.context.SetVar(k, v, internal.LocalScope) 850 | } 851 | 852 | // UpdateVar allows to atomically change the valua of a variable. The `update` callback receives the 853 | // current value and should returns the new value. 854 | func (cmd *Cmd) UpdateVar(k string, update func(string) interface{}) string { 855 | return cmd.context.UpdateVar(k, internal.LocalScope, update) 856 | } 857 | 858 | // UnsetVar removes a variable from the current scope 859 | func (cmd *Cmd) UnsetVar(k string) { 860 | cmd.context.UnsetVar(k, internal.LocalScope) 861 | } 862 | 863 | // ChangeVar sets a variable in the current scope 864 | // and calls the OnChange method 865 | func (cmd *Cmd) ChangeVar(k string, v interface{}) { 866 | var oldv interface{} = NoVar 867 | if cur, ok := cmd.context.GetVar(k); ok { 868 | oldv = cur 869 | } 870 | if newv := cmd.OnChange(k, oldv, v); newv == NoVar { 871 | cmd.context.UnsetVar(k, internal.LocalScope) 872 | } else { 873 | cmd.context.SetVar(k, newv, internal.LocalScope) 874 | } 875 | } 876 | 877 | // GetVar return the value of the specified variable from the closest scope 878 | func (cmd *Cmd) GetVar(k string) (string, bool) { 879 | return cmd.context.GetVar(k) 880 | } 881 | 882 | // GetBoolVar returns the value of the variable as boolean 883 | func (cmd *Cmd) GetBoolVar(name string) (val bool) { 884 | sval, _ := cmd.context.GetVar(name) 885 | val, _ = strconv.ParseBool(sval) 886 | return 887 | } 888 | 889 | // GetIntVar returns the value of the variable as int 890 | func (cmd *Cmd) GetIntVar(name string) (val int) { 891 | sval, _ := cmd.context.GetVar(name) 892 | val, _ = strconv.Atoi(sval) 893 | return 894 | } 895 | 896 | // SilentResult returns true if the command should be silent 897 | // (not print results to the console, but only store in return variable) 898 | func (cmd *Cmd) SilentResult() bool { 899 | return cmd.GetBoolVar("print") == false 900 | } 901 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gobs/args" 5 | "github.com/gobs/cmd" 6 | "github.com/gobs/cmd/plugins/controlflow" 7 | "github.com/gobs/cmd/plugins/json" 8 | "github.com/gobs/cmd/plugins/stats" 9 | 10 | "fmt" 11 | "os" 12 | //"strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | var ( 18 | words = []string{"one", "two", "three", "four"} 19 | ) 20 | 21 | func CompletionFunction(text, line string) (matches []string) { 22 | // for the "ls" command we let readline show real file names 23 | if strings.HasPrefix(line, "ls ") { 24 | return 25 | } 26 | 27 | // for all other commands, we pick from our list of completion words 28 | for _, w := range words { 29 | if strings.HasPrefix(w, text) { 30 | matches = append(matches, w) 31 | } 32 | } 33 | 34 | return 35 | } 36 | 37 | func OnChange(name string, oldv, newv interface{}) interface{} { 38 | switch name { 39 | case "immutable": 40 | if newv == cmd.NoVar { 41 | fmt.Println("cannot delete me") 42 | } else { 43 | fmt.Println("cannot change me") 44 | } 45 | 46 | return oldv 47 | 48 | case "boolvalue": 49 | if newv != cmd.NoVar { 50 | switch newv.(string) { 51 | case "0", "false", "False", "off", "OFF": 52 | newv = false 53 | 54 | default: 55 | newv = true 56 | } 57 | } 58 | } 59 | 60 | fmt.Println("change", name, "from", oldv, "to", newv) 61 | return newv 62 | } 63 | 64 | func OnInterrupt(sig os.Signal) (quit bool) { 65 | fmt.Println("got", sig) 66 | return 67 | } 68 | 69 | var recoverQuit = false 70 | 71 | func OnRecover(r interface{}) bool { 72 | fmt.Println("recovering from", r) 73 | return recoverQuit 74 | } 75 | 76 | func main() { 77 | commander := &cmd.Cmd{ 78 | HistoryFile: ".rlhistory", 79 | Complete: CompletionFunction, 80 | OnChange: OnChange, 81 | Interrupt: OnInterrupt, 82 | Recover: OnRecover, 83 | EnableShell: true, 84 | } 85 | 86 | commander.GetPrompt = func(cont bool) string { 87 | if cont { 88 | return commander.ContinuationPrompt 89 | } 90 | 91 | return strings.ReplaceAll(commander.Prompt, "%T", time.Now().Format("2006-01-02 03:04:05")) 92 | } 93 | 94 | commander.Init(controlflow.Plugin, json.Plugin, stats.Plugin) 95 | 96 | /* 97 | commander.Vars = map[string]string{ 98 | "user": "Bob", 99 | "cwd": "/right/here", 100 | "ret": "42", 101 | } 102 | */ 103 | 104 | commander.Add(cmd.Command{ 105 | "ls", 106 | `list stuff`, 107 | func(line string) (stop bool) { 108 | fmt.Println("listing stuff") 109 | return 110 | }, 111 | nil}) 112 | 113 | /* 114 | commander.Add(cmd.Command{ 115 | "sleep", 116 | `sleep for a while`, 117 | func(line string) (stop bool) { 118 | s := time.Second 119 | 120 | if t, err := strconv.Atoi(line); err == nil { 121 | s *= time.Duration(t) 122 | } 123 | 124 | fmt.Println("sleeping...") 125 | time.Sleep(s) 126 | return 127 | }, 128 | nil, 129 | }) 130 | */ 131 | 132 | commander.Add(cmd.Command{ 133 | Name: ">", 134 | Help: `Set prompt`, 135 | Call: func(line string) (stop bool) { 136 | // commander.Prompt = line // set prompt 137 | commander.SetPrompt(line, 20) // set prompt with max length of 20 138 | return 139 | }}) 140 | 141 | commander.Add(cmd.Command{ 142 | Name: "timing", 143 | Help: `Enable timing`, 144 | Call: func(line string) (stop bool) { 145 | line = strings.ToLower(line) 146 | commander.Timing = line == "true" || line == "yes" || line == "1" || line == "on" 147 | return 148 | }}) 149 | 150 | commander.Add(cmd.Command{ 151 | Name: "args", 152 | Help: "parse args", 153 | Call: func(line string) (stop bool) { 154 | fmt.Printf("%q\n", args.GetArgs(line)) 155 | return 156 | }}) 157 | 158 | commander.Add(cmd.Command{ 159 | Name: "panic", 160 | Help: "panic [-q] message: panic (and test recover)", 161 | Call: func(line string) (stop bool) { 162 | if line == "-q" || strings.HasPrefix(line, "-q ") { 163 | recoverQuit = true 164 | line = strings.TrimSpace(line[2:]) 165 | } 166 | 167 | panic(line) 168 | return 169 | }}) 170 | 171 | if len(os.Args) > 1 { 172 | cmd := strings.Join(os.Args[1:], " ") 173 | if commander.OneCmd(cmd) { 174 | return 175 | } 176 | } 177 | 178 | commander.CmdLoop() 179 | 180 | } 181 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gobs/cmd 2 | 3 | go 1.21.3 4 | 5 | require ( 6 | github.com/alitto/pond v1.8.3 7 | github.com/gobs/args v0.0.0-20210311043657-b8c0b223be93 8 | github.com/gobs/jsonpath v1.0.0 9 | github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b 10 | github.com/gobs/simplejson v0.0.0-20181106204727-c70e6bd5e26b 11 | github.com/gobs/sortedmap v1.0.0 12 | github.com/montanaflynn/stats v0.7.0 13 | github.com/peterh/liner v1.2.2 14 | golang.org/x/sync v0.1.0 15 | ) 16 | 17 | require ( 18 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 19 | github.com/kr/text v0.2.0 // indirect 20 | github.com/mattn/go-runewidth v0.0.3 // indirect 21 | github.com/rogpeppe/go-internal v1.11.0 // indirect 22 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect 23 | golang.org/x/sys v0.1.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alitto/pond v1.8.3 h1:ydIqygCLVPqIX/USe5EaV/aSRXTRXDEI9JwuDdu+/xs= 2 | github.com/alitto/pond v1.8.3/go.mod h1:CmvIIGd5jKLasGI3D87qDkQxjzChdKMmnXMg3fG6M6Q= 3 | github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= 4 | github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= 5 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 6 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/gobs/args v0.0.0-20210311043657-b8c0b223be93 h1:70jFzur8/dg4E5NKFMOPLAxk4wSyGm3vQ+7PuBEoHzE= 9 | github.com/gobs/args v0.0.0-20210311043657-b8c0b223be93/go.mod h1:ZpqkpUmnBz2Jz7hMGSPRbHtYC82FP/IZ1Y7A2riYH0s= 10 | github.com/gobs/jsonpath v1.0.0 h1:8Zrmj957KrrpXJnDHlCLOS8PjRPbc+4bjEeaO83yLOs= 11 | github.com/gobs/jsonpath v1.0.0/go.mod h1:tAu2blpXVbJ1wmMkDqolYSsXCcRo0r/e2nuBQugz6K0= 12 | github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= 13 | github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= 14 | github.com/gobs/simplejson v0.0.0-20181106204727-c70e6bd5e26b h1:R6nfOvxM/FSBpF6uOitK+RslvoNZ5kUIdOC3HOfiInI= 15 | github.com/gobs/simplejson v0.0.0-20181106204727-c70e6bd5e26b/go.mod h1:I5K8pVtjLb3st/ifOHRR6S5Z8RS2qj8fUtM0SLndj8Y= 16 | github.com/gobs/sortedmap v1.0.0 h1:/Mi6smdHqt0XGsr/5HzGttoy/mXjuJq6ssIhENkeNz4= 17 | github.com/gobs/sortedmap v1.0.0/go.mod h1:G24cnpMlxl9YJB04q7se7A2FkoJV4X3iWHU8zb32mnY= 18 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 19 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 20 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 21 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 22 | github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= 23 | github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 24 | github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= 25 | github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 26 | github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= 27 | github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= 28 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 29 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 30 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= 31 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= 32 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 33 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 34 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 36 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | -------------------------------------------------------------------------------- /internal/internal.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | 14 | "github.com/peterh/liner" 15 | ) 16 | 17 | type Arguments = map[string]string 18 | 19 | type Dict = map[string]interface{} 20 | type List = []interface{} 21 | 22 | type Scope int 23 | 24 | const ( 25 | InvalidScope Scope = iota 26 | LocalScope 27 | ParentScope 28 | GlobalScope 29 | ) 30 | 31 | func (s Scope) String() string { 32 | switch s { 33 | case LocalScope: 34 | return "local" 35 | case ParentScope: 36 | return "parent" 37 | case GlobalScope: 38 | return "global" 39 | default: 40 | return "invalid scope" 41 | } 42 | } 43 | 44 | type Context struct { 45 | line *liner.State // interactive line reader 46 | scanner BasicScanner // file based line reader 47 | 48 | historyFile string 49 | hasHistory bool 50 | scopes []Arguments 51 | 52 | sync.Mutex 53 | } 54 | 55 | func NewContext() *Context { 56 | return &Context{} 57 | } 58 | 59 | func (ctx *Context) StartLiner(history string) { 60 | ctx.Lock() 61 | ctx.line = liner.NewLiner() 62 | ctx.readHistoryFile(history) 63 | ctx.Unlock() 64 | ctx.ScanLiner() 65 | } 66 | 67 | func (ctx *Context) StopLiner() { 68 | ctx.Lock() 69 | defer ctx.Unlock() 70 | 71 | if ctx.line != nil { 72 | ctx.writeHistoryFile() 73 | ctx.line.Close() 74 | } 75 | } 76 | 77 | func (ctx *Context) UpdateHistory(line string) { 78 | ctx.Lock() 79 | defer ctx.Unlock() 80 | 81 | if ctx.line != nil { 82 | ctx.line.AppendHistory(line) 83 | ctx.hasHistory = true 84 | } 85 | } 86 | 87 | func (ctx *Context) SetWordCompleter(completer func(line string, pos int) (head string, completions []string, tail string)) { 88 | if ctx.line != nil { 89 | ctx.line.SetWordCompleter(completer) 90 | } 91 | } 92 | 93 | func (ctx *Context) readHistoryFile(history string) { 94 | if len(history) == 0 { 95 | // no history file 96 | return 97 | } 98 | 99 | filepath := history // start with current directory 100 | if f, err := os.Open(filepath); err == nil { 101 | ctx.line.ReadHistory(f) 102 | f.Close() 103 | 104 | ctx.historyFile = filepath 105 | return 106 | } 107 | 108 | filepath = path.Join(os.Getenv("HOME"), filepath) // then check home directory 109 | if f, err := os.Open(filepath); err == nil { 110 | ctx.line.ReadHistory(f) 111 | f.Close() 112 | 113 | ctx.historyFile = filepath 114 | return 115 | } 116 | 117 | if f, err := os.Create(filepath); err == nil { // if we can create the history file, set the path 118 | // create history file 119 | f.Close() 120 | 121 | ctx.historyFile = filepath 122 | } 123 | } 124 | 125 | func (ctx *Context) writeHistoryFile() { 126 | if len(ctx.historyFile) == 0 || !ctx.hasHistory { 127 | // no history file or no changes 128 | return 129 | } 130 | 131 | if f, err := os.Create(ctx.historyFile); err == nil { 132 | ctx.line.WriteHistory(f) 133 | f.Close() 134 | } 135 | } 136 | 137 | // PushScope pushes a new scope for variables, with the associated dvalues 138 | func (ctx *Context) PushScope(vars map[string]string, args []string) { 139 | ctx.Lock() 140 | defer ctx.Unlock() 141 | 142 | scope := make(Arguments) 143 | 144 | for k, v := range vars { 145 | scope[k] = v 146 | } 147 | 148 | for i, v := range args { 149 | k := strconv.Itoa(i) 150 | scope[k] = v 151 | } 152 | 153 | if args != nil { 154 | scope["*"] = strings.Join(args[1:], " ") // all args 155 | scope["#"] = strconv.Itoa(len(args[1:])) // args[0] is the function name 156 | } 157 | 158 | ctx.scopes = append(ctx.scopes, scope) 159 | } 160 | 161 | // PopScope removes the current scope, restoring the previous one 162 | func (ctx *Context) PopScope() { 163 | ctx.Lock() 164 | defer ctx.Unlock() 165 | 166 | l := len(ctx.scopes) 167 | if l == 0 { 168 | panic("no scopes") 169 | } 170 | 171 | ctx.scopes = ctx.scopes[:l-1] 172 | } 173 | 174 | // GetScope returns the variable sets for the specified scope 175 | func (ctx *Context) GetScope(scope Scope) Arguments { 176 | ctx.Lock() 177 | defer ctx.Unlock() 178 | 179 | i := len(ctx.scopes) - 1 // index of local scope 180 | if i < 0 { 181 | panic("no scopes") 182 | } 183 | 184 | switch scope { 185 | case GlobalScope: 186 | i = 0 // index of global scope 187 | 188 | case ParentScope: 189 | if i > 0 { 190 | i -= 1 // index of parent scope 191 | } 192 | } 193 | 194 | return ctx.scopes[i] 195 | } 196 | 197 | // SetVar sets a variable in the current, parent or global scope 198 | func (ctx *Context) SetVar(k string, v interface{}, scope Scope) { 199 | ctx.Lock() 200 | defer ctx.Unlock() 201 | 202 | ctx.setVar(k, v, scope) 203 | } 204 | 205 | // UnsetVar removes a variable from the current, parent or global scope 206 | func (ctx *Context) UnsetVar(k string, scope Scope) { 207 | ctx.Lock() 208 | defer ctx.Unlock() 209 | 210 | i := len(ctx.scopes) - 1 // index of local scope 211 | if i < 0 { 212 | panic("no scopes") 213 | } 214 | 215 | switch scope { 216 | case GlobalScope: 217 | i = 0 // index of global scope 218 | 219 | case ParentScope: 220 | if i > 0 { 221 | i -= 1 // index of parent scope 222 | } 223 | } 224 | 225 | if _, ok := ctx.scopes[i][k]; ok { 226 | delete(ctx.scopes[i], k) 227 | } 228 | } 229 | 230 | // GetVar return the value of the specified variable from the closest scope 231 | func (ctx *Context) GetVar(k string) (string, bool) { 232 | ctx.Lock() 233 | defer ctx.Unlock() 234 | 235 | return ctx.getVar(k) 236 | } 237 | 238 | // UpdateVar allows to atomically change the valua of a variable. The `update` callback receives the 239 | // current value and should returns the new value. 240 | func (ctx *Context) UpdateVar(k string, scope Scope, update func(string) interface{}) string { 241 | ctx.Lock() 242 | defer ctx.Unlock() 243 | 244 | current, _ := ctx.getVar(k) 245 | return ctx.setVar(k, update(current), scope) 246 | } 247 | 248 | // getVar return the value of the specified variable from the closest scope 249 | func (ctx *Context) getVar(k string) (string, bool) { 250 | for i := len(ctx.scopes) - 1; i >= 0; i-- { 251 | if v, ok := ctx.scopes[i][k]; ok { 252 | return v, true 253 | } 254 | } 255 | 256 | return "", false 257 | } 258 | 259 | // setVar sets a variable in the current, parent or global scope 260 | func (ctx *Context) setVar(k string, v interface{}, scope Scope) string { 261 | i := len(ctx.scopes) - 1 // index of local scope 262 | if i < 0 { 263 | panic("no scopes") 264 | } 265 | 266 | switch scope { 267 | case GlobalScope: 268 | i = 0 // index of global scope 269 | 270 | case ParentScope: 271 | save := i 272 | 273 | for ; i > 0; i-- { 274 | if _, ok := ctx.scopes[i][k]; ok { // find where the variable is defined 275 | break 276 | } 277 | } 278 | 279 | if i < 0 { 280 | i = save 281 | } 282 | } 283 | 284 | // 285 | // here we should convert complex types to a meaningful 286 | // string representation (i.e. json) 287 | // 288 | 289 | /* 290 | switch t := v.(type) { 291 | case Dict: 292 | ; 293 | 294 | case Array: 295 | ; 296 | } 297 | */ 298 | 299 | ctx.scopes[i][k] = fmt.Sprintf("%v", v) 300 | return ctx.scopes[i][k] 301 | } 302 | 303 | // GetAllVars return a copy of all variables available at the current scope 304 | func (ctx *Context) GetAllVars() (all Arguments) { 305 | ctx.Lock() 306 | defer ctx.Unlock() 307 | 308 | all = Arguments{} 309 | 310 | for _, scope := range ctx.scopes { 311 | for k, v := range scope { 312 | all[k] = v 313 | } 314 | } 315 | 316 | return 317 | } 318 | 319 | // GetAllVars return a copy of all variables available at the current scope 320 | func (ctx *Context) GetVarNames() (names []string) { 321 | for name, _ := range ctx.GetAllVars() { 322 | names = append(names, name) 323 | } 324 | 325 | sort.Strings(names) 326 | return 327 | } 328 | 329 | func (ctx *Context) ShiftArgs(n int) { 330 | vars := ctx.GetScope(LocalScope) 331 | if _, ok := vars["#"]; !ok { 332 | return // no arguments 333 | } 334 | 335 | nargs, _ := strconv.Atoi(vars["#"]) 336 | if n > nargs { 337 | return // don't touch arguments 338 | } 339 | 340 | var args []string 341 | 342 | for i := 1; i < nargs; i++ { 343 | ki := strconv.Itoa(i) 344 | kn := strconv.Itoa(i + n) 345 | 346 | if i+n > nargs { 347 | delete(vars, ki) 348 | } else { 349 | vars[ki] = vars[kn] 350 | args = append(args, vars[kn]) 351 | } 352 | } 353 | 354 | vars["*"] = strings.Join(args, " ") 355 | vars["#"] = strconv.Itoa(len(args)) 356 | } 357 | 358 | // A basic scanner interface 359 | type BasicScanner interface { 360 | Scan(prompt string) bool 361 | Text() string 362 | Err() error 363 | } 364 | 365 | // An implementation of basicScanner that works on a list of lines 366 | type ScanLines struct { 367 | lines []string 368 | } 369 | 370 | func (s *ScanLines) Scan(prompt string) bool { 371 | return len(s.lines) > 0 372 | } 373 | 374 | func (s *ScanLines) Text() (text string) { 375 | if len(s.lines) == 0 { 376 | return 377 | } 378 | 379 | text, s.lines = s.lines[0], s.lines[1:] 380 | return 381 | } 382 | 383 | func (s *ScanLines) Err() (err error) { 384 | return 385 | } 386 | 387 | // An implementation of basicScanner that works with "liner" 388 | type ScanLiner struct { 389 | line *liner.State 390 | text string 391 | err error 392 | } 393 | 394 | func (s *ScanLiner) Scan(prompt string) bool { 395 | s.text, s.err = s.line.Prompt(prompt) 396 | return s.err == nil 397 | } 398 | 399 | func (s *ScanLiner) Text() string { 400 | return s.text 401 | } 402 | 403 | func (s *ScanLiner) Err() error { 404 | return s.err 405 | } 406 | 407 | // An implementation of basicScanner that works with an io.Reader (wrapped in a bufio.Scanner) 408 | type ScanReader struct { 409 | sr *bufio.Scanner 410 | } 411 | 412 | func (s *ScanReader) Scan(prompt string) bool { 413 | return s.sr.Scan() 414 | } 415 | 416 | func (s *ScanReader) Text() string { 417 | return s.sr.Text() 418 | } 419 | 420 | func (s *ScanReader) Err() error { 421 | return s.sr.Err() 422 | } 423 | 424 | // SetScanner sets the current scanner and return the previos one 425 | func (ctx *Context) SetScanner(curr BasicScanner) (prev BasicScanner) { 426 | ctx.Lock() 427 | defer ctx.Unlock() 428 | 429 | prev, ctx.scanner = ctx.scanner, curr 430 | return 431 | } 432 | 433 | // ScanLiner sets the current scanner to a "liner" scanner 434 | func (ctx *Context) ScanLiner() BasicScanner { 435 | return ctx.SetScanner(&ScanLiner{line: ctx.line}) 436 | } 437 | 438 | // ScanBlock sets the current scanner to a block scanner 439 | func (ctx *Context) ScanBlock(block []string) BasicScanner { 440 | return ctx.SetScanner(&ScanLines{lines: block}) 441 | } 442 | 443 | // ScanReader sets the current scanner to an io.Reader scanner 444 | func (ctx *Context) ScanReader(r io.Reader) BasicScanner { 445 | return ctx.SetScanner(&ScanReader{sr: bufio.NewScanner(r)}) 446 | } 447 | 448 | func (ctx *Context) readOneLine(prompt string) (line string, err error) { 449 | ctx.Lock() 450 | scanner := ctx.scanner 451 | ctx.Unlock() 452 | 453 | if scanner == nil { 454 | panic("nil scanner") 455 | } 456 | 457 | if scanner.Scan(prompt) { 458 | line = scanner.Text() 459 | } else if scanner.Err() != nil { 460 | err = scanner.Err() 461 | } else { 462 | err = io.EOF 463 | } 464 | 465 | // fmt.Printf("readOneLine:%v %q %v\n", prompt, line, err) 466 | return 467 | } 468 | 469 | func (ctx *Context) ReadLine(prompt, cont string) (line string, err error) { 470 | line, err = ctx.readOneLine(prompt) 471 | if err != nil { 472 | return 473 | } 474 | 475 | line = strings.TrimSpace(line) 476 | 477 | // 478 | // merge lines ending with '\' into one single line 479 | // 480 | for strings.HasSuffix(line, "\\") { // continuation 481 | line = strings.TrimRight(line, "\\") 482 | line = strings.TrimSpace(line) 483 | 484 | l, err := ctx.readOneLine(cont) 485 | if err != nil { 486 | fmt.Fprintln(os.Stderr, "continuation", err) 487 | break 488 | } 489 | 490 | line += " " + strings.TrimSpace(l) 491 | } 492 | 493 | return 494 | } 495 | 496 | func (ctx *Context) ReadBlock(body, next, cont string) ([]string, []string, error) { 497 | if !strings.HasSuffix(body, "{") { // one line body 498 | body := strings.Replace(body, "\\$", "$", -1) // for one-liners variables should be escaped 499 | return []string{body}, nil, nil 500 | } 501 | 502 | if body != "{" { // we can't do inline command + body 503 | return nil, nil, fmt.Errorf("unexpected body and block") 504 | } 505 | 506 | opened := 1 507 | var block1, block2 []string 508 | var line string 509 | var err error 510 | 511 | for { 512 | 513 | line, err = ctx.ReadLine(cont, cont) 514 | if err != nil { 515 | return nil, nil, err 516 | } 517 | 518 | line = strings.TrimSpace(line) 519 | if strings.HasPrefix(line, "#") || line == "" { 520 | block1 = append(block1, line) 521 | continue 522 | } 523 | 524 | if strings.HasPrefix(line, "}") { 525 | opened -= 1 526 | if opened <= 0 { // close first block 527 | break 528 | } 529 | } 530 | if strings.HasSuffix(line, "{") { 531 | opened += 1 532 | } 533 | 534 | block1 = append(block1, line) 535 | } 536 | 537 | line = strings.TrimPrefix(line, "}") 538 | line = strings.TrimSpace(line) 539 | 540 | if strings.HasPrefix(line, "#") || line == "" { 541 | return block1, nil, nil 542 | } 543 | 544 | if next != "" && !strings.HasPrefix(line, next) { 545 | return nil, nil, fmt.Errorf("expected %q, got %q", next, line) 546 | } 547 | 548 | line = line[len(next):] 549 | line = strings.TrimSpace(line) 550 | 551 | if line != "{" { 552 | return nil, nil, fmt.Errorf("expected }, got %q", line) 553 | } 554 | 555 | opened = 1 556 | 557 | for { 558 | 559 | line, err = ctx.ReadLine(cont, cont) 560 | if err != nil { 561 | return nil, nil, err 562 | } 563 | 564 | line = strings.TrimSpace(line) 565 | if strings.HasPrefix(line, "#") || line == "" { 566 | block2 = append(block2, line) 567 | continue 568 | } 569 | 570 | if strings.HasPrefix(line, "}") { 571 | opened -= 1 572 | if opened <= 0 { // close second block 573 | break 574 | } 575 | } 576 | if strings.HasSuffix(line, "{") { 577 | opened += 1 578 | } 579 | 580 | block2 = append(block2, line) 581 | } 582 | 583 | return block1, block2, nil 584 | } 585 | 586 | func (ctx *Context) ResetTerminal() { 587 | if ctx.line != nil { 588 | ctx.line.Close() 589 | } 590 | } 591 | 592 | func (ctx *Context) TerminalMode() (liner.ModeApplier, error) { 593 | return liner.TerminalMode() 594 | } 595 | 596 | func (ctx *Context) RestoreMode(m liner.ModeApplier) { 597 | if m != nil { 598 | m.ApplyMode() 599 | } 600 | } 601 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | cmd plugins 2 | =========== 3 | 4 | Plugins provide additional commands to cmd. 5 | 6 | ## Available plugins: 7 | - [controlflow](https://github.com/gobs/cmd/tree/master/plugins/controlflow) : provides flow-control related commands 8 | (conditionals, loops, etc.) 9 | - [json](https://github.com/gobs/cmd/tree/master/plugins/json) : provides json related commands 10 | (name/value to json, json format, jsonpath) 11 | - [stats](https://github.com/gobs/cmd/tree/master/plugins/stats) : provides statistics related commands 12 | -------------------------------------------------------------------------------- /plugins/controlflow/controlflow.go: -------------------------------------------------------------------------------- 1 | package controlflow 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math" 7 | "math/rand" 8 | "os" 9 | "regexp" 10 | "sort" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/gobs/args" 17 | "github.com/gobs/cmd" 18 | "github.com/gobs/cmd/internal" 19 | "github.com/gobs/pretty" 20 | "github.com/gobs/simplejson" 21 | "github.com/gobs/sortedmap" 22 | ) 23 | 24 | type controlFlow struct { 25 | cmd.Plugin 26 | 27 | cmd *cmd.Cmd 28 | ctx *internal.Context 29 | 30 | _oneCmd func(string) bool 31 | _help func(string) bool 32 | _interrupt func(os.Signal) bool 33 | 34 | functions map[string][]string 35 | 36 | interruptCount int 37 | inLoop bool 38 | 39 | sync.RWMutex 40 | } 41 | 42 | type loop struct { 43 | start, end, step, Index int64 44 | } 45 | 46 | func newLoop(count int64) (l *loop) { 47 | switch { 48 | case count > 0: 49 | l = &loop{start: 1, end: count, step: 1} 50 | 51 | case count < 0: 52 | l = &loop{start: -count, end: 1, step: -1} 53 | } 54 | 55 | return 56 | } 57 | 58 | func (l *loop) Next() bool { 59 | if l == nil || l.Index == l.end { 60 | return false 61 | } 62 | 63 | if l.Index == 0 { 64 | l.Index = l.start 65 | } else { 66 | l.Index += l.step 67 | } 68 | 69 | return true 70 | } 71 | 72 | func (l *loop) First() bool { 73 | return l.Index == l.start 74 | } 75 | 76 | func (l *loop) Last() bool { 77 | return l.Index == l.end 78 | } 79 | 80 | func (l *loop) Reset() { 81 | l.Index = 0 82 | } 83 | 84 | func parseWait(line string) (wait time.Duration) { 85 | w, err := strconv.Atoi(line) 86 | if err == nil { 87 | wait = time.Duration(w) * time.Second 88 | } else { 89 | wait, _ = time.ParseDuration(line) 90 | } 91 | 92 | return 93 | } 94 | 95 | var ( 96 | Plugin = &controlFlow{} 97 | 98 | reArg = regexp.MustCompile(`\$(\w+|\(\w+\)|\(env.\w+\)|[\*#]|\([\*#]\))`) // $var or $(var) 99 | reVarAssign = regexp.MustCompile(`([\d\w]+)(=(.*))`) // name=value 100 | ) 101 | 102 | func (cf *controlFlow) functionNames() (names []string, max int) { 103 | for name, _ := range cf.functions { 104 | names = append(names, name) 105 | if len(name) > max { 106 | max = len(name) 107 | } 108 | } 109 | sort.Strings(names) 110 | return 111 | } 112 | 113 | func (cf *controlFlow) sleepInterrupted(wait time.Duration) bool { 114 | for ; wait > time.Second; wait -= time.Second { 115 | time.Sleep(time.Second) 116 | if cf.cmd.Interrupted() { 117 | return true 118 | } 119 | } 120 | 121 | if wait > 0 { 122 | time.Sleep(wait) 123 | } 124 | 125 | return cf.cmd.Interrupted() 126 | } 127 | 128 | func (cf *controlFlow) command_function(line string) (stop bool) { 129 | // function 130 | if line == "" { 131 | names, _ := cf.functionNames() 132 | 133 | if len(names) == 0 { 134 | fmt.Println("no functions") 135 | } else { 136 | fmt.Println("functions:") 137 | for _, fn := range names { 138 | fmt.Println(" ", fn) 139 | } 140 | } 141 | return 142 | } 143 | 144 | parts := strings.SplitN(line, " ", 2) 145 | // function name 146 | if len(parts) == 1 { 147 | fn := parts[0] 148 | body, ok := cf.functions[fn] 149 | if !ok { 150 | fmt.Println("no function", fn) 151 | } else { 152 | fmt.Println("function", fn, "{") 153 | for _, l := range body { 154 | fmt.Println(" ", l) 155 | } 156 | fmt.Println("}") 157 | } 158 | return 159 | } 160 | 161 | // function name body 162 | fname, body := parts[0], strings.TrimSpace(parts[1]) 163 | if body == "--delete" { 164 | if _, ok := cf.functions[fname]; ok { 165 | delete(cf.functions, fname) 166 | fmt.Println("function", fname, "deleted") 167 | } else { 168 | fmt.Println("no function", fname) 169 | } 170 | 171 | return 172 | } 173 | 174 | lines, _, err := cf.ctx.ReadBlock(body, "", cf.cmd.ContinuationPrompt) 175 | if err != nil { 176 | fmt.Println(err) 177 | return true 178 | } 179 | 180 | cf.functions[fname] = lines 181 | return 182 | } 183 | 184 | type opType int 185 | 186 | const ( 187 | opNone = iota 188 | opSet 189 | opRemove 190 | opIncr 191 | opDecr 192 | ) 193 | 194 | func (cf *controlFlow) command_variable(aline string) (stop bool) { 195 | options, line := args.GetOptions(aline) 196 | 197 | var scope internal.Scope 198 | var op = opSet 199 | 200 | for _, opt := range options { 201 | switch opt { 202 | case "-g", "--global": 203 | scope = internal.GlobalScope 204 | 205 | case "-p", "--parent", "--return": 206 | scope = internal.ParentScope 207 | 208 | case "-r", "-rm", "--remove", "-u", "--unset": 209 | op = opRemove 210 | 211 | case "-i", "-incr", "--incr": 212 | op = opIncr 213 | 214 | case "-d", "-decr", "--decr": 215 | op = opDecr 216 | 217 | default: 218 | fmt.Printf("invalid option -%v in %q\n", op, aline) 219 | return 220 | } 221 | } 222 | 223 | // var 224 | if len(line) == 0 { 225 | if scope != internal.InvalidScope { 226 | fmt.Printf("invalid use of %v scope option in %q\n", scope, aline) 227 | return 228 | } 229 | 230 | for _, kv := range sortedmap.AsSortedMap(cf.ctx.GetAllVars()) { 231 | fmt.Println(" ", kv) 232 | } 233 | 234 | return 235 | } 236 | 237 | parts := args.GetArgsN(line, 2) // [ name, value ] 238 | if len(parts) == 1 { // see if it's name=value 239 | matches := reVarAssign.FindStringSubmatch(line) 240 | if len(matches) > 0 { // [name=var name =var var] 241 | parts = []string{matches[1], matches[3]} 242 | } 243 | } 244 | 245 | name := parts[0] 246 | 247 | // var name value 248 | if len(parts) == 2 { 249 | if op != opSet { 250 | fmt.Println("invalid option with name and value in %q\n", aline) 251 | return 252 | } 253 | 254 | var oldv interface{} = cmd.NoVar 255 | if cur, ok := cf.ctx.GetVar(name); ok { 256 | oldv = cur 257 | } 258 | 259 | if newv := cf.cmd.OnChange(name, oldv, parts[1]); newv == cmd.NoVar { 260 | cf.ctx.UnsetVar(name, scope) 261 | } else { 262 | cf.ctx.SetVar(name, newv, scope) 263 | } 264 | return 265 | } 266 | 267 | // var -r|-incr|-decr name| 268 | switch op { 269 | case opRemove: 270 | var oldv interface{} = cmd.NoVar 271 | if cur, ok := cf.ctx.GetVar(name); ok { 272 | oldv = cur 273 | } 274 | 275 | if newv := cf.cmd.OnChange(name, oldv, cmd.NoVar); newv == cmd.NoVar { 276 | cf.ctx.UnsetVar(name, scope) 277 | } else { 278 | cf.ctx.SetVar(name, newv, scope) 279 | } 280 | return 281 | 282 | case opIncr: 283 | cf.ctx.UpdateVar(name, scope, func(cur string) interface{} { 284 | v, _ := parseInt(cur) 285 | return v + 1 286 | }) 287 | return 288 | 289 | case opDecr: 290 | cf.ctx.UpdateVar(name, scope, func(cur string) interface{} { 291 | v, _ := parseInt(cur) 292 | return v - 1 293 | }) 294 | return 295 | } 296 | 297 | // var name 298 | if scope != internal.InvalidScope { 299 | fmt.Printf("invalid use of %v scope option in %q\n", scope, aline) 300 | return 301 | } 302 | 303 | value, ok := cf.ctx.GetVar(name) 304 | if ok { 305 | fmt.Println(name, "=", value) 306 | } 307 | return 308 | } 309 | 310 | func (cf *controlFlow) command_shift(line string) (stop bool) { 311 | start := 1 312 | args := args.GetArgs(line) 313 | if len(args) > 1 { 314 | fmt.Println("too many arguments") 315 | return 316 | } 317 | 318 | if len(args) == 1 { 319 | if n, err := parseInt(args[0]); err != nil { 320 | fmt.Println(err) 321 | return 322 | } else { 323 | start = n 324 | } 325 | } 326 | 327 | cf.ctx.ShiftArgs(start) 328 | return 329 | } 330 | 331 | func (cf *controlFlow) expandVariables(line string) string { 332 | line = strings.Replace(line, "$$", "💲", -1) // replace $$ with fat $ 333 | 334 | for { 335 | // fmt.Println("before expand:", line) 336 | found := false 337 | 338 | line = reArg.ReplaceAllStringFunc(line, func(s string) string { 339 | found = true 340 | 341 | // ReplaceAll doesn't return submatches so we need to cleanup 342 | arg := strings.TrimLeft(s, "$(") 343 | arg = strings.TrimRight(arg, ")") 344 | 345 | if strings.HasPrefix(arg, "env.") { 346 | return os.Getenv(arg[4:]) 347 | } 348 | 349 | v, _ := cf.ctx.GetVar(arg) 350 | return v 351 | }) 352 | 353 | // fmt.Println("after expand:", line) 354 | if !found { 355 | break 356 | } 357 | } 358 | 359 | line = strings.Replace(line, "💲", "$", -1) // restore and unescape 360 | return line 361 | } 362 | 363 | func (cf *controlFlow) command_conditional(line string) (stop bool) { 364 | negate := false 365 | 366 | if strings.HasPrefix(line, "!") { // negate condition 367 | negate = true 368 | line = line[1:] 369 | } 370 | 371 | if len(line) == 0 { 372 | fmt.Println("missing condition") 373 | return 374 | } 375 | 376 | parts := args.GetArgsN(line, 2) // [ condition, body ] 377 | if len(parts) != 2 { 378 | fmt.Println("missing body") 379 | return 380 | } 381 | 382 | res, err := cf.evalConditional(parts[0]) 383 | if err != nil { 384 | fmt.Println(err) 385 | return true 386 | } 387 | 388 | trueBlock, falseBlock, err := cf.ctx.ReadBlock(parts[1], "else", cf.cmd.ContinuationPrompt) 389 | if err != nil { 390 | fmt.Println(err) 391 | return true 392 | } 393 | 394 | if negate { 395 | res = !res 396 | } 397 | 398 | if res { 399 | stop = cf.cmd.RunBlock("", trueBlock, nil, false) 400 | } else { 401 | stop = cf.cmd.RunBlock("", falseBlock, nil, false) 402 | } 403 | 404 | return 405 | } 406 | 407 | func compare(args []string, num bool) (int, error) { 408 | l := len(args) 409 | 410 | if l > 2 || (num && l != 2) { 411 | return 0, fmt.Errorf("expected 2 arguments, got %v", len(args)) 412 | } 413 | 414 | var arg1, arg2 string 415 | 416 | if l > 0 { 417 | arg1 = args[0] 418 | } 419 | if l > 1 { 420 | arg2 = args[1] 421 | } 422 | 423 | if num { 424 | n1, _ := parseInt(arg1) 425 | n2, _ := parseInt(arg2) 426 | return n1 - n2, nil 427 | } else { 428 | return strings.Compare(arg1, arg2), nil 429 | } 430 | } 431 | 432 | func boolValue(v string) bool { 433 | if b, err := strconv.ParseBool(v); err == nil { 434 | return b 435 | } 436 | 437 | return v != "" 438 | } 439 | 440 | func (cf *controlFlow) evalConditional(line string) (res bool, err error) { 441 | if strings.HasPrefix(line, "(") && strings.HasSuffix(line, ")") { // (condition arg1 arg2...) 442 | line = line[1:] 443 | line = line[:len(line)-1] 444 | args := args.GetArgs(line) 445 | if len(args) == 0 { 446 | return false, fmt.Errorf("invalid condition: %q", line) 447 | } 448 | 449 | cond, args := args[0], args[1:] 450 | nargs := len(args) 451 | 452 | var cres int 453 | 454 | switch cond { 455 | case "z": 456 | switch nargs { 457 | case 0: 458 | res = true 459 | 460 | case 1: 461 | res = len(args[0]) == 0 462 | 463 | default: 464 | err = fmt.Errorf("expected 1 argument, got %v", nargs) 465 | } 466 | case "n": 467 | switch nargs { 468 | case 0: 469 | res = false 470 | 471 | case 1: 472 | res = len(args[0]) != 0 473 | 474 | default: 475 | res = true 476 | } 477 | case "t": 478 | switch nargs { 479 | case 0: 480 | res = false 481 | 482 | case 1: 483 | res = boolValue(args[0]) 484 | 485 | default: 486 | res = true 487 | } 488 | case "f": 489 | switch nargs { 490 | case 0: 491 | res = true 492 | 493 | case 1: 494 | res = !boolValue(args[0]) 495 | 496 | default: 497 | res = false 498 | } 499 | case "eq": 500 | cres, err = compare(args, false) 501 | res = cres == 0 502 | case "ne": 503 | cres, err = compare(args, false) 504 | res = cres != 0 505 | case "gt": 506 | cres, err = compare(args, false) 507 | res = cres > 0 508 | case "gte": 509 | cres, err = compare(args, false) 510 | res = cres >= 0 511 | case "lt": 512 | cres, err = compare(args, false) 513 | res = cres < 0 514 | case "lte": 515 | cres, err = compare(args, false) 516 | res = cres <= 0 517 | case "eq#": 518 | cres, err = compare(args, true) 519 | res = cres == 0 520 | case "ne#": 521 | cres, err = compare(args, true) 522 | res = cres != 0 523 | case "gt#": 524 | cres, err = compare(args, true) 525 | res = cres > 0 526 | case "gte#": 527 | cres, err = compare(args, true) 528 | res = cres >= 0 529 | case "lt#": 530 | cres, err = compare(args, true) 531 | res = cres < 0 532 | case "lte#": 533 | cres, err = compare(args, true) 534 | res = cres <= 0 535 | case "startswith": 536 | switch nargs { 537 | case 0: 538 | err = fmt.Errorf("expected 2 argument, got 0") 539 | 540 | case 1: 541 | res = false 542 | 543 | case 2: 544 | res = strings.HasPrefix(args[1], args[0]) 545 | } 546 | case "endswith": 547 | switch nargs { 548 | case 0: 549 | err = fmt.Errorf("expected 2 argument, got 0") 550 | 551 | case 1: 552 | res = false 553 | 554 | case 2: 555 | res = strings.HasSuffix(args[1], args[0]) 556 | } 557 | case "contains": 558 | switch nargs { 559 | case 0: 560 | err = fmt.Errorf("expected 2 argument, got 0") 561 | 562 | case 1: 563 | res = false 564 | 565 | case 2: 566 | res = strings.Contains(args[1], args[0]) 567 | } 568 | default: 569 | err = fmt.Errorf("invalid condition: %q", line) 570 | } 571 | } else if len(line) == 0 || line == "0" { 572 | res = false 573 | } else { 574 | res = true 575 | } 576 | 577 | return 578 | } 579 | 580 | func parseInt64(v string) (int64, error) { 581 | base := 10 582 | if strings.HasPrefix(v, "0x") { 583 | base = 16 584 | v = v[2:] 585 | } else if strings.HasPrefix(v, "0") { 586 | base = 8 587 | v = v[1:] 588 | } 589 | 590 | return strconv.ParseInt(v, base, 64) 591 | } 592 | 593 | func parseInt(v string) (int, error) { 594 | i, err := parseInt64(v) 595 | return int(i), err 596 | } 597 | 598 | func parseFloat(v string) (float64, error) { 599 | return strconv.ParseFloat(v, 64) 600 | } 601 | 602 | func intString(v int64, base int) string { 603 | if base == 0 { 604 | base = 10 605 | } 606 | 607 | return strconv.FormatInt(v, base) 608 | } 609 | 610 | func floatString(v float64) string { 611 | s := strconv.FormatFloat(v, 'f', 3, 64) 612 | return strings.TrimSuffix(s, ".000") 613 | } 614 | 615 | const expr_help = `expr operator operands... 616 | 617 | operators: 618 | +|-|*|/ number number 619 | round [up|down] number 620 | rand max [base] 621 | upper string 622 | lower string 623 | trim string 624 | substr start:end string 625 | re|regex|regexp expr string 626 | or first rest` 627 | 628 | func (cf *controlFlow) command_expression(aline string) (stop bool) { 629 | parts := args.GetArgsN(aline, 2) // [ op, arg1 ] 630 | if len(parts) != 2 { 631 | fmt.Println("usage:", expr_help) 632 | return 633 | } 634 | 635 | op, line := parts[0], parts[1] 636 | 637 | var res interface{} 638 | 639 | switch op { 640 | case "hex": // hex number... 641 | var li []string 642 | 643 | for _, n := range args.GetArgs(line) { 644 | i, _ := parseInt64(n) 645 | li = append(li, intString(i, 16)) 646 | } 647 | 648 | res = strings.Join(li, " ") 649 | 650 | case "round": // [up|down] number 651 | roundFunction := func(n float64) float64 { 652 | f := math.Floor(n) 653 | if n-f > 0.5 { 654 | return math.Ceil(n) 655 | } 656 | 657 | return f 658 | } 659 | 660 | if strings.HasPrefix(line, "up ") { 661 | roundFunction = math.Ceil 662 | line = strings.TrimSpace(line[3:]) 663 | } else if strings.HasPrefix(line, "down ") { 664 | roundFunction = math.Floor 665 | line = strings.TrimSpace(line[5:]) 666 | } 667 | 668 | n, err := parseFloat(line) 669 | if err != nil { 670 | fmt.Println("not a number") 671 | return 672 | } 673 | 674 | res = floatString(roundFunction(n)) 675 | 676 | case "rand": 677 | parts := args.GetArgs(line) // [ max, base ] 678 | if len(parts) > 2 { 679 | fmt.Println("usage: rand max [base]") 680 | return 681 | } 682 | 683 | neg := false 684 | base := 10 685 | 686 | max, err := parseInt64(parts[0]) 687 | if err != nil || max == 0 { 688 | max = math.MaxInt64 689 | } else if max < 0 { 690 | neg = true 691 | max = -max 692 | } 693 | 694 | if len(parts) == 2 { 695 | base, err = parseInt(parts[1]) 696 | if err != nil { 697 | fmt.Println("base should be a number") 698 | return 699 | } 700 | 701 | if base <= 0 { 702 | base = 10 703 | } else if base > 36 { 704 | base = 36 705 | } 706 | } 707 | 708 | r := rand.Int63n(max) 709 | if neg { 710 | r = -r 711 | } 712 | res = intString(r, base) 713 | 714 | case "+", "-", "*", "/": 715 | parts := args.GetArgs(line) // [ arg1, arg2 ] 716 | if len(parts) != 2 { 717 | fmt.Println("usage:", op, "arg1 arg2") 718 | return 719 | } 720 | 721 | n1, err := parseFloat(parts[0]) 722 | if err != nil { 723 | fmt.Println("not a number:", parts[0]) 724 | return 725 | } 726 | 727 | n2, err := parseFloat(parts[1]) 728 | if err != nil { 729 | fmt.Println("not a number:", parts[1]) 730 | return 731 | } 732 | 733 | if op == "+" { 734 | n1 += n2 735 | } else if op == "-" { 736 | n1 -= n2 737 | } else if op == "*" { 738 | n1 *= n2 739 | } else if op == "/" { 740 | n1 /= n2 741 | } 742 | res = floatString(n1) 743 | 744 | case "upper": 745 | res = strings.ToUpper(line) 746 | 747 | case "lower": 748 | res = strings.ToLower(line) 749 | 750 | case "trim": 751 | res = strings.TrimSpace(line) 752 | 753 | case "substr": 754 | parts := args.GetArgsN(line, 2) // [ start:end, line ] 755 | if len(parts) == 0 { 756 | fmt.Println("usage: substr start:end line") 757 | return 758 | } 759 | 760 | if len(parts) == 1 { // empty line ? 761 | line = "" 762 | } else { 763 | line = parts[1] 764 | } 765 | 766 | srange := parts[0] 767 | var start, end int 768 | 769 | if !strings.Contains(srange, ":") { 770 | fmt.Println("expected start:end, got", srange) 771 | return 772 | } 773 | 774 | parts = strings.Split(srange, ":") 775 | 776 | start, _ = parseInt(parts[0]) 777 | if start < 0 { 778 | start = len(line) + start 779 | } 780 | if start < 0 { 781 | start = 0 782 | } else if start > len(line) { 783 | start = len(line) 784 | } 785 | 786 | if parts[1] == "" { // start: 787 | end = len(line) 788 | } else { 789 | end, _ = parseInt(parts[1]) 790 | } 791 | 792 | if end < 0 { 793 | end = start + len(line) + end 794 | } 795 | 796 | if end < start { 797 | end = start 798 | } else if end > len(line) { 799 | end = len(line) 800 | } 801 | 802 | res = line[start:end] 803 | 804 | case "split": 805 | parts := args.GetArgsN(line, 2) // [ sep, line ] 806 | if len(parts) == 0 { 807 | fmt.Println("usage: split sep line") 808 | return 809 | } 810 | 811 | if len(parts) == 1 { // empty line ? 812 | res = "" 813 | } else { 814 | res = fmt.Sprintf("%q", strings.Split(parts[1], parts[0])) 815 | } 816 | 817 | case "re", "regex", "regexp": 818 | parts := args.GetArgsN(line, 2) // [ regexp, line ] 819 | if len(parts) == 0 { 820 | fmt.Println("usage: re expr line") 821 | return 822 | } 823 | 824 | if len(parts) == 1 { // empty line ? 825 | res = "" 826 | break 827 | } 828 | 829 | re, err := regexp.Compile(parts[0]) 830 | if err != nil { 831 | fmt.Println(err) 832 | return 833 | } 834 | 835 | parts = re.FindStringSubmatch(parts[1]) 836 | switch len(parts) { 837 | case 0: // no results 838 | res = "" 839 | case 1: // no submatches 840 | res = parts[0] 841 | case 2: // one submatch 842 | res = parts[1] 843 | default: 844 | res = fmt.Sprintf("%q", parts[1:]) 845 | } 846 | 847 | case "or": 848 | parts := args.GetArgsN(line, 2) // [ head, remain ] 849 | switch len(parts) { 850 | case 0: 851 | res = "" 852 | 853 | case 1: 854 | res = parts[0] 855 | 856 | case 2: 857 | if len(parts[0]) > 0 { 858 | res = parts[0] 859 | } else { 860 | res = parts[1] 861 | } 862 | } 863 | 864 | default: 865 | 866 | fmt.Printf("invalid operator: %v in %q\n", op, aline) 867 | return 868 | } 869 | 870 | if !cf.cmd.SilentResult() { 871 | fmt.Println(res) 872 | } 873 | 874 | cf.cmd.SetVar("result", res) 875 | return 876 | } 877 | 878 | func getList(line string) []interface{} { 879 | if strings.HasPrefix(line, "[") { 880 | j, err := simplejson.LoadString(line) 881 | if err == nil { 882 | return j.MustArray() 883 | } 884 | 885 | line = line[1:] 886 | if strings.HasSuffix(line, "]") { 887 | line = line[:len(line)-1] 888 | } 889 | } else if strings.HasPrefix(line, "(") { 890 | line = line[1:] 891 | if strings.HasSuffix(line, ")") { 892 | line = line[:len(line)-1] 893 | } 894 | } 895 | 896 | arr := args.GetArgs(line) 897 | iarr := make([]interface{}, len(arr)) 898 | for i, v := range arr { 899 | iarr[i] = v 900 | } 901 | return iarr 902 | } 903 | 904 | func (cf *controlFlow) command_repeat(line string) (stop bool) { 905 | count := int64(math.MaxInt64) // almost forever 906 | wait := time.Duration(0) // no wait 907 | arg := "" 908 | 909 | for { 910 | if strings.HasPrefix(line, "-") { 911 | // some options 912 | parts := strings.SplitN(line, " ", 2) 913 | if len(parts) < 2 { 914 | // no command 915 | fmt.Println("nothing to repeat") 916 | return 917 | } 918 | 919 | arg, line = parts[0], strings.TrimSpace(parts[1]) 920 | if arg == "--" { 921 | break 922 | } 923 | 924 | if strings.HasPrefix(arg, "--count=") { 925 | arg = cf.expandVariables(arg) 926 | count, _ = strconv.ParseInt(arg[8:], 10, 64) 927 | } else if strings.HasPrefix(arg, "--wait=") { 928 | arg = cf.expandVariables(arg) 929 | wait = parseWait(arg[7:]) 930 | } else { 931 | // unknown option 932 | fmt.Println("invalid option", arg) 933 | return 934 | } 935 | } else { 936 | break 937 | } 938 | } 939 | 940 | block, _, err := cf.ctx.ReadBlock(line, "", cf.cmd.ContinuationPrompt) 941 | if err != nil { 942 | fmt.Println(err) 943 | return 944 | } 945 | 946 | cf.ctx.PushScope(nil, nil) 947 | cf.cmd.SetVar("count", count) 948 | 949 | cf.Lock() 950 | cf.inLoop = true 951 | cf.Unlock() 952 | 953 | for l := newLoop(count); l.Next(); { 954 | if wait > 0 && !l.First() { 955 | if cf.sleepInterrupted(wait) { 956 | break 957 | } 958 | } 959 | 960 | cf.cmd.SetVar("index", l.Index) 961 | if cf.cmd.RunBlock("", block, nil, true) || cf.cmd.Interrupted() { 962 | break 963 | } 964 | } 965 | 966 | cf.Lock() 967 | cf.inLoop = false 968 | cf.Unlock() 969 | 970 | cf.ctx.PopScope() 971 | return 972 | } 973 | 974 | func (cf *controlFlow) command_foreach(line string) (stop bool) { 975 | arg := "" 976 | wait := time.Duration(0) // no wait 977 | 978 | for { 979 | if strings.HasPrefix(line, "-") { 980 | // some options 981 | parts := strings.SplitN(line, " ", 2) 982 | if len(parts) < 2 { 983 | // no command 984 | return 985 | } 986 | 987 | arg, line = parts[0], strings.TrimSpace(parts[1]) 988 | if arg == "--" { 989 | break 990 | } 991 | 992 | if strings.HasPrefix(arg, "--wait=") { 993 | arg = cf.expandVariables(arg) 994 | wait = parseWait(arg[7:]) 995 | } else { 996 | // unknown option 997 | fmt.Println("invalid option", arg) 998 | return 999 | } 1000 | } else { 1001 | break 1002 | } 1003 | } 1004 | 1005 | parts := args.GetArgsN(line, 2) // [ list, command ] 1006 | if len(parts) != 2 { 1007 | fmt.Println("missing argument(s)") 1008 | return 1009 | } 1010 | 1011 | list, command := cf.expandVariables(parts[0]), parts[1] 1012 | 1013 | args := getList(list) 1014 | count := len(args) 1015 | 1016 | block, _, err := cf.ctx.ReadBlock(command, "", cf.cmd.ContinuationPrompt) 1017 | if err != nil { 1018 | fmt.Println(err) 1019 | return 1020 | } 1021 | 1022 | cf.ctx.PushScope(nil, nil) 1023 | cf.cmd.SetVar("count", count) 1024 | 1025 | cf.Lock() 1026 | cf.inLoop = true 1027 | cf.Unlock() 1028 | 1029 | for i, v := range args { 1030 | if wait > 0 && i > 0 { 1031 | if cf.sleepInterrupted(wait) { 1032 | break 1033 | } 1034 | } 1035 | 1036 | // here we should convert complex types to a meaningful 1037 | // string representation (i.e. json) 1038 | 1039 | switch t := v.(type) { 1040 | case map[string]interface{}: 1041 | v, _ = simplejson.DumpString(t) 1042 | 1043 | case []interface{}: 1044 | v, _ = simplejson.DumpString(t) 1045 | } 1046 | 1047 | cf.cmd.SetVar("index", i) 1048 | cf.cmd.SetVar("item", v) 1049 | if cf.cmd.RunBlock("", block, nil, true) || cf.cmd.Interrupted() { 1050 | break 1051 | } 1052 | } 1053 | 1054 | cf.Lock() 1055 | cf.inLoop = false 1056 | cf.Unlock() 1057 | 1058 | cf.ctx.PopScope() 1059 | return 1060 | } 1061 | 1062 | func (cf *controlFlow) command_load(line string) (stop bool) { 1063 | if len(line) == 0 { 1064 | fmt.Println("missing script file") 1065 | return 1066 | } 1067 | 1068 | fname := line 1069 | f, err := os.Open(fname) 1070 | if err != nil { 1071 | fmt.Println(err) 1072 | return 1073 | } 1074 | 1075 | prev := cf.ctx.ScanReader(f) 1076 | 1077 | defer func() { 1078 | cf.ctx.SetScanner(prev) 1079 | f.Close() 1080 | }() 1081 | 1082 | for { 1083 | line, err = cf.ctx.ReadLine("load", "") 1084 | if err != nil { 1085 | if err != io.EOF { 1086 | fmt.Println(err) 1087 | } 1088 | break 1089 | } 1090 | 1091 | if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") { 1092 | cf.cmd.EmptyLine() 1093 | continue 1094 | } 1095 | 1096 | // fmt.Println("load-one", line) 1097 | stop = cf.cmd.OneCmd(line) 1098 | if stop || cf.cmd.Interrupted() { 1099 | break 1100 | } 1101 | } 1102 | 1103 | return 1104 | } 1105 | 1106 | func (cf *controlFlow) command_sleep(line string) (stop bool) { 1107 | wait := parseWait(line) 1108 | cf.sleepInterrupted(wait) 1109 | return 1110 | } 1111 | 1112 | func (cf *controlFlow) command_stop(string) (stop bool) { 1113 | return true 1114 | } 1115 | 1116 | func (cf *controlFlow) help(line string) (stop bool) { 1117 | if line == "" { 1118 | cf._help(line) 1119 | 1120 | if len(cf.functions) > 0 { 1121 | fmt.Println() 1122 | fmt.Println("Available functions:") 1123 | fmt.Println("================================================================") 1124 | 1125 | names, max := cf.functionNames() 1126 | 1127 | tp := pretty.NewTabPrinter(80 / (max + 1)) 1128 | tp.TabWidth(max + 1) 1129 | 1130 | for _, c := range names { 1131 | tp.Print(c) 1132 | } 1133 | tp.Println() 1134 | } 1135 | } else if _, ok := cf.functions[line]; ok { 1136 | fmt.Println(line, "is a function") 1137 | } else { 1138 | cf._help(line) 1139 | } 1140 | 1141 | return 1142 | } 1143 | 1144 | // XXX: don't expand one-line body of "function" or "repeat" 1145 | func canExpand(line string) bool { 1146 | if strings.HasPrefix(line, "function ") { 1147 | return false 1148 | } 1149 | if strings.HasPrefix(line, "repeat ") { 1150 | return false 1151 | } 1152 | if strings.HasPrefix(line, "foreach ") { 1153 | return false 1154 | } 1155 | return true 1156 | } 1157 | 1158 | func (cf *controlFlow) runFunction(line string) bool { 1159 | if canExpand(line) { 1160 | line = cf.expandVariables(line) 1161 | } 1162 | 1163 | if strings.HasPrefix(line, "@") { 1164 | line = "load " + line[1:] 1165 | } else { 1166 | parts := strings.SplitN(line, " ", 2) 1167 | 1168 | cname, params := parts[0], "" 1169 | if len(parts) > 1 { 1170 | params = strings.TrimSpace(parts[1]) 1171 | } 1172 | 1173 | if function, ok := cf.functions[cname]; ok { 1174 | if cf.cmd.GetBoolVar("echo") { 1175 | fmt.Println(cf.cmd.Prompt, line) 1176 | } 1177 | 1178 | return cf.cmd.RunBlock(cname, function, args.GetArgs(params), true) 1179 | } 1180 | } 1181 | 1182 | return cf._oneCmd(line) 1183 | } 1184 | 1185 | func (cf *controlFlow) loopCommand() (looping bool) { 1186 | cf.RLock() 1187 | looping = cf.inLoop 1188 | cf.RUnlock() 1189 | return 1190 | } 1191 | 1192 | func (cf *controlFlow) interruptFunction(s os.Signal) bool { 1193 | if s == os.Interrupt && cf.loopCommand() { 1194 | return false 1195 | } 1196 | 1197 | return cf._interrupt(s) 1198 | } 1199 | 1200 | // PluginInit initialize this plugin 1201 | func (cf *controlFlow) PluginInit(c *cmd.Cmd, ctx *internal.Context) error { 1202 | if cf.cmd != nil { 1203 | return nil // already initialized 1204 | } 1205 | 1206 | rand.Seed(time.Now().Unix()) 1207 | 1208 | cf.cmd, cf.ctx = c, ctx 1209 | cf._oneCmd, c.OneCmd = c.OneCmd, cf.runFunction 1210 | cf._help, c.Help = c.Help, cf.help 1211 | cf._interrupt, c.Interrupt = c.Interrupt, cf.interruptFunction 1212 | cf.functions = make(map[string][]string) 1213 | 1214 | cf.cmd.AddCompleter("function", cmd.NewWordCompleter(func() (names []string) { 1215 | names, _ = cf.functionNames() 1216 | return 1217 | }, func(s, l string) bool { 1218 | return strings.HasPrefix(l, "function ") 1219 | })) 1220 | cf.cmd.AddCompleter("var", cmd.NewWordCompleter(func() []string { 1221 | return cf.ctx.GetVarNames() 1222 | }, func(s, l string) bool { 1223 | return strings.HasPrefix(l, "var ") || strings.HasPrefix(l, "set ") 1224 | })) 1225 | 1226 | c.Add(cmd.Command{"function", `function name body`, cf.command_function, nil}) 1227 | c.Add(cmd.Command{"var", `var [-g|--global|--parent] [-r|--remove|-u|--unset|-i|-incr|-d|--decr] name value`, cf.command_variable, nil}) 1228 | c.Add(cmd.Command{"shift", `shift [n]`, cf.command_shift, nil}) 1229 | c.Add(cmd.Command{"if", `if (condition) command`, cf.command_conditional, nil}) 1230 | c.Add(cmd.Command{"expr", expr_help, cf.command_expression, nil}) 1231 | c.Add(cmd.Command{"foreach", `foreach [--wait=duration] (items...) command`, cf.command_foreach, nil}) 1232 | c.Add(cmd.Command{"repeat", `repeat [--count=n] [--wait=duration] [--echo] command`, cf.command_repeat, nil}) 1233 | c.Add(cmd.Command{"load", `load script-file`, cf.command_load, nil}) 1234 | c.Add(cmd.Command{"sleep", `sleep duration`, cf.command_sleep, nil}) 1235 | c.Add(cmd.Command{"stop", `stop function or block`, cf.command_stop, nil}) 1236 | 1237 | c.Commands["set"] = c.Commands["var"] 1238 | return nil 1239 | } 1240 | -------------------------------------------------------------------------------- /plugins/json/README.md: -------------------------------------------------------------------------------- 1 | json plugin 2 | =========== 3 | 4 | The json plugin add some json-related commands to the command loop. 5 | 6 | The new commands are: 7 | 8 | - json : creates a json object out of key/value pairs or lists 9 | - jsonpath : parses a json object and extract specified fields 10 | - format : pretty-print specified json object 11 | 12 | -------------------------------------------------------------------------------- /plugins/json/json.go: -------------------------------------------------------------------------------- 1 | // Package json add some json-related commands to the command loop. 2 | // 3 | // The new commands are: 4 | // 5 | // json : creates a json object out of key/value pairs or lists 6 | // jsonpath : parses a json object and extract specified fields 7 | // format : pretty-print specified json object 8 | package json 9 | 10 | import ( 11 | "fmt" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/gobs/args" 17 | "github.com/gobs/cmd" 18 | "github.com/gobs/cmd/internal" 19 | "github.com/gobs/jsonpath" 20 | "github.com/gobs/simplejson" 21 | ) 22 | 23 | type jsonPlugin struct { 24 | cmd.Plugin 25 | } 26 | 27 | var ( 28 | Plugin = &jsonPlugin{} 29 | 30 | reFieldValue = regexp.MustCompile(`(\w[\d\w-]*)(=(.*))?`) // field-name=value 31 | ) 32 | 33 | func unquote(s string, q byte) (string, error) { 34 | l := len(s) 35 | if l == 1 { 36 | return s, fmt.Errorf("tooshort") 37 | } 38 | 39 | if s[l-1] == q { 40 | return s[1 : l-1], nil 41 | } 42 | 43 | return s, fmt.Errorf("unbalanced") 44 | } 45 | 46 | func parseValue(v string) (interface{}, error) { 47 | switch { 48 | case strings.HasPrefix(v, "{") || strings.HasPrefix(v, "["): 49 | j, err := simplejson.LoadString(v) 50 | if err != nil { 51 | return nil, fmt.Errorf("error parsing |%v|", v) 52 | } else { 53 | return j.Data(), nil 54 | } 55 | 56 | case strings.HasPrefix(v, `"`): 57 | return unquote(v, '"') 58 | 59 | case strings.HasPrefix(v, `'`): 60 | return unquote(v, '\'') 61 | 62 | case strings.HasPrefix(v, "`"): 63 | return unquote(v, '`') 64 | 65 | case v == "": 66 | return v, nil 67 | 68 | case v == "true": 69 | return true, nil 70 | 71 | case v == "false": 72 | return false, nil 73 | 74 | case v == "null": 75 | return nil, nil 76 | 77 | default: 78 | if i, err := strconv.ParseInt(v, 10, 64); err == nil { 79 | return i, nil 80 | } 81 | if f, err := strconv.ParseFloat(v, 64); err == nil { 82 | return f, nil 83 | } 84 | 85 | return v, nil 86 | } 87 | } 88 | 89 | // Function PrintJson prints the specified object formatted as a JSON object 90 | func PrintJson(v interface{}) { 91 | fmt.Println(simplejson.MustDumpString(v, simplejson.Indent(" "))) 92 | } 93 | 94 | // Function StringJson return the specified object as a JSON string 95 | func StringJson(v interface{}, unq bool) (ret string) { 96 | ret = simplejson.MustDumpString(v) 97 | if unq { 98 | ret, _ = unquote(strings.TrimSpace(ret), '"') 99 | } 100 | 101 | return 102 | } 103 | 104 | type map_type = map[string]interface{} 105 | type array_type = []interface{} 106 | 107 | func merge_maps(dst, src map_type) map_type { 108 | for k, vs := range src { 109 | if vd, ok := dst[k]; ok { 110 | if ms, ok := vs.(map_type); ok { 111 | if md, ok := vd.(map_type); ok { 112 | merge_maps(md, ms) 113 | continue 114 | } 115 | } 116 | } 117 | dst[k] = vs 118 | } 119 | 120 | return dst 121 | } 122 | 123 | func merge_array(dst array_type, src interface{}) array_type { 124 | if a, ok := src.(array_type); ok { 125 | return append(dst, a...) 126 | } else { 127 | return append(dst, src) 128 | } 129 | } 130 | 131 | // PluginInit initialize this plugin 132 | func (p *jsonPlugin) PluginInit(commander *cmd.Cmd, _ *internal.Context) error { 133 | 134 | setError := func(err interface{}) { 135 | fmt.Println(err) 136 | commander.SetVar("error", err) 137 | } 138 | 139 | setJson := func(v interface{}) { 140 | commander.SetVar("json", StringJson(v, true)) 141 | commander.SetVar("error", "") 142 | 143 | if !commander.SilentResult() { 144 | PrintJson(v) 145 | } 146 | } 147 | 148 | commander.Add(cmd.Command{"json", 149 | ` 150 | json field1=value1 field2=value2... // json object 151 | json {"name1":"value1", "name2":"value2"} 152 | json [value1, value2...] 153 | json -a|--array value1 value2 value3`, 154 | func(line string) (stop bool) { 155 | var res interface{} 156 | var ares []interface{} 157 | 158 | if strings.HasPrefix(line, "-a ") { 159 | line = strings.TrimSpace(line[3:]) 160 | ares = []interface{}{} 161 | } else if strings.HasPrefix(line, "--array ") { 162 | line = strings.TrimSpace(line[8:]) 163 | ares = []interface{}{} 164 | } 165 | 166 | for len(line) > 0 { 167 | jbody, rest, err := simplejson.LoadPartialString(line) 168 | if err == nil { 169 | switch v := res.(type) { 170 | case nil: // first time 171 | res = jbody.Data() 172 | 173 | case map_type: 174 | src, err := jbody.Map() 175 | if err != nil { 176 | setError(fmt.Errorf("merge source should be a map")) 177 | return 178 | } 179 | res = merge_maps(v, src) 180 | 181 | case array_type: 182 | res = merge_array(v, jbody.Data()) 183 | } 184 | } else { 185 | args := args.GetArgsN(line, 2, args.InfieldBrackets()) 186 | 187 | matches := reFieldValue.FindStringSubmatch(args[0]) 188 | if len(matches) > 0 { // [field=value field =value value] 189 | name, svalue := matches[1], matches[3] 190 | value, err := parseValue(svalue) 191 | if err != nil { 192 | setError(err) 193 | return 194 | } 195 | 196 | mval := map[string]interface{}{name: value} 197 | 198 | switch v := res.(type) { 199 | case nil: // first time 200 | res = mval 201 | 202 | case map_type: 203 | res = merge_maps(v, mval) 204 | 205 | case array_type: 206 | res = merge_array(v, mval) 207 | } 208 | } else { 209 | setError(fmt.Errorf("invalid name=value pair: %v", args[0])) 210 | return 211 | } 212 | 213 | if len(args) == 2 { 214 | rest = args[1] 215 | } else { 216 | break 217 | } 218 | } 219 | 220 | line = strings.TrimSpace(rest) 221 | if ares != nil { 222 | ares = append(ares, res) 223 | res = nil 224 | } 225 | } 226 | 227 | if ares == nil { 228 | setJson(res) 229 | } else { 230 | setJson(ares) 231 | } 232 | return 233 | }, 234 | nil}) 235 | 236 | commander.Add(cmd.Command{ 237 | "jsonpath", 238 | `jsonpath [-v] [-e] [-c] path {json}`, 239 | func(line string) (stop bool) { 240 | var joptions jsonpath.ProcessOptions 241 | var verbose bool 242 | 243 | options, line := args.GetOptions(line) 244 | for _, o := range options { 245 | if o == "-e" || o == "--enhanced" { 246 | joptions |= jsonpath.Enhanced 247 | } else if o == "-c" || o == "--collapse" { 248 | joptions |= jsonpath.Collapse 249 | } else if o == "-v" || o == "--verbose" { 250 | verbose = true 251 | } else { 252 | line = "" // to force an error 253 | break 254 | } 255 | } 256 | 257 | parts := args.GetArgsN(line, 2) 258 | if len(parts) != 2 { 259 | setError("invalid-usage") 260 | return 261 | } 262 | 263 | path := parts[0] 264 | if !(strings.HasPrefix(path, "$.") || strings.HasPrefix(path, "$[")) { 265 | path = "$." + path 266 | } 267 | 268 | jbody, err := simplejson.LoadString(parts[1]) 269 | if err != nil { 270 | setError(err) 271 | return 272 | } 273 | 274 | jp := jsonpath.NewProcessor() 275 | if !jp.Parse(path) { 276 | setError(fmt.Errorf("failed to parse %q", path)) 277 | return // syntax error 278 | } 279 | 280 | if verbose { 281 | fmt.Println("jsonpath", path) 282 | for _, n := range jp.Nodes { 283 | fmt.Println(" ", n) 284 | } 285 | } 286 | 287 | res := jp.Process(jbody, joptions) 288 | setJson(res) 289 | return 290 | }, 291 | nil}) 292 | 293 | commander.Add(cmd.Command{ 294 | "format", 295 | `format object`, 296 | func(line string) (stop bool) { 297 | jbody, err := simplejson.LoadString(line) 298 | if err != nil { 299 | fmt.Println("format:", err) 300 | fmt.Println("input:", line) 301 | return 302 | } 303 | 304 | PrintJson(jbody.Data()) 305 | return 306 | }, 307 | nil}) 308 | 309 | return nil 310 | } 311 | -------------------------------------------------------------------------------- /plugins/stats/stats.go: -------------------------------------------------------------------------------- 1 | // Package stats add some statistics-related commands to the command loop. 2 | // 3 | // The new commands are in the form: 4 | // 5 | // stats {type} values... 6 | package stats 7 | 8 | import ( 9 | "fmt" 10 | "math" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/gobs/args" 16 | "github.com/gobs/cmd" 17 | "github.com/gobs/cmd/internal" 18 | "github.com/montanaflynn/stats" 19 | ) 20 | 21 | type statsPlugin struct { 22 | cmd.Plugin 23 | } 24 | 25 | var ( 26 | Plugin = &statsPlugin{} 27 | ) 28 | 29 | func parseFloat(v string) (float64, error) { 30 | return strconv.ParseFloat(v, 64) 31 | } 32 | 33 | func floatString(v float64) string { 34 | s := strconv.FormatFloat(v, 'f', 3, 64) 35 | return strings.TrimSuffix(s, ".000") 36 | } 37 | 38 | // sortedCopy returns a sorted copy of float64s 39 | func sortedCopy(input stats.Float64Data) (sorted stats.Float64Data) { 40 | sorted = make(stats.Float64Data, input.Len()) 41 | copy(sorted, input) 42 | sort.Float64s(sorted) 43 | return 44 | } 45 | 46 | // Percentile finds the relative standing in a slice of floats 47 | // (note: the "Percentile" method in "stats" is incorrect) 48 | 49 | func Percentile(input stats.Float64Data, percent float64) (percentile float64, err error) { 50 | 51 | if input.Len() == 0 { 52 | return math.NaN(), stats.EmptyInput 53 | } 54 | 55 | if percent < 0 || percent > 100 { 56 | return math.NaN(), stats.BoundsErr 57 | } 58 | 59 | // Start by sorting a copy of the slice 60 | sorted := sortedCopy(input) 61 | 62 | // Edge cases 63 | if percent == 0.0 { // The percentile argument of 0 will return the minimum value in the dataset. 64 | return sorted[0], nil 65 | } 66 | if percent == 50.0 { // The percentile argument of 50 returns the median value. 67 | return sorted.Median() 68 | } 69 | if percent == 100.0 { // The percentile argument of 100 returns the maximum value from the dataset. 70 | return sorted[len(sorted)-1], nil 71 | } 72 | 73 | // Find the rank. Rank is the position of an element in the dataset. 74 | rank := ((percent / 100) * float64(len(sorted)-1)) 75 | 76 | ri := int(rank) 77 | rf := rank - float64(ri) 78 | 79 | percentile = sorted[ri] + rf*(sorted[ri+1]-sorted[ri]) 80 | return 81 | } 82 | 83 | // PluginInit initialize this plugin 84 | func (p *statsPlugin) PluginInit(commander *cmd.Cmd, _ *internal.Context) error { 85 | 86 | commander.Add(cmd.Command{"stats", 87 | ` 88 | stats {count|sort|min|max|mean|median|sum|variance|std|pN} value... 89 | `, 90 | func(line string) (stop bool) { 91 | var res float64 92 | var err error 93 | 94 | parts := args.GetArgs(line) // [ type, value, ... ] 95 | if len(parts) == 0 { 96 | fmt.Println("usage: stats {count|sort|min|max|mean|median|sum|variance|std|pN} value...") 97 | return 98 | } 99 | 100 | if len(parts) == 1 { 101 | res = 0.0 102 | } else { 103 | cmd, parts := parts[0], parts[1:] 104 | sample := false 105 | population := false 106 | geometric := false 107 | harmonic := false 108 | nearestRank := false 109 | 110 | if len(parts) > 0 { 111 | switch parts[0] { 112 | case "-g", "--geometric": 113 | geometric = true 114 | parts = parts[1:] 115 | 116 | case "-h", "--harmonic": 117 | harmonic = true 118 | parts = parts[1:] 119 | 120 | case "-s", "--sample": 121 | sample = true 122 | parts = parts[1:] 123 | 124 | case "-p", "--population": 125 | population = true 126 | parts = parts[1:] 127 | 128 | case "-n", "--nearest-rank": 129 | nearestRank = true 130 | parts = parts[1:] 131 | } 132 | } 133 | 134 | data := stats.LoadRawData(parts) 135 | pc := 0.0 136 | 137 | if strings.HasPrefix(cmd, "p") { 138 | pc, err = parseFloat(cmd[1:]) 139 | if err != nil { 140 | fmt.Println("invalid percentile command:", cmd) 141 | return 142 | } 143 | 144 | cmd = "p" 145 | } 146 | 147 | switch cmd { 148 | case "sort": 149 | sorted := sortedCopy(data) 150 | ssort := make([]string, len(sorted)) 151 | for i, v := range sorted { 152 | ssort[i] = floatString(v) 153 | } 154 | sres := strings.Join(ssort, " ") 155 | commander.SetVar("error", "") 156 | commander.SetVar("result", sres) 157 | if !commander.SilentResult() { 158 | fmt.Println(sres) 159 | } 160 | return 161 | 162 | case "count": 163 | res = float64(len(data)) 164 | case "min": 165 | res, err = data.Min() 166 | case "max": 167 | res, err = data.Max() 168 | case "mean": 169 | if geometric { 170 | res, err = data.GeometricMean() 171 | } else if harmonic { 172 | res, err = data.HarmonicMean() 173 | } else { 174 | res, err = data.Mean() 175 | } 176 | case "median": 177 | res, err = data.Median() 178 | //case "mode": 179 | // res, err = data.Mode() 180 | case "sum": 181 | res, err = data.Sum() 182 | case "variance": 183 | if sample { 184 | res, err = data.SampleVariance() 185 | } else if population { 186 | res, err = data.PopulationVariance() 187 | } else { 188 | res, err = data.Variance() 189 | } 190 | case "std": 191 | if sample { 192 | res, err = data.StandardDeviationSample() 193 | } else if population { 194 | res, err = data.StandardDeviationPopulation() 195 | } else { 196 | res, err = data.StandardDeviation() 197 | } 198 | case "p": 199 | if nearestRank { 200 | res, err = data.PercentileNearestRank(pc) 201 | } else { 202 | res, err = Percentile(data, pc) 203 | } 204 | default: 205 | fmt.Println("usage: stats {count|sort|min|max|mean|median|sum|variance|std|pN} value...") 206 | return 207 | } 208 | } 209 | 210 | if err != nil { 211 | commander.SetVar("error", err) 212 | commander.SetVar("result", "0") 213 | fmt.Println(err) 214 | } else { 215 | sres := floatString(res) 216 | if !commander.SilentResult() { 217 | fmt.Println(sres) 218 | } 219 | 220 | commander.SetVar("error", "") 221 | commander.SetVar("result", sres) 222 | } 223 | 224 | return 225 | }, 226 | nil}) 227 | 228 | return nil 229 | } 230 | --------------------------------------------------------------------------------