├── .gitignore ├── LICENSE ├── README.md ├── glide.lock ├── glide.yaml ├── parser.go ├── parser_test.go └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,windows,go 3 | 4 | ### OSX ### 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | 32 | ### Linux ### 33 | *~ 34 | 35 | # temporary files which can be created if a process still has a handle open of a deleted file 36 | .fuse_hidden* 37 | 38 | # KDE directory preferences 39 | .directory 40 | 41 | # Linux trash folder which might appear on any partition or disk 42 | .Trash-* 43 | 44 | 45 | ### Windows ### 46 | # Windows image file caches 47 | Thumbs.db 48 | ehthumbs.db 49 | 50 | # Folder config file 51 | Desktop.ini 52 | 53 | # Recycle Bin used on file shares 54 | $RECYCLE.BIN/ 55 | 56 | # Windows Installer files 57 | *.cab 58 | *.msi 59 | *.msm 60 | *.msp 61 | 62 | # Windows shortcuts 63 | *.lnk 64 | 65 | ### Go ### 66 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 67 | *.o 68 | *.a 69 | *.so 70 | 71 | # Folders 72 | _obj 73 | _test 74 | 75 | # Architecture specific extensions/prefixes 76 | *.[568vq] 77 | [568vq].out 78 | 79 | *.cgo1.go 80 | *.cgo2.c 81 | _cgo_defun.c 82 | _cgo_gotypes.go 83 | _cgo_export.* 84 | 85 | _testmain.go 86 | 87 | *.exe 88 | *.test 89 | *.prof 90 | 91 | # Output of the go coverage tool, specifically when used with LiteIDE 92 | *.out 93 | 94 | # External packages folder 95 | vendor/ 96 | 97 | coverage.txt 98 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tarcísio Gruppi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/txgruppi/parseargs-go) 2 | [![Codeship](https://img.shields.io/codeship/173b62f0-bcc9-0133-0239-6e8926ac3d5c/master.svg?style=flat-square)](https://codeship.com/projects/136367) 3 | [![Codecov](https://img.shields.io/codecov/c/github/txgruppi/parseargs-go/master.svg?style=flat-square)](https://codecov.io/github/txgruppi/parseargs-go) 4 | [![Go Report Card](https://img.shields.io/badge/go_report-A+-brightgreen.svg?style=flat-square)](https://goreportcard.com/report/github.com/txgruppi/parseargs-go) 5 | 6 | # `parseargs-go` 7 | 8 | This is a port of the [parserargs.js](https://github.com/txgruppi/parseargs.js) project to [Go](https://golang.org). 9 | 10 | What about parsing arguments allowing quotes in them? But beware that this library will not parse flags (-- and -), flags will be returned as simple strings. 11 | 12 | ## Installation 13 | 14 | `go get -u github.com/txgruppi/parseargs-go` 15 | 16 | ## Example 17 | 18 | ```go 19 | package main 20 | 21 | import ( 22 | "fmt" 23 | "log" 24 | 25 | "github.com/txgruppi/parseargs-go" 26 | ) 27 | 28 | func main() { 29 | setInRedis := `set name "Put your name here"` 30 | parsed, err := parseargs.Parse(setInRedis) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | fmt.Printf("%#v\n", parsed) // []string{"set", "name", "Put your name here"} 35 | } 36 | ``` 37 | 38 | ## Tests 39 | 40 | ``` 41 | go get -u -t github.com/txgruppi/parseargs-go 42 | cd $GOPATH/src/github.com/txgruppi/parseargs-go 43 | go test ./... 44 | ``` 45 | 46 | ## License 47 | 48 | MIT 49 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 9c9ba62582a03059f242e0054f84861558d09787fe3406faa5206fca9759b870 2 | updated: 2017-01-24T19:50:54.717937216-02:00 3 | imports: [] 4 | testImports: 5 | - name: github.com/gopherjs/gopherjs 6 | version: 2967252ace8b112e63a5b5879e92de915fe731f4 7 | subpackages: 8 | - js 9 | - name: github.com/jtolds/gls 10 | version: bb0351aa7eb6f322f32667d51375f26a2bca6628 11 | - name: github.com/smartystreets/assertions 12 | version: 26acb9229f421449ac63d014995b282d59261a8b 13 | subpackages: 14 | - internal/go-render/render 15 | - internal/oglematchers 16 | - name: github.com/smartystreets/goconvey 17 | version: d4c757aa9afd1e2fc1832aaab209b5794eb336e1 18 | subpackages: 19 | - convey 20 | - convey/gotest 21 | - convey/reporting 22 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/txgruppi/parseargs-go 2 | import: [] 3 | testImport: 4 | - package: github.com/smartystreets/goconvey 5 | version: ~1.6.2 6 | subpackages: 7 | - convey 8 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package parseargs 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | whitespaceRegexp = regexp.MustCompile("\\s") 11 | specialCharsRegexp = regexp.MustCompile(`\s|"|'`) 12 | backSlashRemovalRegexp = regexp.MustCompile(`\\([\s"'\\])`) 13 | 14 | // ErrInvalidArgument is the error returned when an unexpected character 15 | // is found by the parser. 16 | ErrInvalidArgument = errors.New("invalid argument(s)") 17 | 18 | // ErrInvalidSyntax is the error returned when some of the syntax rules are 19 | // violeted by the input. 20 | ErrInvalidSyntax = errors.New("invalid syntax") 21 | 22 | // ErrUnexpectedEndOfInput is the error returned when the parser gets to the 23 | // end of the string with an unfinished string. 24 | ErrUnexpectedEndOfInput = errors.New("unexpected end of input") 25 | ) 26 | 27 | // Parse parses a string into a list or arguments. The default argument 28 | // separator is one or a sequence of whitespaces but it also understands 29 | // quotted string and escaped quotes. 30 | func Parse(input string) ([]string, error) { 31 | return newParser(input).parse() 32 | } 33 | 34 | func newParser(input string) *parser { 35 | runes := []rune(strings.TrimSpace(input)) 36 | return &parser{ 37 | runes: runes, 38 | length: len(runes), 39 | } 40 | } 41 | 42 | type parser struct { 43 | runes []rune 44 | length int 45 | reading bool 46 | startChar rune 47 | startIndex int 48 | } 49 | 50 | func (p *parser) parse() ([]string, error) { 51 | result := []string{} 52 | for index, current := range p.runes { 53 | if p.checkInvalidArgument(current) { 54 | return nil, ErrInvalidArgument 55 | } 56 | 57 | if p.shouldStartReadingWord(current) { 58 | p.reading = true 59 | p.startChar = ' ' 60 | p.startIndex = index 61 | 62 | if p.shouldFinishReadingAtEndOfInput(index) { 63 | result = append(result, string(p.read(p.startIndex, p.length))) 64 | } 65 | continue 66 | } 67 | 68 | if p.shouldStartReadingQuottedString(current) { 69 | p.reading = true 70 | p.startChar = current 71 | p.startIndex = index 72 | continue 73 | } 74 | 75 | if !p.reading { 76 | continue 77 | } 78 | 79 | if p.shouldFinishReadingWord(current) { 80 | if !p.hasValidBackslash(index) { 81 | return nil, ErrInvalidSyntax 82 | } 83 | result = append(result, string(p.read(p.startIndex, index))) 84 | continue 85 | } 86 | 87 | if p.shouldFinishReadingQuottedString(index, current) { 88 | result = append(result, string(p.read(p.startIndex+1, index))) 89 | continue 90 | } 91 | 92 | if p.shouldFinishReadingAtEndOfInput(index) { 93 | result = append(result, string(p.read(p.startIndex, p.length))) 94 | continue 95 | } 96 | } 97 | 98 | if p.hasEndedUnexpectedly() { 99 | return nil, ErrUnexpectedEndOfInput 100 | } 101 | 102 | return p.cleanUpResult(result), nil 103 | } 104 | 105 | func (p *parser) shouldFinishReadingAtEndOfInput(index int) bool { 106 | return p.isEndOfInput(index) && p.startChar == ' ' 107 | } 108 | 109 | func (p *parser) cleanUpResult(result []string) []string { 110 | for index, value := range result { 111 | result[index] = backSlashRemovalRegexp.ReplaceAllString(value, "$1") 112 | } 113 | return result 114 | } 115 | 116 | func (p *parser) hasEndedUnexpectedly() bool { 117 | return p.startIndex >= 0 || p.startChar != 0 118 | } 119 | 120 | func (p *parser) shouldFinishReadingQuottedString(index int, char rune) bool { 121 | return p.startChar == char && p.isSpecial(p.startChar) && p.hasValidBackslash(index) 122 | } 123 | 124 | func (p *parser) shouldFinishReadingWord(char rune) bool { 125 | return p.startChar == ' ' && p.isWhitespace(char) 126 | } 127 | 128 | func (p *parser) shouldStartReadingQuottedString(char rune) bool { 129 | return !p.reading && p.isSpecial(char) && !p.isWhitespace(char) 130 | } 131 | 132 | func (p *parser) isEndOfInput(index int) bool { 133 | return index == p.length-1 134 | } 135 | 136 | func (p *parser) shouldStartReadingWord(char rune) bool { 137 | return !(p.reading || p.isSpecial(char)) 138 | } 139 | 140 | func (p *parser) checkInvalidArgument(char rune) bool { 141 | return p.reading && p.startChar == ' ' && p.isSpecial(char) && !p.isWhitespace(char) 142 | } 143 | 144 | func (p *parser) read(start, end int) []rune { 145 | p.reading = false 146 | p.startChar = 0 147 | p.startIndex = -1 148 | return p.runes[start:end] 149 | } 150 | 151 | func (p *parser) isWhitespace(r rune) bool { 152 | return whitespaceRegexp.MatchString(string(r)) 153 | } 154 | 155 | func (p *parser) isSpecial(r rune) bool { 156 | return specialCharsRegexp.MatchString(string(r)) 157 | } 158 | 159 | func (p *parser) hasValidBackslash(index int) bool { 160 | counter := 0 161 | 162 | for { 163 | if index-1-counter < 0 { 164 | break 165 | } 166 | 167 | if p.runes[index-1-counter] == '\\' { 168 | counter++ 169 | continue 170 | } 171 | 172 | break 173 | } 174 | 175 | return counter%2 == 0 176 | } 177 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package parseargs_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | "github.com/txgruppi/parseargs-go" 8 | ) 9 | 10 | func TestParser(t *testing.T) { 11 | Convey("valid", t, func() { 12 | Convey("should understand simple words", func() { 13 | input := `my string of arguments` 14 | expected := []string{`my`, `string`, `of`, `arguments`} 15 | actual, err := parseargs.Parse(input) 16 | So(err, ShouldBeNil) 17 | So(actual, ShouldResemble, expected) 18 | }) 19 | 20 | Convey("should understand single quote", func() { 21 | input := `my 'string of arguments'` 22 | expected := []string{`my`, `string of arguments`} 23 | actual, err := parseargs.Parse(input) 24 | So(err, ShouldBeNil) 25 | So(actual, ShouldResemble, expected) 26 | }) 27 | 28 | Convey("should understand double quote", func() { 29 | input := `"my string" of arguments` 30 | expected := []string{`my string`, `of`, `arguments`} 31 | actual, err := parseargs.Parse(input) 32 | So(err, ShouldBeNil) 33 | So(actual, ShouldResemble, expected) 34 | }) 35 | 36 | Convey("should understand escaped single quote", func() { 37 | input := `my 'string \'of\'' arguments` 38 | expected := []string{`my`, `string 'of'`, `arguments`} 39 | actual, err := parseargs.Parse(input) 40 | So(err, ShouldBeNil) 41 | So(actual, ShouldResemble, expected) 42 | }) 43 | 44 | Convey("should understand escaped double quote", func() { 45 | input := `my "string \"of\"" arguments` 46 | expected := []string{`my`, `string "of"`, `arguments`} 47 | actual, err := parseargs.Parse(input) 48 | So(err, ShouldBeNil) 49 | So(actual, ShouldResemble, expected) 50 | }) 51 | 52 | Convey("should understand double quotes inside single quotted string", func() { 53 | input := `my 'string "of" arguments'` 54 | expected := []string{`my`, `string "of" arguments`} 55 | actual, err := parseargs.Parse(input) 56 | So(err, ShouldBeNil) 57 | So(actual, ShouldResemble, expected) 58 | }) 59 | 60 | Convey("should understand single quotes inside double quotted string", func() { 61 | input := `my "string 'of' arguments"` 62 | expected := []string{`my`, `string 'of' arguments`} 63 | actual, err := parseargs.Parse(input) 64 | So(err, ShouldBeNil) 65 | So(actual, ShouldResemble, expected) 66 | }) 67 | 68 | Convey("should ignore consecutive spaces", func() { 69 | input := `my string of arguments` 70 | expected := []string{`my`, `string`, `of`, `arguments`} 71 | actual, err := parseargs.Parse(input) 72 | So(err, ShouldBeNil) 73 | So(actual, ShouldResemble, expected) 74 | }) 75 | 76 | Convey("should accept tabs, newlines and cartridge returns as spaces", func() { 77 | input := "my\tstring\nof\rarguments" 78 | expected := []string{`my`, `string`, `of`, `arguments`} 79 | actual, err := parseargs.Parse(input) 80 | So(err, ShouldBeNil) 81 | So(actual, ShouldResemble, expected) 82 | }) 83 | 84 | Convey("should read a one char word at the end of the input", func() { 85 | input := `my string of arguments 0` 86 | expected := []string{`my`, `string`, `of`, `arguments`, `0`} 87 | actual, err := parseargs.Parse(input) 88 | So(err, ShouldBeNil) 89 | So(actual, ShouldResemble, expected) 90 | }) 91 | }) 92 | 93 | Convey("invalid", t, func() { 94 | Convey("should not allow for a quotted string to start right after a word", func() { 95 | input := `my"string" of arguments` 96 | expected := `invalid argument(s)` 97 | _, err := parseargs.Parse(input) 98 | So(err, ShouldNotBeNil) 99 | So(err.Error(), ShouldEqual, expected) 100 | }) 101 | 102 | Convey("should detect unexpected EOF", func() { 103 | input := `my "string of arguments` 104 | expected := `unexpected end of input` 105 | _, err := parseargs.Parse(input) 106 | So(err, ShouldNotBeNil) 107 | So(err.Error(), ShouldEqual, expected) 108 | }) 109 | 110 | Convey("should detect wrongly escaped quotes", func() { 111 | input := `my \\"string\\" of arguments` 112 | expected := `invalid argument(s)` 113 | _, err := parseargs.Parse(input) 114 | So(err, ShouldNotBeNil) 115 | So(err.Error(), ShouldEqual, expected) 116 | }) 117 | 118 | Convey("should not allow escaped spaces", func() { 119 | input := `my\ string of arguments` 120 | expected := `invalid syntax` 121 | _, err := parseargs.Parse(input) 122 | So(err, ShouldNotBeNil) 123 | So(err.Error(), ShouldEqual, expected) 124 | }) 125 | }) 126 | } 127 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "mode: atomic" > coverage.txt 6 | counter=0 7 | for package in $(find . -iname '*.go' | grep -v '^./vendor' | xargs -n 1 dirname | sort -n | uniq -c | awk '{print $2}'); do 8 | out="${counter}.txt" 9 | go test -v -coverprofile="$out" -covermode=atomic "$package" 10 | tail -n +2 "$out" >> coverage.txt 11 | rm "$out" 12 | counter=$(expr "$counter" + 1) 13 | done 14 | --------------------------------------------------------------------------------