blob: dd0ab46cdb2050c5c14b20e825fe3a458f126d53 [file] [log] [blame]
// 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
stEnd
)
type class int
const (
clBreak class = iota
clNewline
clQuote
clSingle
clDouble
clOther
)
type action int
const (
drop action = iota
push
xpush
emit
)
var update = map[state]map[class]struct {
state
action
}{
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},
},
}
func init() {
update[stNone] = update[stBreak]
}
var byteClass = map[byte]class{
' ': clBreak,
'\t': clBreak,
'\n': clNewline,
'\\': clQuote,
'\'': clSingle,
'"': clDouble,
}
func classOf(b byte) class {
if c, ok := byteClass[b]; ok {
return c
}
return clOther
}
// Split partitions s into fields divided on space, tab, and newline
// characters. Leading and trailing whitespace are discarded before
// splitting. Single and double quotation marks will be handled as described in
// http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02.
//
// The Boolean flag reports whether the split is "valid", meaning there were no
// unclosed quotations in the string.
func Split(s string) ([]string, bool) {
buf := bufio.NewReader(strings.NewReader(strings.TrimSpace(s)))
var cur bytes.Buffer
var ss []string
pop := func() {
ss = append(ss, cur.String())
cur.Reset()
}
st := stNone
for {
c, err := buf.ReadByte()
if err == io.EOF {
break
} else if err != nil {
panic(err)
}
next := update[st][classOf(c)]
switch next.action {
case push:
cur.WriteByte(c)
case xpush:
cur.Write([]byte{'\\', c})
case emit:
pop()
case drop:
break
default:
panic("unknown action")
}
st = next.state
}
if st != stNone {
pop()
}
return ss, st == stNone || st == stWord
}
func quotable(s string) (hasQ, hasOther bool) {
for i := 0; i < len(s); i++ {
hasQ = hasQ || s[i] == '\''
hasOther = hasOther || strings.IndexByte(allQuote, s[i]) >= 0
}
return
}
// Quote returns a copy of s in which shell metacharacters are quoted to
// protect them from evaluation.
func Quote(s string) string {
if s == "" {
return "''"
}
hasQ, hasOther := quotable(s)
if !hasQ && !hasOther {
return s // fast path: nothing needs quotation
}
var buf bytes.Buffer
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))
for i, s := range ss {
quoted[i] = Quote(s)
}
return strings.Join(quoted, " ")
}