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
}