refactor(shellquote): replace go-shellquote with custom shell quoting implementation
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package shellquote
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnterminatedSingleQuote = errors.New("unterminated single-quoted string")
|
||||
ErrUnterminatedDoubleQuote = errors.New("unterminated double-quoted string")
|
||||
ErrUnterminatedEscape = errors.New("unterminated backslash-escape")
|
||||
)
|
||||
|
||||
type state int
|
||||
|
||||
const (
|
||||
stateUnquoted state = iota
|
||||
stateSingleQuoted
|
||||
stateDoubleQuoted
|
||||
)
|
||||
|
||||
// Split splits a string into words following POSIX-like shell quoting rules.
|
||||
// It handles single quotes, double quotes, and backslash escapes.
|
||||
func Split(input string) ([]string, error) {
|
||||
var words []string
|
||||
var word strings.Builder
|
||||
inWord := false
|
||||
parseState := stateUnquoted
|
||||
|
||||
i := 0
|
||||
for i < len(input) {
|
||||
ch := input[i]
|
||||
|
||||
switch parseState {
|
||||
case stateUnquoted:
|
||||
switch {
|
||||
case ch == '\\':
|
||||
if i+1 >= len(input) {
|
||||
return nil, ErrUnterminatedEscape
|
||||
}
|
||||
if input[i+1] == '\n' {
|
||||
// Line continuation: skip both backslash and newline
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
i++
|
||||
word.WriteByte(input[i])
|
||||
inWord = true
|
||||
case ch == '\'':
|
||||
parseState = stateSingleQuoted
|
||||
inWord = true
|
||||
case ch == '"':
|
||||
parseState = stateDoubleQuoted
|
||||
inWord = true
|
||||
case ch == ' ' || ch == '\t' || ch == '\n':
|
||||
if inWord {
|
||||
words = append(words, word.String())
|
||||
word.Reset()
|
||||
inWord = false
|
||||
}
|
||||
default:
|
||||
word.WriteByte(ch)
|
||||
inWord = true
|
||||
}
|
||||
|
||||
case stateSingleQuoted:
|
||||
if ch == '\'' {
|
||||
parseState = stateUnquoted
|
||||
} else {
|
||||
word.WriteByte(ch)
|
||||
}
|
||||
|
||||
case stateDoubleQuoted:
|
||||
switch {
|
||||
case ch == '"':
|
||||
parseState = stateUnquoted
|
||||
case ch == '\\':
|
||||
if i+1 >= len(input) {
|
||||
return nil, ErrUnterminatedEscape
|
||||
}
|
||||
next := input[i+1]
|
||||
// In double quotes, backslash only escapes: $ ` " \n \
|
||||
if next == '$' || next == '`' || next == '"' || next == '\n' || next == '\\' {
|
||||
if next == '\n' {
|
||||
// Line continuation: skip both backslash and newline
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
i++
|
||||
word.WriteByte(next)
|
||||
} else {
|
||||
// Backslash is literal for other characters
|
||||
word.WriteByte(ch)
|
||||
}
|
||||
default:
|
||||
word.WriteByte(ch)
|
||||
}
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
switch parseState {
|
||||
case stateSingleQuoted:
|
||||
return nil, ErrUnterminatedSingleQuote
|
||||
case stateDoubleQuoted:
|
||||
return nil, ErrUnterminatedDoubleQuote
|
||||
}
|
||||
|
||||
if inWord {
|
||||
words = append(words, word.String())
|
||||
}
|
||||
|
||||
return words, nil
|
||||
}
|
||||
Reference in New Issue
Block a user