From 2709e1f26a61d082220dc15ba85b378fab1468dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 16 Sep 2021 17:39:18 +0200 Subject: [PATCH 1/4] gofmt -s --- terminal/display_posix.go | 1 + terminal/output.go | 1 + terminal/runereader_bsd.go | 1 + terminal/runereader_linux.go | 1 + terminal/runereader_posix.go | 1 + terminal/runereader_ppc64le.go | 1 + 6 files changed, 6 insertions(+) diff --git a/terminal/display_posix.go b/terminal/display_posix.go index 46608087..fbd1b794 100644 --- a/terminal/display_posix.go +++ b/terminal/display_posix.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package terminal diff --git a/terminal/output.go b/terminal/output.go index 6fe11c08..29102420 100644 --- a/terminal/output.go +++ b/terminal/output.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package terminal diff --git a/terminal/runereader_bsd.go b/terminal/runereader_bsd.go index 6ea34092..57f10142 100644 --- a/terminal/runereader_bsd.go +++ b/terminal/runereader_bsd.go @@ -3,6 +3,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build darwin || dragonfly || freebsd || netbsd || openbsd // +build darwin dragonfly freebsd netbsd openbsd package terminal diff --git a/terminal/runereader_linux.go b/terminal/runereader_linux.go index 6dd60ea6..dc7ec670 100644 --- a/terminal/runereader_linux.go +++ b/terminal/runereader_linux.go @@ -2,6 +2,7 @@ // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build linux && !ppc64le // +build linux,!ppc64le package terminal diff --git a/terminal/runereader_posix.go b/terminal/runereader_posix.go index 3b846049..563a0811 100644 --- a/terminal/runereader_posix.go +++ b/terminal/runereader_posix.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows // The terminal mode manipulation code is derived heavily from: diff --git a/terminal/runereader_ppc64le.go b/terminal/runereader_ppc64le.go index ae4eb097..450f796c 100644 --- a/terminal/runereader_ppc64le.go +++ b/terminal/runereader_ppc64le.go @@ -1,3 +1,4 @@ +//go:build ppc64le && linux // +build ppc64le,linux package terminal From 191065b8d1c1e5fe14d54ad04361f141f0572b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Sun, 2 Oct 2022 18:05:38 +0200 Subject: [PATCH 2/4] [WIP] make survey work in mintty --- go.mod | 1 + go.sum | 3 +- input.go | 6 ++- select.go | 4 +- terminal/display_windows.go | 1 + terminal/runereader.go | 71 +++++++++++++++++++++++++++ terminal/runereader_posix.go | 64 +----------------------- terminal/runereader_windows.go | 90 +++++++++++++++++++++++++++++++--- 8 files changed, 166 insertions(+), 74 deletions(-) diff --git a/go.mod b/go.mod index e461778b..2e58b2ed 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,7 @@ module github.com/AlecAivazis/survey/v2 require ( github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 + github.com/cli/safeexec v1.0.0 github.com/creack/pty v1.1.17 github.com/davecgh/go-spew v1.1.1 // indirect github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec diff --git a/go.sum b/go.sum index e83f9839..db0cea68 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= +github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -21,7 +23,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/input.go b/input.go index 2fddff5b..f6a8d0c7 100644 --- a/input.go +++ b/input.go @@ -2,6 +2,7 @@ package survey import ( "errors" + "fmt" "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" @@ -152,10 +153,13 @@ func (i *Input) Prompt(config *PromptConfig) (interface{}, error) { // start reading runes from the standard in rr := i.NewRuneReader() - _ = rr.SetTermMode() + if err := rr.SetTermMode(); err != nil { + return "", fmt.Errorf("SetTermMode: %w", err) + } defer func() { _ = rr.RestoreTermMode() }() + cursor := i.NewCursor() if !config.ShowCursor { cursor.Hide() // hide the cursor diff --git a/select.go b/select.go index 1210122f..d0fd3014 100644 --- a/select.go +++ b/select.go @@ -280,7 +280,9 @@ func (s *Select) Prompt(config *PromptConfig) (interface{}, error) { } rr := s.NewRuneReader() - _ = rr.SetTermMode() + if err := rr.SetTermMode(); err != nil { + return "", fmt.Errorf("SetTermMode: %w", err) + } defer func() { _ = rr.RestoreTermMode() }() diff --git a/terminal/display_windows.go b/terminal/display_windows.go index fc9db9f7..c629559a 100644 --- a/terminal/display_windows.go +++ b/terminal/display_windows.go @@ -1,6 +1,7 @@ package terminal import ( + "fmt" "syscall" "unsafe" ) diff --git a/terminal/runereader.go b/terminal/runereader.go index c6997597..5c164140 100644 --- a/terminal/runereader.go +++ b/terminal/runereader.go @@ -1,6 +1,7 @@ package terminal import ( + "bufio" "fmt" "unicode" @@ -377,6 +378,76 @@ func (rr *RuneReader) ReadLineWithDefault(mask rune, d []rune, onRunes ...OnRune } } +const ( + normalKeypad = '[' + applicationKeypad = 'O' +) + +// readRunePOSIX parses escape sequences such as ESC [ A for arrow keys. +// +// See https://vt100.net/docs/vt102-ug/appendixc.html +func readRunePOSIX(reader *bufio.Reader, discardNext bool) (rune, int, error) { + r, size, err := reader.ReadRune() + if err != nil { + return r, size, err + } + + if r != KeyEscape { + return r, size, err + } + + if reader.Buffered() == 0 { + // no more characters so must be `Esc` key + return KeyEscape, 1, nil + } + + r, size, err = reader.ReadRune() + if err != nil { + return r, size, err + } + + // ESC O ... or ESC [ ...? + if r != normalKeypad && r != applicationKeypad { + return r, size, fmt.Errorf("unexpected escape sequence from terminal: %q", []rune{KeyEscape, r}) + } + + keypad := r + + r, size, err = reader.ReadRune() + if err != nil { + return r, size, err + } + + switch r { + case 'A': // ESC [ A or ESC O A + return KeyArrowUp, 1, nil + case 'B': // ESC [ B or ESC O B + return KeyArrowDown, 1, nil + case 'C': // ESC [ C or ESC O C + return KeyArrowRight, 1, nil + case 'D': // ESC [ D or ESC O D + return KeyArrowLeft, 1, nil + case 'F': // ESC [ F or ESC O F + return SpecialKeyEnd, 1, nil + case 'H': // ESC [ H or ESC O H + return SpecialKeyHome, 1, nil + case '3': // ESC [ 3 + if keypad == normalKeypad { + if discardNext { + // discard the following '~' key from buffer + _, _ = reader.Discard(1) + } + return SpecialKeyDelete, 1, nil + } + } + + if discardNext { + // discard the following '~' key from buffer + _, _ = reader.Discard(1) + } + return IgnoreKey, 1, nil +} + func runeWidth(r rune) int { switch width.LookupRune(r).Kind() { case width.EastAsianWide, width.EastAsianFullwidth: diff --git a/terminal/runereader_posix.go b/terminal/runereader_posix.go index 563a0811..5f3aee4f 100644 --- a/terminal/runereader_posix.go +++ b/terminal/runereader_posix.go @@ -12,16 +12,10 @@ package terminal import ( "bufio" "bytes" - "fmt" "syscall" "unsafe" ) -const ( - normalKeypad = '[' - applicationKeypad = 'O' -) - type runeReaderState struct { term syscall.Termios reader *bufio.Reader @@ -71,62 +65,6 @@ func (rr *RuneReader) RestoreTermMode() error { return nil } -// ReadRune Parse escape sequences such as ESC [ A for arrow keys. -// See https://vt100.net/docs/vt102-ug/appendixc.html func (rr *RuneReader) ReadRune() (rune, int, error) { - r, size, err := rr.state.reader.ReadRune() - if err != nil { - return r, size, err - } - - if r != KeyEscape { - return r, size, err - } - - if rr.state.reader.Buffered() == 0 { - // no more characters so must be `Esc` key - return KeyEscape, 1, nil - } - - r, size, err = rr.state.reader.ReadRune() - if err != nil { - return r, size, err - } - - // ESC O ... or ESC [ ...? - if r != normalKeypad && r != applicationKeypad { - return r, size, fmt.Errorf("unexpected escape sequence from terminal: %q", []rune{KeyEscape, r}) - } - - keypad := r - - r, size, err = rr.state.reader.ReadRune() - if err != nil { - return r, size, err - } - - switch r { - case 'A': // ESC [ A or ESC O A - return KeyArrowUp, 1, nil - case 'B': // ESC [ B or ESC O B - return KeyArrowDown, 1, nil - case 'C': // ESC [ C or ESC O C - return KeyArrowRight, 1, nil - case 'D': // ESC [ D or ESC O D - return KeyArrowLeft, 1, nil - case 'F': // ESC [ F or ESC O F - return SpecialKeyEnd, 1, nil - case 'H': // ESC [ H or ESC O H - return SpecialKeyHome, 1, nil - case '3': // ESC [ 3 - if keypad == normalKeypad { - // discard the following '~' key from buffer - _, _ = rr.state.reader.Discard(1) - return SpecialKeyDelete, 1, nil - } - } - - // discard the following '~' key from buffer - _, _ = rr.state.reader.Discard(1) - return IgnoreKey, 1, nil + return readRunePOSIX(rr.state.reader, true) } diff --git a/terminal/runereader_windows.go b/terminal/runereader_windows.go index 791092f5..3706ee65 100644 --- a/terminal/runereader_windows.go +++ b/terminal/runereader_windows.go @@ -1,9 +1,15 @@ package terminal import ( + "bufio" "bytes" + "fmt" + "os/exec" + "strings" "syscall" "unsafe" + + "github.com/cli/safeexec" ) var ( @@ -51,26 +57,83 @@ type keyEventRecord struct { type runeReaderState struct { term uint32 + err error + // when sttyExe is set, we're controlling the terminal by shelling out to that instead of syscalls + sttyExe string + sttyState string + reader *bufio.Reader + buf *bytes.Buffer } func newRuneReaderState(input FileReader) runeReaderState { - return runeReaderState{} + s := runeReaderState{} + + r, _, err := getConsoleMode.Call(uintptr(input.Fd()), uintptr(unsafe.Pointer(&s.term))) + if r == 0 { + // This typically fails in mintty (used by Git Bash). As a fallback, detect if `stty` is available + // and use that to put the terminal into raw mode: + if stty, state, sttyErr := sttyGetState(input); sttyErr == nil { + s.sttyExe = stty + s.sttyState = state + s.buf = &bytes.Buffer{} + s.reader = bufio.NewReader(&BufferedReader{ + In: input, + Buffer: s.buf, + }) + } else { + s.err = err + } + } + + return s +} + +func sttyGetState(input FileReader) (string, string, error) { + stty, err := safeexec.LookPath("stty") + if err != nil { + return "", "", err + } + c := exec.Command(stty, "-F", "/dev/tty", "-g") + c.Stdin = input + out, err := c.Output() + return stty, strings.TrimSpace(string(out)), err +} + +func sttySetMode(input FileReader, stty string) error { + c := exec.Command(stty, "-F", "/dev/tty", "cbreak", "min", "1") + c.Stdin = input + if err := c.Run(); err != nil { + return err + } + + c = exec.Command(stty, "-F", "/dev/tty", "-echo", "-isig", "-icanon") + c.Stdin = input + return c.Run() +} + +func sttyRestore(input FileReader, stty string, state string) error { + c := exec.Command(stty, "-F", "/dev/tty", state) + c.Stdin = input + return c.Run() } func (rr *RuneReader) Buffer() *bytes.Buffer { - return nil + return rr.state.buf } func (rr *RuneReader) SetTermMode() error { - r, _, err := getConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(unsafe.Pointer(&rr.state.term))) - // windows return 0 on error - if r == 0 { - return err + if rr.state.sttyExe != "" { + if err := sttySetMode(rr.stdio.In, rr.state.sttyExe); err != nil { + return fmt.Errorf("stty: %w", err) + } + return nil + } else if rr.state.err != nil { + return rr.state.err } newState := rr.state.term newState &^= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT - r, _, err = setConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(newState)) + r, _, err := setConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(newState)) // windows return 0 on error if r == 0 { return err @@ -79,6 +142,13 @@ func (rr *RuneReader) SetTermMode() error { } func (rr *RuneReader) RestoreTermMode() error { + if rr.state.sttyExe != "" { + if err := sttyRestore(rr.stdio.In, rr.state.sttyExe, rr.state.sttyState); err != nil { + return fmt.Errorf("stty: %w", err) + } + return nil + } + r, _, err := setConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(rr.state.term)) // windows return 0 on error if r == 0 { @@ -88,13 +158,17 @@ func (rr *RuneReader) RestoreTermMode() error { } func (rr *RuneReader) ReadRune() (rune, int, error) { + if rr.state.sttyExe != "" { + return readRunePosix(rr.state.reader, false) + } + ir := &inputRecord{} bytesRead := 0 for { rv, _, e := readConsoleInput.Call(rr.stdio.In.Fd(), uintptr(unsafe.Pointer(ir)), 1, uintptr(unsafe.Pointer(&bytesRead))) // windows returns non-zero to indicate success if rv == 0 && e != nil { - return 0, 0, e + return 0, 0, fmt.Errorf("readConsoleInput: %#v", e) } if ir.eventType != EVENT_KEY { From 6cd7e15f67d35f3c950bd2ba61ca261313fbab46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Sun, 2 Oct 2022 18:06:45 +0200 Subject: [PATCH 3/4] fix windows import --- core/template.go | 20 ++++++++++---------- core/write.go | 11 ++++++----- survey.go | 1 - terminal/display_windows.go | 1 - 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/core/template.go b/core/template.go index 6b3d20cd..77f25850 100644 --- a/core/template.go +++ b/core/template.go @@ -23,11 +23,11 @@ var TemplateFuncsNoColor = map[string]interface{}{ }, } -//RunTemplate returns two formatted strings given a template and -//the data it requires. The first string returned is generated for -//user-facing output and may or may not contain ANSI escape codes -//for colored output. The second string does not contain escape codes -//and can be used by the renderer for layout purposes. +// RunTemplate returns two formatted strings given a template and +// the data it requires. The first string returned is generated for +// user-facing output and may or may not contain ANSI escape codes +// for colored output. The second string does not contain escape codes +// and can be used by the renderer for layout purposes. func RunTemplate(tmpl string, data interface{}) (string, string, error) { tPair, err := GetTemplatePair(tmpl) if err != nil { @@ -52,11 +52,11 @@ var ( memoMutex = &sync.RWMutex{} ) -//GetTemplatePair returns a pair of compiled templates where the -//first template is generated for user-facing output and the -//second is generated for use by the renderer. The second -//template does not contain any color escape codes, whereas -//the first template may or may not depending on DisableColor. +// GetTemplatePair returns a pair of compiled templates where the +// first template is generated for user-facing output and the +// second is generated for use by the renderer. The second +// template does not contain any color escape codes, whereas +// the first template may or may not depending on DisableColor. func GetTemplatePair(tmpl string) ([2]*template.Template, error) { memoMutex.RLock() if t, ok := memoizedGetTemplate[tmpl]; ok { diff --git a/core/write.go b/core/write.go index 94a886c3..caad7bc8 100644 --- a/core/write.go +++ b/core/write.go @@ -143,11 +143,12 @@ func (err errFieldNotMatch) Is(target error) bool { // implements the dynamic er // // Usage: // err := survey.Ask(qs, &v); -// if err != nil { -// if name, ok := core.IsFieldNotMatch(err); ok { -// [...name is the not matched question name] -// } -// } +// +// if err != nil { +// if name, ok := core.IsFieldNotMatch(err); ok { +// [...name is the not matched question name] +// } +// } func IsFieldNotMatch(err error) (string, bool) { if err != nil { if v, ok := err.(errFieldNotMatch); ok { diff --git a/survey.go b/survey.go index 59afea3c..aad73bbe 100644 --- a/survey.go +++ b/survey.go @@ -278,7 +278,6 @@ in the documentation. For example: } survey.AskOne(prompt, &name) - */ func AskOne(p Prompt, response interface{}, opts ...AskOpt) error { err := Ask([]*Question{{Prompt: p}}, response, opts...) diff --git a/terminal/display_windows.go b/terminal/display_windows.go index c629559a..fc9db9f7 100644 --- a/terminal/display_windows.go +++ b/terminal/display_windows.go @@ -1,7 +1,6 @@ package terminal import ( - "fmt" "syscall" "unsafe" ) From 0214094a2745ebc8bc3e715e846df6474335a604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Sun, 2 Oct 2022 18:12:08 +0200 Subject: [PATCH 4/4] vendor safeexec --- vendor/github.com/cli/safeexec/LICENSE | 25 ++++ vendor/github.com/cli/safeexec/README.md | 40 ++++++ vendor/github.com/cli/safeexec/go.mod | 3 + vendor/github.com/cli/safeexec/lookpath.go | 9 ++ .../cli/safeexec/lookpath_windows.go | 120 ++++++++++++++++++ vendor/modules.txt | 2 + 6 files changed, 199 insertions(+) create mode 100644 vendor/github.com/cli/safeexec/LICENSE create mode 100644 vendor/github.com/cli/safeexec/README.md create mode 100644 vendor/github.com/cli/safeexec/go.mod create mode 100644 vendor/github.com/cli/safeexec/lookpath.go create mode 100644 vendor/github.com/cli/safeexec/lookpath_windows.go diff --git a/vendor/github.com/cli/safeexec/LICENSE b/vendor/github.com/cli/safeexec/LICENSE new file mode 100644 index 00000000..ca498575 --- /dev/null +++ b/vendor/github.com/cli/safeexec/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2020, GitHub Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/cli/safeexec/README.md b/vendor/github.com/cli/safeexec/README.md new file mode 100644 index 00000000..bd73e9ad --- /dev/null +++ b/vendor/github.com/cli/safeexec/README.md @@ -0,0 +1,40 @@ +# safeexec + +A Go module that provides a safer alternative to `exec.LookPath()` on Windows. + +The following, relatively common approach to running external commands has a subtle vulnerability on Windows: +```go +import "os/exec" + +func gitStatus() error { + // On Windows, this will result in `.\git.exe` or `.\git.bat` being executed + // if either were found in the current working directory. + cmd := exec.Command("git", "status") + return cmd.Run() +} +``` + +Searching the current directory (surprising behavior) before searching folders listed in the PATH environment variable (expected behavior) seems to be intended in Go and unlikely to be changed: https://github.com/golang/go/issues/38736 + +Since Go does not provide a version of [`exec.LookPath()`](https://golang.org/pkg/os/exec/#LookPath) that only searches PATH and does not search the current working directory, this module provides a `LookPath` function that works consistently across platforms. + +Example use: +```go +import ( + "os/exec" + "github.com/cli/safeexec" +) + +func gitStatus() error { + gitBin, err := safeexec.LookPath("git") + if err != nil { + return err + } + cmd := exec.Command(gitBin, "status") + return cmd.Run() +} +``` + +## TODO + +Ideally, this module would also provide `exec.Command()` and `exec.CommandContext()` equivalents that delegate to the patched version of `LookPath`. However, this doesn't seem possible since `LookPath` may return an error, while `exec.Command/CommandContext()` themselves do not return an error. In the standard library, the resulting `exec.Cmd` struct stores the LookPath error in a private field, but that functionality isn't available to us. diff --git a/vendor/github.com/cli/safeexec/go.mod b/vendor/github.com/cli/safeexec/go.mod new file mode 100644 index 00000000..266fab44 --- /dev/null +++ b/vendor/github.com/cli/safeexec/go.mod @@ -0,0 +1,3 @@ +module github.com/cli/safeexec + +go 1.15 diff --git a/vendor/github.com/cli/safeexec/lookpath.go b/vendor/github.com/cli/safeexec/lookpath.go new file mode 100644 index 00000000..41b77707 --- /dev/null +++ b/vendor/github.com/cli/safeexec/lookpath.go @@ -0,0 +1,9 @@ +// +build !windows + +package safeexec + +import "os/exec" + +func LookPath(file string) (string, error) { + return exec.LookPath(file) +} diff --git a/vendor/github.com/cli/safeexec/lookpath_windows.go b/vendor/github.com/cli/safeexec/lookpath_windows.go new file mode 100644 index 00000000..19b3e52f --- /dev/null +++ b/vendor/github.com/cli/safeexec/lookpath_windows.go @@ -0,0 +1,120 @@ +// Copyright (c) 2009 The Go Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Package safeexec provides alternatives for exec package functions to avoid +// accidentally executing binaries found in the current working directory on +// Windows. +package safeexec + +import ( + "os" + "os/exec" + "path/filepath" + "strings" +) + +func chkStat(file string) error { + d, err := os.Stat(file) + if err != nil { + return err + } + if d.IsDir() { + return os.ErrPermission + } + return nil +} + +func hasExt(file string) bool { + i := strings.LastIndex(file, ".") + if i < 0 { + return false + } + return strings.LastIndexAny(file, `:\/`) < i +} + +func findExecutable(file string, exts []string) (string, error) { + if len(exts) == 0 { + return file, chkStat(file) + } + if hasExt(file) { + if chkStat(file) == nil { + return file, nil + } + } + for _, e := range exts { + if f := file + e; chkStat(f) == nil { + return f, nil + } + } + return "", os.ErrNotExist +} + +// LookPath searches for an executable named file in the +// directories named by the PATH environment variable. +// If file contains a slash, it is tried directly and the PATH is not consulted. +// LookPath also uses PATHEXT environment variable to match +// a suitable candidate. +// The result may be an absolute path or a path relative to the current directory. +func LookPath(file string) (string, error) { + var exts []string + x := os.Getenv(`PATHEXT`) + if x != "" { + for _, e := range strings.Split(strings.ToLower(x), `;`) { + if e == "" { + continue + } + if e[0] != '.' { + e = "." + e + } + exts = append(exts, e) + } + } else { + exts = []string{".com", ".exe", ".bat", ".cmd"} + } + + if strings.ContainsAny(file, `:\/`) { + if f, err := findExecutable(file, exts); err == nil { + return f, nil + } else { + return "", &exec.Error{file, err} + } + } + + // https://github.com/golang/go/issues/38736 + // if f, err := findExecutable(filepath.Join(".", file), exts); err == nil { + // return f, nil + // } + + path := os.Getenv("path") + for _, dir := range filepath.SplitList(path) { + if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil { + return f, nil + } + } + return "", &exec.Error{file, exec.ErrNotFound} +} diff --git a/vendor/modules.txt b/vendor/modules.txt index d7817c50..655c7089 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,5 +1,7 @@ # github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 github.com/Netflix/go-expect +# github.com/cli/safeexec v1.0.0 +github.com/cli/safeexec # github.com/creack/pty v1.1.17 github.com/creack/pty # github.com/davecgh/go-spew v1.1.1