Skip to content

Commit

Permalink
start to implement gokcehan#1784, set shell as template for CMD on Wi…
Browse files Browse the repository at this point in the history
…ndows

- make `tokenize` to honor quotes
- create function `firstField` to get only the first word from string
- SlellCommand in os_windows.go now handles new type of gOpts.shell for example:
`set shell 'cmd /c "%c %a"'`
- tests for cmd.exe
  • Loading branch information
39555 committed Aug 8, 2024
1 parent 0f8359f commit 193772b
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 2 deletions.
48 changes: 47 additions & 1 deletion misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,55 @@ func unescape(s string) string {
return string(buf)
}

type fieldScanner struct {
quoted bool
esc bool
}

func (t *fieldScanner) next(r rune) bool {
if r == '\'' || r == '"' || r == '`' {
t.quoted = !t.quoted
}
if t.esc {
t.esc = false
return false
}
if r == '\\' {
t.esc = true
return false
}
return !t.quoted && unicode.IsSpace(r)
}

func firstField(s string) (int, int) {
t := &fieldScanner{}
start := -1
for end, rune := range s {
if t.next(rune) {
if start >= 0 {
return start, end
}
} else {
if start < 0 {
start = end
}
}
}
return start, len(s)
}

// This function splits the given string by whitespaces. It is aware of escaped
// whitespaces so that they are not split unintentionally.
// whitespaces and quoted strings so that they are not split unintentionally.
func tokenize(s string) []string {
t := &fieldScanner{}
toks := strings.FieldsFunc(s, t.next)
if len(toks) == 0 {
toks = append(toks, s)
}
return toks
}

func tokenize_old(s string) []string {
esc := false
var buf []rune
var toks []string
Expand Down
2 changes: 1 addition & 1 deletion misc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func TestTokenize(t *testing.T) {

for _, test := range tests {
if got := tokenize(test.s); !reflect.DeepEqual(got, test.exp) {
t.Errorf("at input '%v' expected '%v' but got '%v'", test.s, test.exp, got)
t.Errorf("at input '%#v' expected '%#v' but got '%#v'", test.s, test.exp, got)
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions os_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,21 @@ func detachedCommand(name string, arg ...string) *exec.Cmd {
}

func shellCommand(s string, args []string) *exec.Cmd {
if strings.Contains(gOpts.shell, " ") {
start, end := firstField(gOpts.shell)
cmd := exec.Command(gOpts.shell[start:end])
cmd.Args = nil
cmdLine := syscall.EscapeArg(cmd.Path) + " " + gOpts.shell[end:]

cmdLine = strings.ReplaceAll(cmdLine, "%c", s)
cmdLine = strings.ReplaceAll(cmdLine, "%a", strings.Join(args, " "))

cmd.SysProcAttr = &syscall.SysProcAttr{
CmdLine: cmdLine,
}
return cmd

}
// Windows CMD requires special handling to deal with quoted arguments
if strings.ToLower(gOpts.shell) == "cmd" {
var builder strings.Builder
Expand Down
92 changes: 92 additions & 0 deletions os_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
)


func TestFirst(t *testing.T) {
tests := []struct {
in string
out string
}{
{"cmd", "cmd"},
{"cmd /c", "cmd"},
{`"C:\Program Files\echo.bat" hello world`, `"C:\Program Files\echo.bat"`},
{`ec\ ho -c "sdsd" 1 2 3`, `ec\ ho`},
}

for _, test := range tests {
start, end := firstField(test.in)
r := test.in[start:end]
if !reflect.DeepEqual(r, test.out) {
t.Errorf("at input '%#v' expected '%#v' but parsed '%#v'", test.in, test.out, r)
}
}
}

func setup(_ *testing.T) (string, func(_ *testing.T)) {
tmpDir, err := os.MkdirTemp("", "lfTestShellCommand")
if err != nil {
panic(err)
}
wd, err := os.Getwd()
_ = wd
if err != nil {
panic(err)
}
os.Chdir(tmpDir)

// Return a function to teardown the test
return tmpDir, func(t *testing.T) {
os.Chdir(wd)
os.RemoveAll(tmpDir)
}
}

func TestShellCommand_Cmd(t *testing.T) {
tmpDir, teardown := setup(t)
defer teardown(t)

shell := `cmd.exe /c "%c %a"`
gOpts.shell = shell

run := func(c string, a []string) {
cmd := shellCommand( c, a)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
t.Errorf("failed to run command '%#v' with args '%#v' %v", c, a, err)
}
}

tmpBat := filepath.Join(tmpDir, "Test Folder With Spaces", "echo.bat")
os.MkdirAll(filepath.Dir(tmpBat), os.ModePerm)
f, err := os.Create(tmpBat)
if err != nil {
panic(err)
}
defer f.Close()

_, err = f.WriteString(`
ECHO script args: %0 %*
`)
if err != nil {
panic(err)
}
run(fmt.Sprintf(`"%s"`, tmpBat), []string{`hello friend`})

run(fmt.Sprintf(`dir "%s\Test Folder With Spaces" | %%PAGER%%`, tmpDir), []string{})

c := `mkdir "foo bar"`
run(c, []string{})
if stat, err := os.Stat("foo bar"); err != nil || !stat.IsDir() {
t.Errorf("failed to run command '%#v' %v", c, err)
}
}

0 comments on commit 193772b

Please sign in to comment.