mirror of
https://github.com/navidrome/navidrome.git
synced 2026-04-17 13:10:27 -04:00
116 lines
2.3 KiB
Go
116 lines
2.3 KiB
Go
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
|
|
}
|