Parse a command line string into flags and arguments in Golang
If the args were passed to your program on the command line then the shell should handle this and os.Args
will be populated correctly. For example, in your case os.Args[1:]
will equal
[]string{"-v", "--format", "some example", "-i", "test"}
If you just have the string though, for some reason, and you'd like to mimic what the shell would do with it, then I recommend a package like https://github.com/kballard/go-shellquote
Looks similar to shlex:
import "github.com/google/shlex"
shlex.Split("one \"two three\" four") -> []string{"one", "two three", "four"}
@laurent 's answer is wonderful, but it doesn't work when command
includes utf-8 char.
It fail the third test:
func TestParseCommandLine(t *testing.T){
tests := []struct{
name string
input string
want []string
}{
{
"normal",
"hello world",
[]string{"hello", "world"},
},
{
"quote",
"hello \"world hello\"",
[]string{"hello", "world hello"},
},
{
"utf-8",
"hello 世界",
[]string{"hello", "世界"},
},
{
"space",
"hello\\ world",
[]string{"hello world"},
},
}
for _, tt := range tests{
t.Run(tt.name, func(t *testing.T) {
got, _ := parseCommandLine(tt.input)
if !reflect.DeepEqual(got, tt.want){
t.Errorf("expect %v, got %v", tt.want, got)
}
})
}
}
Based on his/her answer, i wrote this func that works good for utf-8, just by replacing for i := 0; i < len(command); i++ {c := command[i]
to for _, c := range command
Here's the my answer:
func parseCommandLine(command string) ([]string, error) {
var args []string
state := "start"
current := ""
quote := "\""
escapeNext := true
for _, c := range command {
if state == "quotes" {
if string(c) != quote {
current += string(c)
} else {
args = append(args, current)
current = ""
state = "start"
}
continue
}
if escapeNext {
current += string(c)
escapeNext = false
continue
}
if c == '\\' {
escapeNext = true
continue
}
if c == '"' || c == '\'' {
state = "quotes"
quote = string(c)
continue
}
if state == "arg" {
if c == ' ' || c == '\t' {
args = append(args, current)
current = ""
state = "start"
} else {
current += string(c)
}
continue
}
if c != ' ' && c != '\t' {
state = "arg"
current += string(c)
}
}
if state == "quotes" {
return []string{}, errors.New(fmt.Sprintf("Unclosed quote in command line: %s", command))
}
if current != "" {
args = append(args, current)
}
return args, nil
}
For information, this is the function I've ended up creating.
It splits a command into its arguments. For example, cat -v "some file.txt"
, will return ["cat", "-v", "some file.txt"]
.
It also correctly handles escaped characters, spaces in particular. So cat -v some\ file.txt
will also correctly be split into ["cat", "-v", "some file.txt"]
func parseCommandLine(command string) ([]string, error) {
var args []string
state := "start"
current := ""
quote := "\""
escapeNext := true
for i := 0; i < len(command); i++ {
c := command[i]
if state == "quotes" {
if string(c) != quote {
current += string(c)
} else {
args = append(args, current)
current = ""
state = "start"
}
continue
}
if (escapeNext) {
current += string(c)
escapeNext = false
continue
}
if (c == '\\') {
escapeNext = true
continue
}
if c == '"' || c == '\'' {
state = "quotes"
quote = string(c)
continue
}
if state == "arg" {
if c == ' ' || c == '\t' {
args = append(args, current)
current = ""
state = "start"
} else {
current += string(c)
}
continue
}
if c != ' ' && c != '\t' {
state = "arg"
current += string(c)
}
}
if state == "quotes" {
return []string{}, errors.New(fmt.Sprintf("Unclosed quote in command line: %s", command))
}
if current != "" {
args = append(args, current)
}
return args, nil
}