pax_global_header00006660000000000000000000000064145512577250014527gustar00rootroot0000000000000052 comment=3b4c6fd59526ecacb8399a2b21d753ed754d41f3 golang-bitbucket-creachadair-shell-0.0.8/000077500000000000000000000000001455125772500203065ustar00rootroot00000000000000golang-bitbucket-creachadair-shell-0.0.8/LICENSE000066400000000000000000000027431455125772500213210ustar00rootroot00000000000000Copyright (c) 2015, Michael J. Fromberger All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. golang-bitbucket-creachadair-shell-0.0.8/README.md000066400000000000000000000002001455125772500215550ustar00rootroot00000000000000# shell http://godoc.org/bitbucket.org/creachadair/shell The `shell` package implements basic shell command-line splitting. golang-bitbucket-creachadair-shell-0.0.8/bench_test.go000066400000000000000000000025551455125772500227620ustar00rootroot00000000000000package shell_test import ( "bytes" "fmt" "math" "math/rand" "strings" "testing" "bitbucket.org/creachadair/shell" ) var input string // Generate a long random string with balanced quotations for perf testing. func init() { var buf bytes.Buffer src := rand.NewSource(12345) r := rand.New(src) const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789 \t\n\n\n" pick := func(f float64) byte { pos := math.Ceil(f*float64(len(alphabet))) - 1 return alphabet[int(pos)] } const inputLen = 100000 var quote struct { q byte n int } for i := 0; i < inputLen; i++ { if quote.n == 0 { q := r.Float64() if q < .1 { quote.q = '"' quote.n = r.Intn(256) buf.WriteByte('"') continue } else if q < .15 { quote.q = '\'' quote.n = r.Intn(256) buf.WriteByte('\'') continue } } buf.WriteByte(pick(r.Float64())) if quote.n > 0 { quote.n-- if quote.n == 0 { buf.WriteByte(quote.q) } } } input = buf.String() } func BenchmarkSplit(b *testing.B) { var lens []int for i := 1; i < len(input); i *= 4 { lens = append(lens, i) } lens = append(lens, len(input)) b.ResetTimer() for _, n := range lens { b.Run(fmt.Sprintf("len_%d", n), func(b *testing.B) { for i := 0; i < b.N; i++ { shell.NewScanner(strings.NewReader(input[:n])).Each(ignore) } }) } } func ignore(string) bool { return true } golang-bitbucket-creachadair-shell-0.0.8/bitbucket-pipelines.yml000066400000000000000000000013741455125772500250000ustar00rootroot00000000000000definitions: steps: - step: &Verify script: - PACKAGE_PATH="${GOPATH}/src/bitbucket.org/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}" - mkdir -pv "${PACKAGE_PATH}" - tar -cO --exclude-vcs --exclude=bitbucket-pipelines.yml . | tar -xv -C "${PACKAGE_PATH}" - cd "${PACKAGE_PATH}" - go version # log the version of Go we are using in this step - export GO111MODULE=on # enable modules inside $GOPATH - go get -v ./... - go build -v ./... - go test -v -race -cpu=1,4 ./... - go vet -v ./... pipelines: default: # run on each push - step: image: golang:1.20 <<: *Verify - step: image: golang:1.21 <<: *Verify golang-bitbucket-creachadair-shell-0.0.8/go.mod000066400000000000000000000001311455125772500214070ustar00rootroot00000000000000module bitbucket.org/creachadair/shell go 1.20 require github.com/google/go-cmp v0.6.0 golang-bitbucket-creachadair-shell-0.0.8/go.sum000066400000000000000000000002471455125772500214440ustar00rootroot00000000000000github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang-bitbucket-creachadair-shell-0.0.8/shell.go000066400000000000000000000170171455125772500217520ustar00rootroot00000000000000// Package shell supports splitting and joining of shell command strings. // // The Split function divides a string into whitespace-separated fields, // respecting single and double quotation marks as defined by the Shell Command // Language section of IEEE Std 1003.1 2013. The Quote function quotes // characters that would otherwise be subject to shell evaluation, and the Join // function concatenates quoted strings with spaces between them. // // The relationship between Split and Join is that given // // fields, ok := Split(Join(ss)) // // the following relationship will hold: // // fields == ss && ok package shell import ( "bufio" "bytes" "io" "strings" ) // These characters must be quoted to escape special meaning. This list // doesn't include the single quote. const mustQuote = "|&;<>()$`\\\"\t\n" // These characters should be quoted to escape special meaning, since in some // contexts they are special (e.g., "x=y" in command position, "*" for globs). const shouldQuote = `*?[#~=%` // These are the separator characters in unquoted text. const spaces = " \t\n" const allQuote = mustQuote + shouldQuote + spaces type state int const ( stNone state = iota stBreak stBreakQ stWord stWordQ stSingle stDouble stDoubleQ ) type class int const ( clOther class = iota clBreak clNewline clQuote clSingle clDouble ) type action int const ( drop action = iota push xpush emit ) // N.B. Benchmarking shows that array lookup is substantially faster than map // lookup here, but it requires caution when changing the state machine. In // particular: // // 1. The state and action values must be small integers. // 2. The update table must completely cover the state values. // 3. Each action slice must completely cover the action values. var update = [...][]struct { state action }{ stNone: {}, stBreak: { clBreak: {stBreak, drop}, clNewline: {stBreak, drop}, clQuote: {stBreakQ, drop}, clSingle: {stSingle, drop}, clDouble: {stDouble, drop}, clOther: {stWord, push}, }, stBreakQ: { clBreak: {stWord, push}, clNewline: {stBreak, drop}, clQuote: {stWord, push}, clSingle: {stWord, push}, clDouble: {stWord, push}, clOther: {stWord, push}, }, stWord: { clBreak: {stBreak, emit}, clNewline: {stBreak, emit}, clQuote: {stWordQ, drop}, clSingle: {stSingle, drop}, clDouble: {stDouble, drop}, clOther: {stWord, push}, }, stWordQ: { clBreak: {stWord, push}, clNewline: {stWord, drop}, clQuote: {stWord, push}, clSingle: {stWord, push}, clDouble: {stWord, push}, clOther: {stWord, push}, }, stSingle: { clBreak: {stSingle, push}, clNewline: {stSingle, push}, clQuote: {stSingle, push}, clSingle: {stWord, drop}, clDouble: {stSingle, push}, clOther: {stSingle, push}, }, stDouble: { clBreak: {stDouble, push}, clNewline: {stDouble, push}, clQuote: {stDoubleQ, drop}, clSingle: {stDouble, push}, clDouble: {stWord, drop}, clOther: {stDouble, push}, }, stDoubleQ: { clBreak: {stDouble, xpush}, clNewline: {stDouble, drop}, clQuote: {stDouble, push}, clSingle: {stDouble, xpush}, clDouble: {stDouble, push}, clOther: {stDouble, xpush}, }, } var classOf = [256]class{ ' ': clBreak, '\t': clBreak, '\n': clNewline, '\\': clQuote, '\'': clSingle, '"': clDouble, } // A Scanner partitions input from a reader into tokens divided on space, tab, // and newline characters. Single and double quotation marks are handled as // described in http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02. type Scanner struct { buf *bufio.Reader cur bytes.Buffer st state err error } // NewScanner returns a Scanner that reads input from r. func NewScanner(r io.Reader) *Scanner { return &Scanner{ buf: bufio.NewReader(r), st: stBreak, } } // Next advances the scanner and reports whether there are any further tokens // to be consumed. func (s *Scanner) Next() bool { if s.err != nil { return false } s.cur.Reset() for { c, err := s.buf.ReadByte() s.err = err if err == io.EOF { break } else if err != nil { return false } next := update[s.st][classOf[c]] s.st = next.state switch next.action { case push: s.cur.WriteByte(c) case xpush: s.cur.Write([]byte{'\\', c}) case emit: return true // s.cur has a complete token case drop: continue default: panic("unknown action") } } return s.st != stBreak } // Text returns the text of the current token, or "" if there is none. func (s *Scanner) Text() string { return s.cur.String() } // Err returns the error, if any, that resulted from the most recent action. func (s *Scanner) Err() error { return s.err } // Complete reports whether the current token is complete, meaning that it is // unquoted or its quotes were balanced. func (s *Scanner) Complete() bool { return s.st == stBreak || s.st == stWord } // Rest returns an io.Reader for the remainder of the unconsumed input in s. // After calling this method, Next will always return false. The remainder // does not include the text of the current token at the time Rest is called. func (s *Scanner) Rest() io.Reader { s.st = stNone s.cur.Reset() s.err = io.EOF return s.buf } // Each calls f for each token in the scanner until the input is exhausted, f // returns false, or an error occurs. func (s *Scanner) Each(f func(tok string) bool) error { for s.Next() { if !f(s.Text()) { return nil } } if err := s.Err(); err != io.EOF { return err } return nil } // Split returns the remaining tokens in s, not including the current token if // there is one. Any tokens already consumed are still returned, even if there // is an error. func (s *Scanner) Split() []string { var tokens []string for s.Next() { tokens = append(tokens, s.Text()) } return tokens } // Split partitions s into tokens divided on space, tab, and newline characters // using a *Scanner. Leading and trailing whitespace are ignored. // // The Boolean flag reports whether the final token is "valid", meaning there // were no unclosed quotations in the string. func Split(s string) ([]string, bool) { sc := NewScanner(strings.NewReader(s)) ss := sc.Split() return ss, sc.Complete() } func quotable(s string) (hasQ, hasOther bool) { const ( quote = 1 other = 2 all = quote + other ) var v uint for i := 0; i < len(s) && v < all; i++ { if s[i] == '\'' { v |= quote } else if strings.IndexByte(allQuote, s[i]) >= 0 { v |= other } } return v"e != 0, v&other != 0 } // Quote returns a copy of s in which shell metacharacters are quoted to // protect them from evaluation. func Quote(s string) string { var buf bytes.Buffer return quote(s, &buf) } // quote implements quotation, using the provided buffer as scratch space. The // existing contents of the buffer are clobbered. func quote(s string, buf *bytes.Buffer) string { if s == "" { return "''" } hasQ, hasOther := quotable(s) if !hasQ && !hasOther { return s // fast path: nothing needs quotation } buf.Reset() inq := false for i := 0; i < len(s); i++ { ch := s[i] if ch == '\'' { if inq { buf.WriteByte('\'') inq = false } buf.WriteByte('\\') } else if !inq && hasOther { buf.WriteByte('\'') inq = true } buf.WriteByte(ch) } if inq { buf.WriteByte('\'') } return buf.String() } // Join quotes each element of ss with Quote and concatenates the resulting // strings separated by spaces. func Join(ss []string) string { quoted := make([]string, len(ss)) var buf bytes.Buffer for i, s := range ss { quoted[i] = quote(s, &buf) } return strings.Join(quoted, " ") } golang-bitbucket-creachadair-shell-0.0.8/shell_test.go000066400000000000000000000136271455125772500230140ustar00rootroot00000000000000package shell import ( "fmt" "io" "log" "strings" "testing" "github.com/google/go-cmp/cmp" ) func TestQuote(t *testing.T) { type testCase struct{ in, want string } tests := []testCase{ {"", "''"}, // empty is special {"abc", "abc"}, // nothing to quote {"--flag", "--flag"}, // " {"'abc", `\'abc`}, // single quote only {"abc'", `abc\'`}, // " {`shan't`, `shan\'t`}, // " {"--flag=value", `'--flag=value'`}, {"a b\tc", "'a b\tc'"}, {`a"b"c`, `'a"b"c'`}, {`'''`, `\'\'\'`}, {`\`, `'\'`}, {`'a=b`, `\''a=b'`}, // quotes and other stuff {`a='b`, `'a='\''b'`}, // " {`a=b'`, `'a=b'\'`}, // " } // Verify that all the designated special characters get quoted. for _, c := range shouldQuote + mustQuote { tests = append(tests, testCase{ in: string(c), want: fmt.Sprintf(`'%c'`, c), }) } for _, test := range tests { got := Quote(test.in) if got != test.want { t.Errorf("Quote %q: got %q, want %q", test.in, got, test.want) } } } func TestSplit(t *testing.T) { tests := []struct { in string want []string ok bool }{ // Variations of empty input yield an empty split. {"", nil, true}, {" ", nil, true}, {"\t", nil, true}, {"\n ", nil, true}, // Various escape sequences work properly. {`\ `, []string{" "}, true}, {`a\ `, []string{"a "}, true}, {`\\a`, []string{`\a`}, true}, {`"a\"b"`, []string{`a"b`}, true}, {`'\'`, []string{"\\"}, true}, // Leading and trailing whitespace are discarded correctly. {"a", []string{"a"}, true}, {" a", []string{"a"}, true}, {"a\n", []string{"a"}, true}, // Escaped newlines are magic in the correct ways. {"a\\\nb", []string{"ab"}, true}, {"a \\\n b\tc", []string{"a", "b", "c"}, true}, // Various splits with and without quotes. Quoted whitespace is // preserved. {"a b c", []string{"a", "b", "c"}, true}, {`a 'b c'`, []string{"a", "b c"}, true}, {"\"a\nb\"cd e'f'", []string{"a\nbcd", "ef"}, true}, {"'\n \t '", []string{"\n \t "}, true}, // Quoted empty strings are preserved in various places. {"''", []string{""}, true}, {"a ''", []string{"a", ""}, true}, {" a \"\" b ", []string{"a", "", "b"}, true}, {"'' a", []string{"", "a"}, true}, // Unbalanced quotation marks and escapes are detected. {"\\", []string{""}, false}, // escape without a target {"'", []string{""}, false}, // unclosed single {`"`, []string{""}, false}, // unclosed double {`'\''`, []string{`\`}, false}, // unclosed connected double {`"\\" '`, []string{`\`, ``}, false}, // unclosed separate single {"a 'b c", []string{"a", "b c"}, false}, {`a "b c`, []string{"a", "b c"}, false}, {`a "b \"`, []string{"a", `b "`}, false}, } for _, test := range tests { got, ok := Split(test.in) if ok != test.ok { t.Errorf("Split %#q: got valid=%v, want %v", test.in, ok, test.ok) } if diff := cmp.Diff(test.want, got); diff != "" { t.Errorf("Split %#q: (-want, +got)\n%s", test.in, diff) } } } func TestScannerSplit(t *testing.T) { tests := []struct { in string want, rest []string }{ {"", nil, nil}, {" ", nil, nil}, {"--", nil, nil}, {"a -- b", []string{"a"}, []string{"b"}}, {"a b c -- d -- e ", []string{"a", "b", "c"}, []string{"d", "--", "e"}}, {`"a b c --" -- "d "`, []string{"a b c --"}, []string{"d "}}, {` -- "foo`, nil, []string{"foo"}}, // unterminated {"cmd -flag -- arg1 arg2", []string{"cmd", "-flag"}, []string{"arg1", "arg2"}}, } for _, test := range tests { t.Logf("Scanner split input: %q", test.in) s := NewScanner(strings.NewReader(test.in)) var got, rest []string for s.Next() { if s.Text() == "--" { rest = s.Split() break } got = append(got, s.Text()) } if s.Err() != io.EOF { t.Errorf("Unexpected scan error: %v", s.Err()) } if diff := cmp.Diff(test.want, got); diff != "" { t.Errorf("Scanner split prefix: (-want, +got)\n%s", diff) } if diff := cmp.Diff(test.rest, rest); diff != "" { t.Errorf("Scanner split suffix: (-want, +got)\n%s", diff) } } } func TestRoundTrip(t *testing.T) { tests := [][]string{ nil, {"a"}, {"a "}, {"a", "b", "c"}, {"a", "b c"}, {"--flag=value"}, {"m='$USER'", "nop+", "$$"}, {`"a" b `, "c"}, {"odd's", "bodkins", "x'", "x''", "x\"\"", "$x':y"}, {"a=b", "--foo", "${bar}", `\$`}, {"cat", "a${b}.txt", "|", "tee", "capture", "2>", "/dev/null"}, } for _, test := range tests { s := Join(test) t.Logf("Join %#q = %v", test, s) got, ok := Split(s) if !ok { t.Errorf("Split %+q: should be valid, but is not", s) } if diff := cmp.Diff(test, got); diff != "" { t.Errorf("Split %+q: (-want, +got)\n%s", s, diff) } } } func ExampleScanner() { const input = `a "free range" exploration of soi\ disant novelties` s := NewScanner(strings.NewReader(input)) sum, count := 0, 0 for s.Next() { count++ sum += len(s.Text()) } fmt.Println(len(input), count, sum, s.Complete(), s.Err()) // Output: 51 6 43 true EOF } func ExampleScanner_Rest() { const input = `things 'and stuff' %end% all the remaining stuff` s := NewScanner(strings.NewReader(input)) for s.Next() { if s.Text() == "%end%" { fmt.Print("found marker; ") break } } rest, err := io.ReadAll(s.Rest()) if err != nil { log.Fatal(err) } fmt.Println(string(rest)) // Output: found marker; all the remaining stuff } func ExampleScanner_Each() { const input = `a\ b 'c d' "e f's g" stop "go directly to jail"` if err := NewScanner(strings.NewReader(input)).Each(func(tok string) bool { fmt.Println(tok) return tok != "stop" }); err != nil { log.Fatal(err) } // Output: // a b // c d // e f's g // stop } func ExampleScanner_Split() { const input = `cmd -flag=t -- foo bar baz` s := NewScanner(strings.NewReader(input)) for s.Next() { if s.Text() == "--" { fmt.Println("** Args:", strings.Join(s.Split(), ", ")) } else { fmt.Println(s.Text()) } } // Output: // cmd // -flag=t // ** Args: foo, bar, baz }