This is an automated email from the ASF dual-hosted git repository. rohit pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/cloudstack-cloudmonkey.git
commit 88b5006d17af46546a105c580315a48b4088f406 Author: Rohit Yadav <[email protected]> AuthorDate: Mon Oct 22 00:43:11 2018 +0530 cmk: shlex input, new columnar output, refactorings Signed-off-by: Rohit Yadav <[email protected]> --- cli/completer.go | 3 - cli/exec.go | 23 +- cli/{shell.go => prompt.go} | 40 +- cmd/api.go | 4 +- cmd/output.go | 44 ++- cmd/set.go | 2 +- cmd/version.go | 2 +- cmk.go | 12 +- config/about.go | 11 +- config/config.go | 15 +- go.mod | 2 +- go.sum | 4 +- vendor/github.com/google/shlex/COPYING | 202 ++++++++++ vendor/github.com/google/shlex/README | 2 + vendor/github.com/google/shlex/shlex.go | 417 +++++++++++++++++++++ vendor/github.com/mattn/go-shellwords/.travis.yml | 8 - vendor/github.com/mattn/go-shellwords/LICENSE | 21 -- vendor/github.com/mattn/go-shellwords/README.md | 47 --- .../github.com/mattn/go-shellwords/shellwords.go | 145 ------- .../github.com/mattn/go-shellwords/util_posix.go | 19 - .../github.com/mattn/go-shellwords/util_windows.go | 17 - vendor/modules.txt | 4 +- 22 files changed, 725 insertions(+), 319 deletions(-) diff --git a/cli/completer.go b/cli/completer.go index 2345bbf..06bd29b 100644 --- a/cli/completer.go +++ b/cli/completer.go @@ -72,9 +72,6 @@ func inArray(s string, array []string) bool { var cachedResponse map[string]interface{} func completer(in prompt.Document) []prompt.Suggest { - if in.TextBeforeCursor() == "" { - return []prompt.Suggest{} - } args := strings.Split(strings.TrimLeft(in.TextBeforeCursor(), " "), " ") for i := range args { diff --git a/cli/exec.go b/cli/exec.go index 1c14418..32f2e2d 100644 --- a/cli/exec.go +++ b/cli/exec.go @@ -19,24 +19,29 @@ package cli import ( "fmt" - "strings" + "os/exec" + "runtime" "github.com/apache/cloudstack-cloudmonkey/cmd" - "github.com/mattn/go-shellwords" + "github.com/google/shlex" ) +// ExecLine executes a line of command func ExecLine(line string) error { - shellwords.ParseEnv = true - parser := shellwords.NewParser() - args, err := parser.Parse(line) + args, err := shlex.Split(line) if err != nil { - fmt.Println("🙈 Failed to parse line:", err) return err } - if parser.Position > 0 { - line = fmt.Sprintf("shell %s %v", cfg.Name(), line) - args = strings.Split(line, " ") + if runtime.GOOS != "windows" { + for _, arg := range args { + if arg == "|" { + if result, err := exec.Command("bash", "-c", "cmk", line).Output(); err == nil { + fmt.Println(string(result)) + return nil + } + } + } } return ExecCmd(args) diff --git a/cli/shell.go b/cli/prompt.go similarity index 79% rename from cli/shell.go rename to cli/prompt.go index f9550d1..c558730 100644 --- a/cli/shell.go +++ b/cli/prompt.go @@ -19,49 +19,39 @@ package cli import ( "fmt" - "os" "github.com/apache/cloudstack-cloudmonkey/config" "github.com/c-bata/go-prompt" ) +// CLI config instance var cfg *config.Config -func executor(in string) { - if err := ExecLine(in); err != nil { - fmt.Println("🙈 Error:", err) - } +// SetConfig allows to set a config.Config object to cli +func SetConfig(c *config.Config) { + cfg = c } -func prefix() (string, bool) { - return cfg.GetPrompt(), true -} - -// ExecShell starts a shell -func ExecShell(sysArgs []string) { - cfg = config.NewConfig() - - if len(sysArgs) > 0 { - err := ExecCmd(sysArgs) - if err != nil { - fmt.Println("🙈 Error:", err) - os.Exit(1) - } - os.Exit(0) - } - +// ExecPrompt starts a CLI prompt +func ExecPrompt() { cfg.HasShell = true cfg.PrintHeader() lines, _ := readLines(cfg.HistoryFile) shell := prompt.New( - executor, + func(in string) { + if err := ExecLine(in); err != nil { + fmt.Println("🙈 Error:", err) + } + }, completer, prompt.OptionTitle("cloudmonkey"), prompt.OptionPrefix(cfg.GetPrompt()), - prompt.OptionLivePrefix(prefix), - prompt.OptionMaxSuggestion(8), + prompt.OptionLivePrefix(func() (string, bool) { + return cfg.GetPrompt(), true + }), + prompt.OptionMaxSuggestion(5), prompt.OptionHistory(lines), prompt.OptionPrefixTextColor(prompt.DefaultColor), prompt.OptionPreviewSuggestionTextColor(prompt.DarkBlue), diff --git a/cmd/api.go b/cmd/api.go index 0c3b7fd..3ba7ea4 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -91,7 +91,9 @@ func init() { } } - printResult(r.Config.Core.Output, response, filterKeys) + if len(response) > 0 { + printResult(r.Config.Core.Output, response, filterKeys) + } return nil }, diff --git a/cmd/output.go b/cmd/output.go index 3784856..2e649a3 100644 --- a/cmd/output.go +++ b/cmd/output.go @@ -20,12 +20,14 @@ package cmd import ( "encoding/json" "fmt" - "github.com/apache/cloudstack-cloudmonkey/config" - "github.com/olekukonko/tablewriter" "os" "reflect" "sort" "strings" + "text/tabwriter" + + "github.com/apache/cloudstack-cloudmonkey/config" + "github.com/olekukonko/tablewriter" ) func printJSON(response map[string]interface{}) { @@ -97,6 +99,40 @@ func printText(response map[string]interface{}) { } } +func printColumn(response map[string]interface{}) { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.DiscardEmptyColumns) + for _, v := range response { + valueType := reflect.TypeOf(v) + if valueType.Kind() == reflect.Slice || valueType.Kind() == reflect.Map { + items, ok := v.([]interface{}) + if !ok { + continue + } + var header []string + for idx, item := range items { + row, ok := item.(map[string]interface{}) + if !ok || len(row) < 1 { + continue + } + + if idx == 0 { + for rk := range row { + header = append(header, strings.ToUpper(rk)) + } + sort.Strings(header) + fmt.Fprintln(w, strings.Join(header, "\t")) + } + var values []string + for _, key := range header { + values = append(values, fmt.Sprintf("%v", row[strings.ToLower(key)])) + } + fmt.Fprintln(w, strings.Join(values, "\t")) + } + } + } + w.Flush() +} + func printCsv(response map[string]interface{}) { for _, v := range response { valueType := reflect.TypeOf(v) @@ -177,10 +213,10 @@ func printResult(outputType string, response map[string]interface{}, filter []st printTable(response) case config.TEXT: printText(response) + case config.COLUMN: + printColumn(response) case config.CSV: printCsv(response) - case config.XML: - fmt.Println("Unfinished output format: xml, use something else") default: fmt.Println("Invalid output type configured, please fix that!") } diff --git a/cmd/set.go b/cmd/set.go index a2eeb9a..4bc29e3 100644 --- a/cmd/set.go +++ b/cmd/set.go @@ -30,7 +30,7 @@ func init() { "prompt": {"🐵", "🐱", "random"}, "asyncblock": {"true", "false"}, "timeout": {"600", "1800", "3600"}, - "output": {"json", "text", "table", "csv", "xml"}, + "output": {"json", "text", "table", "column", "csv"}, "profile": {}, "url": {}, "username": {}, diff --git a/cmd/version.go b/cmd/version.go index 15bcfd9..c07c1d7 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -24,7 +24,7 @@ func init() { Name: "version", Help: "Version info", Handle: func(r *Request) error { - fmt.Println(r.Config.About()) + fmt.Println(r.Config.Name(), r.Config.Version()) return nil }, }) diff --git a/cmk.go b/cmk.go index 9dc7448..7175a2d 100644 --- a/cmk.go +++ b/cmk.go @@ -18,12 +18,22 @@ package main import ( + "fmt" "os" "github.com/apache/cloudstack-cloudmonkey/cli" + "github.com/apache/cloudstack-cloudmonkey/config" ) func main() { args := os.Args[1:] - cli.ExecShell(args) + cli.SetConfig(config.NewConfig()) + if len(args) > 0 { + if err := cli.ExecCmd(args); err != nil { + fmt.Println("🙈 Error:", err) + os.Exit(1) + } + os.Exit(0) + } + cli.ExecPrompt() } diff --git a/config/about.go b/config/about.go index 6438a05..dccf5de 100644 --- a/config/about.go +++ b/config/about.go @@ -21,16 +21,17 @@ import "fmt" // Name of the CLI func (c *Config) Name() string { - return "cmk" + return "Apache CloudStack 🐵 CloudMonkey" } -// About CLI -func (c *Config) About() string { - return "Apache CloudStack 🐵 CloudMonkey 6.0.0-beta1" +// Version CLI +func (c *Config) Version() string { + return "6.0.0-beta1" } // PrintHeader prints startup message in CLI mode func (c *Config) PrintHeader() { - fmt.Println(c.About()) + fmt.Println(c.Name(), c.Version()) fmt.Println("Report issues: https://github.com/apache/cloudstack-cloudmonkey/issues") + fmt.Println() } diff --git a/config/config.go b/config/config.go index ccbd23a..0b0ad6b 100644 --- a/config/config.go +++ b/config/config.go @@ -20,23 +20,24 @@ package config import ( "crypto/tls" "fmt" - "github.com/mitchellh/go-homedir" - "gopkg.in/ini.v1" "net/http" "net/http/cookiejar" "os" "path" "strconv" "time" + + "github.com/mitchellh/go-homedir" + "gopkg.in/ini.v1" ) // Output formats const ( - CSV = "csv" - JSON = "json" - TABLE = "table" - TEXT = "text" - XML = "xml" + COLUMN = "column" + CSV = "csv" + JSON = "json" + TABLE = "table" + TEXT = "text" ) // ServerProfile describes a management server diff --git a/go.mod b/go.mod index 523a813..ed2a67a 100644 --- a/go.mod +++ b/go.mod @@ -21,12 +21,12 @@ require ( github.com/briandowns/spinner v0.0.0-20181018151057-dd69c579ff20 github.com/c-bata/go-prompt v0.2.2 github.com/fatih/color v1.7.0 // indirect + github.com/google/shlex v0.0.0-20150127133951-6f45313302b9 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/mattn/go-colorable v0.0.9 // indirect github.com/mattn/go-isatty v0.0.4 // indirect github.com/mattn/go-runewidth v0.0.3 // indirect - github.com/mattn/go-shellwords v1.0.3 github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104 // indirect github.com/mitchellh/go-homedir v1.0.0 github.com/olekukonko/tablewriter v0.0.0-20180912035003-be2c049b30cc diff --git a/go.sum b/go.sum index 9c8190f..d7b7057 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/c-bata/go-prompt v0.2.2 h1:uyKRz6Z6DUyj49QVijyM339UJV9yhbr70gESwbNU3e github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/google/shlex v0.0.0-20150127133951-6f45313302b9 h1:JM174NTeGNJ2m/oLH3UOWOvWQQKd+BoL3hcSCUWFLt0= +github.com/google/shlex v0.0.0-20150127133951-6f45313302b9/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= @@ -14,8 +16,6 @@ github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-shellwords v1.0.3 h1:K/VxK7SZ+cvuPgFSLKi5QPI9Vr/ipOf4C1gN+ntueUk= -github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104 h1:d8RFOZ2IiFtFWBcKEHAFYJcPTf0wY5q0exFNJZVWa1U= github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= diff --git a/vendor/github.com/google/shlex/COPYING b/vendor/github.com/google/shlex/COPYING new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/vendor/github.com/google/shlex/COPYING @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/google/shlex/README b/vendor/github.com/google/shlex/README new file mode 100644 index 0000000..c86bcc0 --- /dev/null +++ b/vendor/github.com/google/shlex/README @@ -0,0 +1,2 @@ +go-shlex is a simple lexer for go that supports shell-style quoting, +commenting, and escaping. diff --git a/vendor/github.com/google/shlex/shlex.go b/vendor/github.com/google/shlex/shlex.go new file mode 100644 index 0000000..3cb37b7 --- /dev/null +++ b/vendor/github.com/google/shlex/shlex.go @@ -0,0 +1,417 @@ +/* +Copyright 2012 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Package shlex implements a simple lexer which splits input in to tokens using +shell-style rules for quoting and commenting. + +The basic use case uses the default ASCII lexer to split a string into sub-strings: + + shlex.Split("one \"two three\" four") -> []string{"one", "two three", "four"} + +To process a stream of strings: + + l := NewLexer(os.Stdin) + for ; token, err := l.Next(); err != nil { + // process token + } + +To access the raw token stream (which includes tokens for comments): + + t := NewTokenizer(os.Stdin) + for ; token, err := t.Next(); err != nil { + // process token + } + +*/ +package shlex + +import ( + "bufio" + "fmt" + "io" + "strings" +) + +// TokenType is a top-level token classification: A word, space, comment, unknown. +type TokenType int + +// runeTokenClass is the type of a UTF-8 character classification: A quote, space, escape. +type runeTokenClass int + +// the internal state used by the lexer state machine +type lexerState int + +// Token is a (type, value) pair representing a lexographical token. +type Token struct { + tokenType TokenType + value string +} + +// Equal reports whether tokens a, and b, are equal. +// Two tokens are equal if both their types and values are equal. A nil token can +// never be equal to another token. +func (a *Token) Equal(b *Token) bool { + if a == nil || b == nil { + return false + } + if a.tokenType != b.tokenType { + return false + } + return a.value == b.value +} + +// Named classes of UTF-8 runes +const ( + spaceRunes = " \t\r\n" + escapingQuoteRunes = `"` + nonEscapingQuoteRunes = "'" + escapeRunes = `\` + commentRunes = "#" +) + +// Classes of rune token +const ( + unknownRuneClass runeTokenClass = iota + spaceRuneClass + escapingQuoteRuneClass + nonEscapingQuoteRuneClass + escapeRuneClass + commentRuneClass + eofRuneClass +) + +// Classes of lexographic token +const ( + UnknownToken TokenType = iota + WordToken + SpaceToken + CommentToken +) + +// Lexer state machine states +const ( + startState lexerState = iota // no runes have been seen + inWordState // processing regular runes in a word + escapingState // we have just consumed an escape rune; the next rune is literal + escapingQuotedState // we have just consumed an escape rune within a quoted string + quotingEscapingState // we are within a quoted string that supports escaping ("...") + quotingState // we are within a string that does not support escaping ('...') + commentState // we are within a comment (everything following an unquoted or unescaped # +) + +// tokenClassifier is used for classifying rune characters. +type tokenClassifier map[rune]runeTokenClass + +func (typeMap tokenClassifier) addRuneClass(runes string, tokenType runeTokenClass) { + for _, runeChar := range runes { + typeMap[runeChar] = tokenType + } +} + +// newDefaultClassifier creates a new classifier for ASCII characters. +func newDefaultClassifier() tokenClassifier { + t := tokenClassifier{} + t.addRuneClass(spaceRunes, spaceRuneClass) + t.addRuneClass(escapingQuoteRunes, escapingQuoteRuneClass) + t.addRuneClass(nonEscapingQuoteRunes, nonEscapingQuoteRuneClass) + t.addRuneClass(escapeRunes, escapeRuneClass) + t.addRuneClass(commentRunes, commentRuneClass) + return t +} + +// ClassifyRune classifiees a rune +func (t tokenClassifier) ClassifyRune(runeVal rune) runeTokenClass { + return t[runeVal] +} + +// Lexer turns an input stream into a sequence of tokens. Whitespace and comments are skipped. +type Lexer Tokenizer + +// NewLexer creates a new lexer from an input stream. +func NewLexer(r io.Reader) *Lexer { + + return (*Lexer)(NewTokenizer(r)) +} + +// Next returns the next word, or an error. If there are no more words, +// the error will be io.EOF. +func (l *Lexer) Next() (string, error) { + for { + token, err := (*Tokenizer)(l).Next() + if err != nil { + return "", err + } + switch token.tokenType { + case WordToken: + return token.value, nil + case CommentToken: + // skip comments + default: + return "", fmt.Errorf("Unknown token type: %v", token.tokenType) + } + } +} + +// Tokenizer turns an input stream into a sequence of typed tokens +type Tokenizer struct { + input bufio.Reader + classifier tokenClassifier +} + +// NewTokenizer creates a new tokenizer from an input stream. +func NewTokenizer(r io.Reader) *Tokenizer { + input := bufio.NewReader(r) + classifier := newDefaultClassifier() + return &Tokenizer{ + input: *input, + classifier: classifier} +} + +// scanStream scans the stream for the next token using the internal state machine. +// It will panic if it encounters a rune which it does not know how to handle. +func (t *Tokenizer) scanStream() (*Token, error) { + state := startState + var tokenType TokenType + var value []rune + var nextRune rune + var nextRuneType runeTokenClass + var err error + + for { + nextRune, _, err = t.input.ReadRune() + nextRuneType = t.classifier.ClassifyRune(nextRune) + + if err == io.EOF { + nextRuneType = eofRuneClass + err = nil + } else if err != nil { + return nil, err + } + + switch state { + case startState: // no runes read yet + { + switch nextRuneType { + case eofRuneClass: + { + return nil, io.EOF + } + case spaceRuneClass: + { + } + case escapingQuoteRuneClass: + { + tokenType = WordToken + state = quotingEscapingState + } + case nonEscapingQuoteRuneClass: + { + tokenType = WordToken + state = quotingState + } + case escapeRuneClass: + { + tokenType = WordToken + state = escapingState + } + case commentRuneClass: + { + tokenType = CommentToken + state = commentState + } + default: + { + tokenType = WordToken + value = append(value, nextRune) + state = inWordState + } + } + } + case inWordState: // in a regular word + { + switch nextRuneType { + case eofRuneClass: + { + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case spaceRuneClass: + { + t.input.UnreadRune() + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case escapingQuoteRuneClass: + { + state = quotingEscapingState + } + case nonEscapingQuoteRuneClass: + { + state = quotingState + } + case escapeRuneClass: + { + state = escapingState + } + default: + { + value = append(value, nextRune) + } + } + } + case escapingState: // the rune after an escape character + { + switch nextRuneType { + case eofRuneClass: + { + err = fmt.Errorf("EOF found after escape character") + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + default: + { + state = inWordState + value = append(value, nextRune) + } + } + } + case escapingQuotedState: // the next rune after an escape character, in double quotes + { + switch nextRuneType { + case eofRuneClass: + { + err = fmt.Errorf("EOF found after escape character") + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + default: + { + state = quotingEscapingState + value = append(value, nextRune) + } + } + } + case quotingEscapingState: // in escaping double quotes + { + switch nextRuneType { + case eofRuneClass: + { + err = fmt.Errorf("EOF found when expecting closing quote") + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case escapingQuoteRuneClass: + { + state = inWordState + } + case escapeRuneClass: + { + state = escapingQuotedState + } + default: + { + value = append(value, nextRune) + } + } + } + case quotingState: // in non-escaping single quotes + { + switch nextRuneType { + case eofRuneClass: + { + err = fmt.Errorf("EOF found when expecting closing quote") + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case nonEscapingQuoteRuneClass: + { + state = inWordState + } + default: + { + value = append(value, nextRune) + } + } + } + case commentState: // in a comment + { + switch nextRuneType { + case eofRuneClass: + { + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case spaceRuneClass: + { + if nextRune == '\n' { + state = startState + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } else { + value = append(value, nextRune) + } + } + default: + { + value = append(value, nextRune) + } + } + } + default: + { + return nil, fmt.Errorf("Unexpected state: %v", state) + } + } + } +} + +// Next returns the next token in the stream. +func (t *Tokenizer) Next() (*Token, error) { + return t.scanStream() +} + +// Split partitions a string into a slice of strings. +func Split(s string) ([]string, error) { + l := NewLexer(strings.NewReader(s)) + subStrings := make([]string, 0) + for { + word, err := l.Next() + if err != nil { + if err == io.EOF { + return subStrings, nil + } + return subStrings, err + } + subStrings = append(subStrings, word) + } +} diff --git a/vendor/github.com/mattn/go-shellwords/.travis.yml b/vendor/github.com/mattn/go-shellwords/.travis.yml deleted file mode 100644 index 16d1430..0000000 --- a/vendor/github.com/mattn/go-shellwords/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: go -go: - - tip -before_install: - - go get github.com/mattn/goveralls - - go get golang.org/x/tools/cmd/cover -script: - - $HOME/gopath/bin/goveralls -repotoken 2FMhp57u8LcstKL9B190fLTcEnBtAAiEL diff --git a/vendor/github.com/mattn/go-shellwords/LICENSE b/vendor/github.com/mattn/go-shellwords/LICENSE deleted file mode 100644 index 740fa93..0000000 --- a/vendor/github.com/mattn/go-shellwords/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Yasuhiro Matsumoto - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/mattn/go-shellwords/README.md b/vendor/github.com/mattn/go-shellwords/README.md deleted file mode 100644 index b1d235c..0000000 --- a/vendor/github.com/mattn/go-shellwords/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# go-shellwords - -[](https://coveralls.io/r/mattn/go-shellwords?branch=master) -[](https://travis-ci.org/mattn/go-shellwords) - -Parse line as shell words. - -## Usage - -```go -args, err := shellwords.Parse("./foo --bar=baz") -// args should be ["./foo", "--bar=baz"] -``` - -```go -os.Setenv("FOO", "bar") -p := shellwords.NewParser() -p.ParseEnv = true -args, err := p.Parse("./foo $FOO") -// args should be ["./foo", "bar"] -``` - -```go -p := shellwords.NewParser() -p.ParseBacktick = true -args, err := p.Parse("./foo `echo $SHELL`") -// args should be ["./foo", "/bin/bash"] -``` - -```go -shellwords.ParseBacktick = true -p := shellwords.NewParser() -args, err := p.Parse("./foo `echo $SHELL`") -// args should be ["./foo", "/bin/bash"] -``` - -# Thanks - -This is based on cpan module [Parse::CommandLine](https://metacpan.org/pod/Parse::CommandLine). - -# License - -under the MIT License: http://mattn.mit-license.org/2017 - -# Author - -Yasuhiro Matsumoto (a.k.a mattn) diff --git a/vendor/github.com/mattn/go-shellwords/shellwords.go b/vendor/github.com/mattn/go-shellwords/shellwords.go deleted file mode 100644 index 1078039..0000000 --- a/vendor/github.com/mattn/go-shellwords/shellwords.go +++ /dev/null @@ -1,145 +0,0 @@ -package shellwords - -import ( - "errors" - "os" - "regexp" -) - -var ( - ParseEnv bool = false - ParseBacktick bool = false -) - -var envRe = regexp.MustCompile(`\$({[a-zA-Z0-9_]+}|[a-zA-Z0-9_]+)`) - -func isSpace(r rune) bool { - switch r { - case ' ', '\t', '\r', '\n': - return true - } - return false -} - -func replaceEnv(s string) string { - return envRe.ReplaceAllStringFunc(s, func(s string) string { - s = s[1:] - if s[0] == '{' { - s = s[1 : len(s)-1] - } - return os.Getenv(s) - }) -} - -type Parser struct { - ParseEnv bool - ParseBacktick bool - Position int -} - -func NewParser() *Parser { - return &Parser{ParseEnv, ParseBacktick, 0} -} - -func (p *Parser) Parse(line string) ([]string, error) { - args := []string{} - buf := "" - var escaped, doubleQuoted, singleQuoted, backQuote bool - backtick := "" - - pos := -1 - got := false - -loop: - for i, r := range line { - if escaped { - buf += string(r) - escaped = false - continue - } - - if r == '\\' { - if singleQuoted { - buf += string(r) - } else { - escaped = true - } - continue - } - - if isSpace(r) { - if singleQuoted || doubleQuoted || backQuote { - buf += string(r) - backtick += string(r) - } else if got { - if p.ParseEnv { - buf = replaceEnv(buf) - } - args = append(args, buf) - buf = "" - got = false - } - continue - } - - switch r { - case '`': - if !singleQuoted && !doubleQuoted { - if p.ParseBacktick { - if backQuote { - out, err := shellRun(backtick) - if err != nil { - return nil, err - } - buf = out - } - backtick = "" - backQuote = !backQuote - continue - } - backtick = "" - backQuote = !backQuote - } - case '"': - if !singleQuoted { - doubleQuoted = !doubleQuoted - continue - } - case '\'': - if !doubleQuoted { - singleQuoted = !singleQuoted - continue - } - case ';', '&', '|', '<', '>': - if !(escaped || singleQuoted || doubleQuoted || backQuote) { - pos = i - break loop - } - } - - got = true - buf += string(r) - if backQuote { - backtick += string(r) - } - } - - if got { - if p.ParseEnv { - buf = replaceEnv(buf) - } - args = append(args, buf) - } - - if escaped || singleQuoted || doubleQuoted || backQuote { - return nil, errors.New("invalid command line string") - } - - p.Position = pos - - return args, nil -} - -func Parse(line string) ([]string, error) { - return NewParser().Parse(line) -} diff --git a/vendor/github.com/mattn/go-shellwords/util_posix.go b/vendor/github.com/mattn/go-shellwords/util_posix.go deleted file mode 100644 index 4f8ac55..0000000 --- a/vendor/github.com/mattn/go-shellwords/util_posix.go +++ /dev/null @@ -1,19 +0,0 @@ -// +build !windows - -package shellwords - -import ( - "errors" - "os" - "os/exec" - "strings" -) - -func shellRun(line string) (string, error) { - shell := os.Getenv("SHELL") - b, err := exec.Command(shell, "-c", line).Output() - if err != nil { - return "", errors.New(err.Error() + ":" + string(b)) - } - return strings.TrimSpace(string(b)), nil -} diff --git a/vendor/github.com/mattn/go-shellwords/util_windows.go b/vendor/github.com/mattn/go-shellwords/util_windows.go deleted file mode 100644 index 7cad4cf..0000000 --- a/vendor/github.com/mattn/go-shellwords/util_windows.go +++ /dev/null @@ -1,17 +0,0 @@ -package shellwords - -import ( - "errors" - "os" - "os/exec" - "strings" -) - -func shellRun(line string) (string, error) { - shell := os.Getenv("COMSPEC") - b, err := exec.Command(shell, "/c", line).Output() - if err != nil { - return "", errors.New(err.Error() + ":" + string(b)) - } - return strings.TrimSpace(string(b)), nil -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 13028f9..d15237f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -4,14 +4,14 @@ github.com/briandowns/spinner github.com/c-bata/go-prompt # github.com/fatih/color v1.7.0 github.com/fatih/color +# github.com/google/shlex v0.0.0-20150127133951-6f45313302b9 +github.com/google/shlex # github.com/mattn/go-colorable v0.0.9 github.com/mattn/go-colorable # github.com/mattn/go-isatty v0.0.4 github.com/mattn/go-isatty # github.com/mattn/go-runewidth v0.0.3 github.com/mattn/go-runewidth -# github.com/mattn/go-shellwords v1.0.3 -github.com/mattn/go-shellwords # github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104 github.com/mattn/go-tty # github.com/mitchellh/go-homedir v1.0.0
