From 218f3dbd5dbdf9af1f2e2b4409ff264cccf4071b Mon Sep 17 00:00:00 2001 From: Vladimir Vivien Date: Fri, 27 Mar 2020 16:30:29 -0400 Subject: [PATCH] Handle commands with embedded colons --- exec/run_exec_test.go | 54 ++++++++++ script/authconfig_cmd_test.go | 24 +++++ script/capture_cmd_test.go | 186 ++++++++++++++++++++++++++++++++++ script/copy_cmd_test.go | 46 +++++++++ script/env_cmd.go | 4 +- script/env_cmd_test.go | 68 +++++++++++++ script/kubecfg_cmd_test.go | 40 ++++++++ script/kubeget_cmd_test.go | 24 +++++ script/output_cmd_test.go | 20 ++++ script/parser.go | 33 +++--- script/run_cmd.go | 4 +- script/words_split.go | 22 +++- script/words_split_test.go | 59 ++++++++++- 13 files changed, 554 insertions(+), 30 deletions(-) diff --git a/exec/run_exec_test.go b/exec/run_exec_test.go index aba4f906..27db2c8b 100644 --- a/exec/run_exec_test.go +++ b/exec/run_exec_test.go @@ -175,6 +175,60 @@ func TestExecRUN(t *testing.T) { return nil }, }, + { + name: "RUN single command with embeddec colon", + source: func() string { + return fmt.Sprintf(`FROM 127.0.0.1:%s + AUTHCONFIG username:${USER} private-key:${HOME}/.ssh/id_rsa + RUN /bin/echo "HELLO:WORLD" + `, testSSHPort) + }, + exec: func(s *script.Script) error { + + e := New(s) + if err := e.Execute(); err != nil { + return err + } + + exitcode := os.Getenv("CMD_EXITCODE") + if exitcode != "0" { + return fmt.Errorf("RUN has unexpected exit code %s", exitcode) + } + + result := os.Getenv("CMD_RESULT") + if result != "HELLO:WORLD" { + return fmt.Errorf("RUN has unexpected CMD_RESULT: %s", result) + } + return nil + }, + }, + { + name: "RUN with shell wrapped quoted subcommand with embedded colon", + source: func() string { + return fmt.Sprintf(` + FROM 127.0.0.1:%s + AUTHCONFIG username:${USER} private-key:${HOME}/.ssh/id_rsa + RUN shell:"/bin/bash -c" cmd:'echo "Hello:World"'`, testSSHPort) + }, + exec: func(s *script.Script) error { + + e := New(s) + if err := e.Execute(); err != nil { + return err + } + + exitcode := os.Getenv("CMD_EXITCODE") + if exitcode != "0" { + return fmt.Errorf("RUN has unexpected exit code %s", exitcode) + } + + result := os.Getenv("CMD_RESULT") + if strings.TrimSpace(result) != "Hello:World" { + return fmt.Errorf("RUN has unexpected CMD_RESULT: %s", result) + } + return nil + }, + }, } for _, test := range tests { diff --git a/script/authconfig_cmd_test.go b/script/authconfig_cmd_test.go index 333e8973..b4dca5a3 100644 --- a/script/authconfig_cmd_test.go +++ b/script/authconfig_cmd_test.go @@ -115,6 +115,30 @@ func TestCommandAUTHCONFIG(t *testing.T) { }, shouldFail: true, }, + + { + name: "AUTHCONFIG - with embedded colon", + source: func() string { + return "AUTHCONFIG username:test-user private-key:'/a/:b/c'" + }, + script: func(s *Script) error { + cmds := s.Preambles[CmdAuthConfig] + if len(cmds) != 1 { + return fmt.Errorf("Script missing preamble %s", CmdAuthConfig) + } + authCmd, ok := cmds[0].(*AuthConfigCommand) + if !ok { + return fmt.Errorf("Unexpected type %T in script", cmds[0]) + } + if authCmd.GetUsername() != "test-user" { + return fmt.Errorf("Unexpected username %s", authCmd.GetUsername()) + } + if authCmd.GetPrivateKey() != "/a/:b/c" { + return fmt.Errorf("Unexpected private-key %s", authCmd.GetPrivateKey()) + } + return nil + }, + }, } for _, test := range tests { diff --git a/script/capture_cmd_test.go b/script/capture_cmd_test.go index 3c910f89..83185e30 100644 --- a/script/capture_cmd_test.go +++ b/script/capture_cmd_test.go @@ -279,6 +279,192 @@ func TestCommandCAPTURE(t *testing.T) { return nil }, }, + { + name: "CAPTURE with unqoted default with embeded colons", + source: func() string { + return `CAPTURE /bin/echo "HELLO:WORLD"` + }, + script: func(s *Script) error { + if len(s.Actions) != 1 { + return fmt.Errorf("Script has unexpected action count, needs %d", len(s.Actions)) + } + cmd, ok := s.Actions[0].(*CaptureCommand) + if !ok { + return fmt.Errorf("Unexpected action type %T in script", s.Actions[0]) + } + + if cmd.Args()["cmd"] != cmd.GetCmdString() { + return fmt.Errorf("CAPTURE action with unexpected command string %s", cmd.GetCmdString()) + } + cliCmd, cliArgs, err := cmd.GetParsedCmd() + if err != nil { + return fmt.Errorf("CAPTURE command parse failed: %s", err) + } + if cliCmd != "/bin/echo" { + return fmt.Errorf("CAPTURE unexpected command parsed: %s", cliCmd) + } + if len(cliArgs) != 1 { + return fmt.Errorf("CAPTURE unexpected command args parsed: %d", len(cliArgs)) + } + if cliArgs[0] != "HELLO:WORLD" { + return fmt.Errorf("CAPTURE has unexpected cli args: %#v", cliArgs) + } + return nil + }, + }, + { + name: "CAPTURE single-quoted-default with embedded colon", + source: func() string { + return `CAPTURE '/bin/echo -n "HELLO:WORLD"'` + }, + script: func(s *Script) error { + if len(s.Actions) != 1 { + return fmt.Errorf("Script has unexpected actions, needs %d", len(s.Actions)) + } + cmd := s.Actions[0].(*CaptureCommand) + if cmd.Args()["cmd"] != cmd.GetCmdString() { + return fmt.Errorf("CAPTURE action with unexpected CLI string %s", cmd.GetCmdString()) + } + cliCmd, cliArgs, err := cmd.GetParsedCmd() + if err != nil { + return fmt.Errorf("CAPTURE command parse failed: %s", err) + } + if cliCmd != "/bin/echo" { + return fmt.Errorf("CAPTURE unexpected command parsed: %s", cliCmd) + } + if len(cliArgs) != 2 { + return fmt.Errorf("CAPTURE unexpected command args parsed: %d", len(cliArgs)) + } + if cliArgs[0] != "-n" { + return fmt.Errorf("CAPTURE has unexpected cli args: %#v", cliArgs) + } + if cliArgs[1] != "HELLO:WORLD" { + return fmt.Errorf("CAPTURE has unexpected cli args: %#v", cliArgs) + } + return nil + }, + }, + { + name: "CAPTURE single-quoted named param with embedded colon", + source: func() string { + return `CAPTURE cmd:'/bin/echo -n "HELLO:WORLD"'` + }, + script: func(s *Script) error { + if len(s.Actions) != 1 { + return fmt.Errorf("Script has unexpected actions, needs %d", len(s.Actions)) + } + cmd := s.Actions[0].(*CaptureCommand) + if cmd.Args()["cmd"] != cmd.GetCmdString() { + return fmt.Errorf("CAPTURE action with unexpected CLI string %s", cmd.GetCmdString()) + } + cliCmd, cliArgs, err := cmd.GetParsedCmd() + if err != nil { + return fmt.Errorf("CAPTURE command parse failed: %s", err) + } + if cliCmd != "/bin/echo" { + return fmt.Errorf("CAPTURE unexpected command parsed: %s", cliCmd) + } + if len(cliArgs) != 2 { + return fmt.Errorf("CAPTURE unexpected command args parsed: %d", len(cliArgs)) + } + if cliArgs[0] != "-n" { + return fmt.Errorf("CAPTURE has unexpected cli args: %#v", cliArgs) + } + if cliArgs[1] != "HELLO:WORLD" { + return fmt.Errorf("CAPTURE has unexpected cli args: %#v", cliArgs) + } + return nil + }, + }, + { + name: "CAPTURE double-quoted named param with embedded colon", + source: func() string { + return `CAPTURE cmd:"/bin/echo -n 'HELLO:WORLD'"` + }, + script: func(s *Script) error { + if len(s.Actions) != 1 { + return fmt.Errorf("Script has unexpected actions, needs %d", len(s.Actions)) + } + cmd := s.Actions[0].(*CaptureCommand) + if cmd.Args()["cmd"] != cmd.GetCmdString() { + return fmt.Errorf("CAPTURE action with unexpected CLI string %s", cmd.GetCmdString()) + } + cliCmd, cliArgs, err := cmd.GetParsedCmd() + if err != nil { + return fmt.Errorf("CAPTURE command parse failed: %s", err) + } + if cliCmd != "/bin/echo" { + return fmt.Errorf("CAPTURE unexpected command parsed: %s", cliCmd) + } + if len(cliArgs) != 2 { + return fmt.Errorf("CAPTURE unexpected command args parsed: %d", len(cliArgs)) + } + if cliArgs[0] != "-n" { + return fmt.Errorf("CAPTURE has unexpected cli args: %#v", cliArgs) + } + if cliArgs[1] != "HELLO:WORLD" { + return fmt.Errorf("CAPTURE has unexpected cli args: %#v", cliArgs) + } + return nil + }, + }, + { + name: "CAPTURE unquoted named param with multiple embedded colons", + source: func() string { + return "CAPTURE cmd:/bin/date:time:" + }, + script: func(s *Script) error { + cmd := s.Actions[0].(*CaptureCommand) + cliCmd, cliArgs, err := cmd.GetParsedCmd() + if err != nil { + return fmt.Errorf("CAPTURE command parse failed: %s", err) + } + if cliCmd != "/bin/date:time:" { + return fmt.Errorf("CAPTURE parsed unexpected command name: %s", cliCmd) + } + if len(cliArgs) != 0 { + return fmt.Errorf("CAPTURE parsed unexpected command args: %d", len(cliArgs)) + } + + return nil + }, + }, + { + name: "CAPTURE with shell and quoted subproc with embedded colon", + source: func() string { + return `CAPTURE shell:"/bin/bash -c" cmd:"echo 'HELLO:WORLD'"` + }, + script: func(s *Script) error { + if len(s.Actions) != 1 { + return fmt.Errorf("Script has unexpected actions, needs %d", len(s.Actions)) + } + cmd := s.Actions[0].(*CaptureCommand) + if cmd.Args()["cmd"] != cmd.GetCmdString() { + return fmt.Errorf("CAPTURE action with unexpected command string %s", cmd.GetCmdString()) + } + if cmd.Args()["shell"] != cmd.GetCmdShell() { + return fmt.Errorf("CAPTURE action with unexpected shell %s", cmd.GetCmdShell()) + } + + cliCmd, cliArgs, err := cmd.GetParsedCmd() + if err != nil { + return fmt.Errorf("CAPTURE command parse failed: %s", err) + } + if len(cliArgs) != 2 { + return fmt.Errorf("CAPTURE unexpected command args parsed: %#v", cliArgs) + } + if cliCmd != "/bin/bash" { + return fmt.Errorf("CAPTURE unexpected command parsed: %#v", cliCmd) + } + if cliArgs[0] != "-c" { + return fmt.Errorf("CAPTURE has unexpected shell argument: expecting -c, got %s", cliArgs[0]) + } + if cliArgs[1] != "echo 'HELLO:WORLD'" { + return fmt.Errorf("CAPTURE has unexpected shell argument: expecting -c, got %s", cliArgs[0]) + } + return nil + }, + }, } for _, test := range tests { diff --git a/script/copy_cmd_test.go b/script/copy_cmd_test.go index a00e94c6..2cf229f9 100644 --- a/script/copy_cmd_test.go +++ b/script/copy_cmd_test.go @@ -213,6 +213,52 @@ func TestCommandCOPY(t *testing.T) { }, shouldFail: true, }, + { + name: "COPY with quoted default with ebedded colon", + source: func() string { + return `COPY '/a/:b/c'` + }, + script: func(s *Script) error { + if len(s.Actions) != 1 { + return fmt.Errorf("Script has unexpected COPY actions, has %d COPY", len(s.Actions)) + } + + cmd := s.Actions[0].(*CopyCommand) + if len(cmd.Paths()) != 1 { + return fmt.Errorf("COPY has unexpected number of paths %d", len(cmd.Paths())) + } + + arg := cmd.Paths()[0] + if arg != "/a/:b/c" { + return fmt.Errorf("COPY has unexpected argument %s", arg) + } + return nil + }, + }, + { + name: "COPY multiple with named param", + source: func() string { + return `COPY paths:"/a/b/c /e/:f/g"` + }, + script: func(s *Script) error { + if len(s.Actions) != 1 { + return fmt.Errorf("Script has unexpected COPY actions, has %d COPY", len(s.Actions)) + } + + cmd := s.Actions[0].(*CopyCommand) + if len(cmd.Paths()) != 2 { + return fmt.Errorf("COPY has unexpected number of args %d", len(cmd.Paths())) + } + if cmd.Paths()[0] != "/a/b/c" { + return fmt.Errorf("COPY has unexpected argument[0] %s", cmd.Paths()[0]) + } + if cmd.Paths()[1] != "/e/:f/g" { + return fmt.Errorf("COPY has unexpected argument[1] %s", cmd.Paths()[1]) + } + + return nil + }, + }, } for _, test := range tests { diff --git a/script/env_cmd.go b/script/env_cmd.go index 067a6193..f872f660 100644 --- a/script/env_cmd.go +++ b/script/env_cmd.go @@ -53,7 +53,7 @@ func NewEnvCommand(index int, rawArgs string) (*EnvCommand, error) { // supported format keyN=valN keyN="valN" keyN='valN' // foreach key0=val0 key1=val1 ... keyN=valN // split into keyN, valN - envs, err := wordSplit(argMap["vars"]) + envs, err := commandSplit(argMap["vars"]) if err != nil { return nil, fmt.Errorf("ENV: %s", err) } @@ -65,7 +65,7 @@ func NewEnvCommand(index int, rawArgs string) (*EnvCommand, error) { } key := parts[0] - val, err := wordSplit(parts[1]) // safely remove outer quotes + val, err := commandSplit(parts[1]) // safely remove outer quotes if err != nil { return nil, fmt.Errorf("ENV: %s", err) } diff --git a/script/env_cmd_test.go b/script/env_cmd_test.go index 28dabac6..54c0e061 100644 --- a/script/env_cmd_test.go +++ b/script/env_cmd_test.go @@ -177,6 +177,74 @@ func TestCommandENV(t *testing.T) { }, shouldFail: true, }, + { + name: "ENV unquoted with embedded colon", + source: func() string { + return "ENV foo=bar:Baz" + }, + script: func(s *Script) error { + envs := s.Preambles[CmdEnv] + if len(envs) != 1 { + return fmt.Errorf("Script has unexpected number of ENV %d", len(envs)) + } + envCmd, ok := envs[0].(*EnvCommand) + if !ok { + return fmt.Errorf("Unexpected type %T in script", envs[0]) + } + if len(envCmd.Envs()) != 1 { + return fmt.Errorf("ENV has unexpected number of env %d", len(envCmd.Envs())) + } + env := envCmd.Envs()["foo"] + if env != "bar:Baz" { + return fmt.Errorf("ENV has unexpected value: foo=%s", envCmd.Envs()["foo"]) + } + return nil + }, + }, + { + name: "ENV quoted arg with embedded colon", + source: func() string { + return `ENV foo="bar bazz:bat"` + }, + script: func(s *Script) error { + envs := s.Preambles[CmdEnv] + envCmd := envs[0].(*EnvCommand) + if len(envCmd.Envs()) != 1 { + return fmt.Errorf("ENV has unexpected number of env %d", len(envCmd.Envs())) + } + env := envCmd.Envs()["foo"] + if env != "bar bazz:bat" { + return fmt.Errorf("ENV has unexpected value: foo=%s", envCmd.Envs()["foo"]) + } + return nil + }, + }, + { + name: "ENV multiple quoted vars with embedded colon", + source: func() string { + return `ENV vars:'a="b:g" c=d:d e=f'` + }, + script: func(s *Script) error { + envs := s.Preambles[CmdEnv] + if len(envs) != 1 { + return fmt.Errorf("Script has unexpected number of ENV %d", len(envs)) + } + + envCmd0 := envs[0].(*EnvCommand) + if len(envCmd0.Envs()) != 3 { + return fmt.Errorf("ENV has unexpected number of env %d", len(envCmd0.Envs())) + } + env := envCmd0.Envs()["a"] + if env != "b:g" { + return fmt.Errorf("ENV has unexpected value a=%s", envCmd0.Envs()["a"]) + } + env0, env1 := envCmd0.Envs()["c"], envCmd0.Envs()["e"] + if env0 != "d:d" || env1 != "f" { + return fmt.Errorf("ENV has unexpected values env[c]=%s and env[e]=%s", env0, env1) + } + return nil + }, + }, } for _, test := range tests { diff --git a/script/kubecfg_cmd_test.go b/script/kubecfg_cmd_test.go index b68ad092..a844716b 100644 --- a/script/kubecfg_cmd_test.go +++ b/script/kubecfg_cmd_test.go @@ -119,6 +119,46 @@ func TestCommandKUBECONFIG(t *testing.T) { }, shouldFail: true, }, + { + name: "KUBECONFIG default with embedded colon", + source: func() string { + return "KUBECONFIG /a/:b/c" + }, + script: func(s *Script) error { + cfgs := s.Preambles[CmdKubeConfig] + if len(cfgs) != 1 { + return fmt.Errorf("Script has unexpected number of KUBECONFIG %d", len(cfgs)) + } + cfg, ok := cfgs[0].(*KubeConfigCommand) + if !ok { + return fmt.Errorf("Unexpected type %T in script", cfgs[0]) + } + if cfg.Path() != "/a/:b/c" { + return fmt.Errorf("KUBECONFIG has unexpected config %s", cfg.Path()) + } + return nil + }, + }, + { + name: "KUBECONFIG quoted named param with embedded colon", + source: func() string { + return `KUBECONFIG path:"/a/:b/c"` + }, + script: func(s *Script) error { + cfgs := s.Preambles[CmdKubeConfig] + if len(cfgs) != 1 { + return fmt.Errorf("Script has unexpected number of KUBECONFIG %d", len(cfgs)) + } + cfg, ok := cfgs[0].(*KubeConfigCommand) + if !ok { + return fmt.Errorf("Unexpected type %T in script", cfgs[0]) + } + if cfg.Path() != "/a/:b/c" { + return fmt.Errorf("KUBECONFIG has unexpected config %s", cfg.Path()) + } + return nil + }, + }, } for _, test := range tests { diff --git a/script/kubeget_cmd_test.go b/script/kubeget_cmd_test.go index ffc1713d..e8e14f85 100644 --- a/script/kubeget_cmd_test.go +++ b/script/kubeget_cmd_test.go @@ -66,6 +66,30 @@ func TestCommandKUBEGET(t *testing.T) { return nil }, }, + { + name: "KUBEGET objects with params with embedded colon", + source: func() string { + return ` + KUBEGET objects namespaces:"myns test:ns" groups:"v1" kinds:"pods events" versions:"1" names:"my-app" labels:"prod" containers:"webapp"` + }, + script: func(s *Script) error { + kgCmd := s.Actions[0].(*KubeGetCommand) + if len(kgCmd.Args()) != 8 { + return fmt.Errorf("KUBEGET unexpected param count: %d", len(kgCmd.Args())) + } + // check each param + if kgCmd.What() != "objects" { + return fmt.Errorf("KUBEGET unexpected what: %s", kgCmd.What()) + } + if kgCmd.Namespaces() != "myns test:ns" { + return fmt.Errorf("KUBEGET unexpected namespaces: %s", kgCmd.Namespaces()) + } + if kgCmd.Groups() != "v1" { + return fmt.Errorf("KUBEGET unexpected namespaces: %s", kgCmd.Namespaces()) + } + return nil + }, + }, } for _, test := range tests { diff --git a/script/output_cmd_test.go b/script/output_cmd_test.go index f1178755..dd398044 100644 --- a/script/output_cmd_test.go +++ b/script/output_cmd_test.go @@ -126,6 +126,26 @@ func TestCommandOUTPUT(t *testing.T) { }, shouldFail: true, }, + { + name: "OUTPUT named arg with embedded colon", + source: func() string { + return "OUTPUT path:foo/bar.tar.gz:ignore" + }, + script: func(s *Script) error { + outs := s.Preambles[CmdOutput] + if len(outs) != 1 { + return fmt.Errorf("Script has unexpected number of OUTPUT %d", len(outs)) + } + outCmd, ok := outs[0].(*OutputCommand) + if !ok { + return fmt.Errorf("Unexpected type %T in script", outs[0]) + } + if outCmd.Path() != "foo/bar.tar.gz:ignore" { + return fmt.Errorf("OUTPUT has unexpected directory %s", outCmd.Path()) + } + return nil + }, + }, } for _, test := range tests { diff --git a/script/parser.go b/script/parser.go index a4cc8292..6cc3c7b5 100644 --- a/script/parser.go +++ b/script/parser.go @@ -15,10 +15,11 @@ import ( ) var ( - spaceSep = regexp.MustCompile(`\s`) - paramSep = regexp.MustCompile(`:`) - quoteSet = regexp.MustCompile(`[\"\']`) - cmdSep = regexp.MustCompile(`\s`) + spaceSep = regexp.MustCompile(`\s`) + paramSep = regexp.MustCompile(`:`) + quoteSet = regexp.MustCompile(`[\"\']`) + cmdSep = regexp.MustCompile(`\s`) + namedParamRegx = regexp.MustCompile(`^([a-z0-9_\-]+)(:)(["']{0,1}.+["']{0,1})$`) ) // Parse parses the textual script from reader into an *Script representation @@ -169,20 +170,18 @@ func mapArgs(rawArgs string) (map[string]string, error) { argMap := make(map[string]string) // split params: param0: paramN: badparam - params, err := wordSplit(rawArgs) + params, err := commandSplit(rawArgs) if err != nil { return nil, err } // for each, split pram: into {param, } for _, param := range params { - parts := paramSep.Split(param, 2) - if len(parts) != 2 { - return argMap, fmt.Errorf("invalid param: %s", param) + cmdName, cmdStr, err := namedParamSplit(param) + if err != nil { + return nil, fmt.Errorf("map args: %s", err) } - name := parts[0] - val := trimQuotes(parts[1]) - argMap[name] = val + argMap[cmdName] = cmdStr } return argMap, nil @@ -193,15 +192,7 @@ func mapArgs(rawArgs string) (map[string]string, error) { // name:value // func isNamedParam(str string) bool { - if len(str) == 0 { - return false - } - - parts := paramSep.Split(str, 2) - if len(parts) >= 2 { - return true - } - return false + return namedParamRegx.MatchString(str) } // makeParam @@ -274,7 +265,7 @@ func enforceDefaults(script *Script) (*Script, error) { func cmdParse(cmdStr string) (cmd string, args []string, err error) { logrus.Debugf("Parsing: %s", cmdStr) - args, err = wordSplit(cmdStr) + args, err = commandSplit(cmdStr) if err != nil { return "", nil, err } diff --git a/script/run_cmd.go b/script/run_cmd.go index eccf1a65..1f232d8c 100644 --- a/script/run_cmd.go +++ b/script/run_cmd.go @@ -82,13 +82,13 @@ func (c *RunCommand) GetEffectiveCmd() ([]string, error) { cmdStr := c.GetCmdString() shell := c.GetCmdShell() if c.GetCmdShell() != "" { - shArgs, err := wordSplit(shell) + shArgs, err := commandSplit(shell) if err != nil { return nil, err } return append(shArgs, cmdStr), nil } - cmdArgs, err := wordSplit(cmdStr) + cmdArgs, err := commandSplit(cmdStr) if err != nil { return nil, err } diff --git a/script/words_split.go b/script/words_split.go index e13d114d..e7e1b72f 100644 --- a/script/words_split.go +++ b/script/words_split.go @@ -8,13 +8,13 @@ import ( "unicode" ) -// wordSplit splits space-separted strings into words including quoted words: +// commandSplit splits space-separted strings into groups of words including quoted words: // // aaa "bbb" "ccc ddd" eee // // In case of aaa"abcd", the whole thing is returned as aaa"abcd" including qoutes. // In case of "aaa"bbb will be returned as two words "aaa" and "bbb" -func wordSplit(val string) ([]string, error) { +func commandSplit(val string) ([]string, error) { rdr := bufio.NewReader(strings.NewReader(val)) var startQuote rune var word strings.Builder @@ -152,3 +152,21 @@ func trimQuotes(val string) string { return val } + +// namedParamSplit takes a named param in the form of: +// +// pname0:"param value" pname1:'value' pname3:value +// +// Splits them into a slice of [param name, paramvalue] +func namedParamSplit(param string) (cmdName, cmdStr string, err error) { + if len(param) == 0 { + return "", "", nil + } + parts := namedParamRegx.FindStringSubmatch(param) + // len(parts) should be 4 + // [orig string, cmdName, :, cmdStr] + if len(parts) != 4 { + return "", "", fmt.Errorf("malformed param [%s]", parts) + } + return parts[1], trimQuotes(parts[3]), nil +} diff --git a/script/words_split_test.go b/script/words_split_test.go index 88c98a71..85ab3fae 100644 --- a/script/words_split_test.go +++ b/script/words_split_test.go @@ -2,7 +2,7 @@ package script import "testing" -func TestWordSplit(t *testing.T) { +func TestCommandSplit(t *testing.T) { tests := []struct { name string str string @@ -67,7 +67,7 @@ func TestWordSplit(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - words, err := wordSplit(test.str) + words, err := commandSplit(test.str) if err != nil { t.Error(err) } @@ -83,7 +83,7 @@ func TestWordSplit(t *testing.T) { } } -func TestWordSplitTrimQuotes(t *testing.T) { +func TestCommandSplitTrimQuotes(t *testing.T) { tests := []struct { name string str string @@ -145,3 +145,56 @@ func TestWordSplitTrimQuotes(t *testing.T) { }) } } + +func TestNamedParamSplit(t *testing.T) { + tests := []struct { + name string + str string + parts []string + }{ + { + name: "no quotes", + str: `cmd:name:value`, + parts: []string{"cmd", "name:value"}, + }, + { + name: "single quotes", + str: `cmd:'name:single-quote-value'`, + parts: []string{"cmd", "name:single-quote-value"}, + }, + { + name: "double quotes", + str: `cmd:"name: double-quote-value"`, + parts: []string{"cmd", "name: double-quote-value"}, + }, + { + name: "mismatch quotes", + str: `cmd:'name:mismatch-quote-value"`, + parts: []string{"cmd", "name:mismatch-quote-value"}, + }, + { + name: "unbalanced quotes", + str: `cmd:'unbalanced-quote:value`, + parts: []string{"cmd", "unbalanced-quote:value"}, + }, + { + name: "malformed param", + str: `cmd:'malformed-param' cmd:abc`, + parts: []string{"cmd", "malformed-param' cmd:abc"}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + name, val, err := namedParamSplit(test.str) + if err != nil { + t.Error(err) + } + if test.parts[0] != name { + t.Fatalf("expecting param name %s, got %s", test.parts[0], name) + } + if test.parts[1] != val { + t.Fatalf("expecting param value [%s], got [%s]", test.parts[1], val) + } + }) + } +}