diff options
Diffstat (limited to 'pkg/cli')
| -rw-r--r-- | pkg/cli/command.go | 70 | ||||
| -rw-r--r-- | pkg/cli/interface.go | 168 | ||||
| -rw-r--r-- | pkg/cli/query.go | 295 |
3 files changed, 0 insertions, 533 deletions
diff --git a/pkg/cli/command.go b/pkg/cli/command.go deleted file mode 100644 index a9938d1..0000000 --- a/pkg/cli/command.go +++ /dev/null @@ -1,70 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky <majewsky@gmx.net> -* -* This program is free software: you can redistribute it and/or modify it under -* the terms of the GNU General Public License as published by the Free Software -* Foundation, either version 3 of the License, or (at your option) any later -* version. -* -* This program is distributed in the hope that it will be useful, but WITHOUT ANY -* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -* A PARTICULAR PURPOSE. See the GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License along with -* this program. If not, see <http://www.gnu.org/licenses/>. -* -*******************************************************************************/ - -package cli - -import ( - "fmt" - "io" - "os/exec" - "strings" -) - -//Command describes a command that can be run using the methods in the -//Implementation interface. -type Command struct { - Program []string - WorkDir string -} - -type commandError struct { - Cmd Command - Err error -} - -func (e commandError) Error() string { - cmdline := strings.Join(e.Cmd.Program, " ") - if e.Cmd.WorkDir == "" { - return fmt.Sprintf("exec `%s`: %s", - cmdline, e.Err.Error(), - ) - } - return fmt.Sprintf("exec `%s` in %s: %s", - cmdline, e.Cmd.WorkDir, e.Err.Error(), - ) -} - -//CommandRunner is a function that can execute commands given to it. -//This interface is only useful for unit tests; the default CommandRunner -//suffices for all regular operation. -type CommandRunner func(c Command, stdin io.Reader, stdout, stderr io.Writer) error - -//DefaultCommandRunner is a CommandRunner that actually executes the command. -func DefaultCommandRunner(c Command, stdin io.Reader, stdout, stderr io.Writer) error { - cmd := exec.Command(c.Program[0], c.Program[1:]...) - cmd.Stdin = stdin - cmd.Stdout = stdout - cmd.Stderr = stderr - cmd.Dir = c.WorkDir - - err := cmd.Run() - if err != nil { - err = commandError{c, err} - } - return err -} diff --git a/pkg/cli/interface.go b/pkg/cli/interface.go deleted file mode 100644 index a88212b..0000000 --- a/pkg/cli/interface.go +++ /dev/null @@ -1,168 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky <majewsky@gmx.net> -* -* This program is free software: you can redistribute it and/or modify it under -* the terms of the GNU General Public License as published by the Free Software -* Foundation, either version 3 of the License, or (at your option) any later -* version. -* -* This program is distributed in the hope that it will be useful, but WITHOUT ANY -* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -* A PARTICULAR PURPOSE. See the GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License along with -* this program. If not, see <http://www.gnu.org/licenses/>. -* -*******************************************************************************/ - -package cli - -import ( - "bufio" - "bytes" - "fmt" - "io" - "os" - "sort" - "strings" - - "golang.org/x/crypto/ssh/terminal" -) - -//Interface wraps access to the CLI, including input, output and subprocesses. -var Interface *Implementation - -func init() { - SetupInterface(os.Stdin, os.Stdout, os.Stderr, DefaultCommandRunner) -} - -//SetupInterface prepares the Interface instance with nonstandard file streams -//or a nonstandard CommandRunner. This is only required for unit tests. -func SetupInterface(stdin io.Reader, stdout, stderr io.Writer, commandRunner CommandRunner) { - Interface = &Implementation{ - stdin: stdin, - stdout: stdout, - stderr: stderr, - stdinBuf: bufio.NewReader(stdin), - commandRunner: commandRunner, - } - - if stdinFile, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(stdinFile.Fd())) { - Interface.tui = &terminalTUI{Interface} - } else { - Interface.tui = &pipeTUI{Interface} - } -} - -//Implementation wraps access to the CLI, including input, output and subprocesses. -type Implementation struct { - stdin io.Reader - stdout io.Writer - stderr io.Writer - stdinBuf *bufio.Reader - tui TUI - commandRunner CommandRunner - //If this flag is set, only ShowResult() will write into stdout; everything - //else that usually goes to stdout goes to stderr instead. - // - //This is useful when gofu is expected to output a certain value to stdout - //which is used by the next program in the pipe, and additional output from - //subprocesses could confuse the stdout handler. - StdoutProtected bool -} - -//TUI provides the interactive parts of the cli.Implementation, so that these can be -//easily swapped out for mock implementations in unit tests. -type TUI interface { - //ReadLine reads a line from stdin (if tty: uses canonical mode). - ReadLine(prompt string) (string, error) - //Confirm displays a yes/no question and returns whether the user answered "yes". - Confirm(question string) (bool, error) - //Query displays a question and a set of answers and allows the user to select - //one of the answers. Returns the Return attribute of the selected Choice. - Query(prompt string, choices ...Choice) (string, error) - //Print writes the given string (potentially including ANSI escape codes) to - //the given writer. At this point, it can be decided whether to strip out the - //ANSI escape codes. - Print(w io.Writer, msg string) -} - -func (i *Implementation) safeStdout() io.Writer { - if i.StdoutProtected { - return i.stderr - } - return i.stdout -} - -//////////////////////////////////////////////////////////////////////////////// -// input - -//ReadLine reads a line from stdin (if tty: uses canonical mode). -func (i *Implementation) ReadLine(prompt string) (string, error) { - return i.tui.ReadLine(prompt) -} - -//Confirm displays a yes/no question and returns whether the user answered "yes". -func (i *Implementation) Confirm(question string) (bool, error) { - return i.tui.Confirm(question) -} - -//Query displays a question and a set of answers and allows the user to select -//one of the answers. Returns the Return attribute of the selected Choice. -func (i *Implementation) Query(prompt string, choices ...Choice) (string, error) { - return i.tui.Query(prompt, choices...) -} - -//////////////////////////////////////////////////////////////////////////////// -// subprocesses - -//Run executes the given command on the same stdout and stderr. -func (i *Implementation) Run(c Command) error { - return i.commandRunner(c, nil, i.safeStdout(), i.stderr) -} - -//CaptureStdout executes the given command on the same stderr and captures its stdout. -func (i *Implementation) CaptureStdout(c Command) (string, error) { - var buf bytes.Buffer - err := i.commandRunner(c, nil, &buf, i.stderr) - return string(buf.Bytes()), err -} - -//////////////////////////////////////////////////////////////////////////////// -// output - -//ShowResult displays the result of a computation on stdout. -func (i *Implementation) ShowResult(str string) { - str = strings.TrimSpace(str) + "\n" - i.stdout.Write([]byte(str)) -} - -//ShowResultsSorted calls ShowResult() on each of the results after sorting them. -func (i *Implementation) ShowResultsSorted(strs []string) { - sort.Strings(strs) - for _, str := range strs { - i.ShowResult(str) - } -} - -//ShowProgress displays a progress message on stderr. -func (i *Implementation) ShowProgress(str string) { - i.tui.Print(i.stderr, fmt.Sprintf("\x1B[0;1;36m>>\x1B[0;36m %s\x1B[0m\n", strings.TrimSpace(str))) -} - -//ShowWarning displays a warning message on stderr. -func (i *Implementation) ShowWarning(str string) { - i.tui.Print(i.stderr, fmt.Sprintf("\x1B[0;1;33m!!\x1B[0;33m %s\x1B[0m\n", strings.TrimSpace(str))) -} - -//ShowError displays an error message on stderr. -func (i *Implementation) ShowError(str string) { - i.tui.Print(i.stderr, fmt.Sprintf("\x1B[0;1;31m!!\x1B[0;31m %s\x1B[0m\n", strings.TrimSpace(str))) -} - -//ShowUsage displays a usage synopsis on stderr. -func (i *Implementation) ShowUsage(str string) { - str = strings.TrimSpace(str) + "\n" - i.stderr.Write([]byte(str)) -} diff --git a/pkg/cli/query.go b/pkg/cli/query.go deleted file mode 100644 index add170c..0000000 --- a/pkg/cli/query.go +++ /dev/null @@ -1,295 +0,0 @@ -/******************************************************************************* -* -* Copyright 2017 Stefan Majewsky <majewsky@gmx.net> -* -* This program is free software: you can redistribute it and/or modify it under -* the terms of the GNU General Public License as published by the Free Software -* Foundation, either version 3 of the License, or (at your option) any later -* version. -* -* This program is distributed in the hope that it will be useful, but WITHOUT ANY -* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -* A PARTICULAR PURPOSE. See the GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License along with -* this program. If not, see <http://www.gnu.org/licenses/>. -* -*******************************************************************************/ - -package cli - -import ( - "errors" - "fmt" - "io" - "regexp" - "strconv" - "strings" - - terminal "golang.org/x/crypto/ssh/terminal" -) - -//cannot use `var errInterrupted = errors.New("Interrupted!")` because golint -//complains about the formatting of the error message -type errInterrupted struct{} - -func (e errInterrupted) Error() string { - return "Interrupted!" -} - -//////////////////////////////////////////////////////////////////////////////// -// TUI implementation for when stdin is a terminal - -type terminalTUI struct { - i *Implementation -} - -func (t terminalTUI) Print(w io.Writer, msg string) { - w.Write([]byte(msg)) -} - -func (t terminalTUI) ReadLine(prompt string) (string, error) { - if prompt != "" { - t.i.safeStdout().Write([]byte(strings.TrimSpace(prompt) + " ")) - } - str, err := t.i.stdinBuf.ReadString('\n') - return strings.TrimSpace(str), err -} - -func (t terminalTUI) Confirm(question string) (bool, error) { - out := t.i.safeStdout() - out.Write([]byte(strings.TrimSpace(question) + " [y/n] ")) - - buf := buffer{Input: t.i.stdin} - for { - switch string(buf.getNextInput()) { - case "y", "Y": - out.Write([]byte("-> yes\n")) - return true, nil - case "n", "N": - out.Write([]byte("-> no\n")) - return false, nil - case "\x03": // Ctrl-C - return false, errInterrupted{} - } - } -} - -//Choice is a thing that the user can choose during Query(). -type Choice struct { - //If given, the choice can be selected by pressing the key that - //produces this character. - Shortcut byte - //The display string that describes this choice. - Text string - //The string to return from Implementation.Query(). - Return string -} - -func (c Choice) hasShortcut() bool { - return c.Shortcut != '\000' -} - -func (t terminalTUI) Query(prompt string, choices ...Choice) (string, error) { - if len(choices) == 0 { - panic("no choices") - } - - //disable line wrap; unexpected wrapping would confuse our cursor-moving code - out := t.i.safeStdout() - out.Write([]byte("\x1B[?7l")) - defer out.Write([]byte("\x1B[?7h")) - - //display question - out.Write([]byte(strings.TrimSuffix(prompt, "\n") + "\n")) - selected := 0 - - buf := buffer{Input: t.i.stdin} -OUTER: - for { - displayChoices(out, choices, selected) - - input := buf.getNextInput() - if len(input) == 1 { - //single character entered - check if a shortcut matches - for idx, choice := range choices { - if choice.Shortcut == input[0] { - selected = idx - break OUTER - } - } - } - - switch string(input) { - case "\r", "\n": - break OUTER - case "\x1B[A": // Up arrow key - selected-- - if selected < 0 { - selected = 0 - } - case "\x1B[B": // Down arrow key - selected++ - if selected >= len(choices) { - selected = len(choices) - 1 - } - case "\x03": // Ctrl-C - return "", errInterrupted{} - } - - //prepare to re-render choices - removeDisplayLines(out, len(choices)) - } - - //clear query display - removeDisplayLines(out, len(choices)+1) - - //display question + chosen answer - fmt.Fprintf(out, "%s -> %s\n", - strings.TrimSuffix(prompt, "\n"), - strings.TrimSpace(choices[selected].Text), - ) - - return choices[selected].Return, nil -} - -func removeDisplayLines(stdout io.Writer, n int) { - for idx := 0; idx < n; idx++ { - stdout.Write([]byte("\x1B[A\x1B[2K")) - } -} - -func displayChoices(out io.Writer, choices []Choice, selectedIndex int) { - hasShortcuts := false - for _, choice := range choices { - if choice.hasShortcut() { - hasShortcuts = true - break - } - } - - for idx, choice := range choices { - text := " " + strings.TrimSpace(choice.Text) + " \n" - if hasShortcuts { - shortcut := choice.Shortcut - if !choice.hasShortcut() { - shortcut = ' ' - } - text = fmt.Sprintf(" [%c]%s", shortcut, text) - } - - if idx == selectedIndex { - fmt.Fprintf(out, "\x1B[0;7m%s\x1B[0m", text) - } else { - out.Write([]byte(text)) - } - } -} - -var ansiEscapeRx = regexp.MustCompile(`^\x1B\[[\x20-\x3F]*[\x40-\x7E]`) - -type buffer struct { - Input io.Reader - buf [128]byte - fill int -} - -func (b *buffer) getNextInput() []byte { - //do we have a simple input character? - if b.fill > 0 && b.buf[0] != '\x1B' { - result := append([]byte(nil), b.buf[0]) - copy(b.buf[0:], b.buf[1:]) - b.fill-- - return result - } - - //do we have a full ANSI escape sequence? - match := ansiEscapeRx.Find(b.buf[0:b.fill]) - if match != nil { - result := append([]byte(nil), match...) - copy(b.buf[0:], b.buf[len(match):]) - b.fill -= len(match) - return result - } - - oldState, err := terminal.MakeRaw(0) - if err != nil { - panic(err) - } - defer terminal.Restore(0, oldState) - - //fill buffer some more - n, err := b.Input.Read(b.buf[b.fill:]) - if err != nil { - panic(err) - } - b.fill += n - - return b.getNextInput() //restart -} - -//////////////////////////////////////////////////////////////////////////////// -// TUI implementation for when stdin is a pipe (also used in unit tests) - -type pipeTUI struct { - i *Implementation -} - -//AnsiColorCodeRx is a regexp that matches ANSI escape sequences of the type SGR. -var AnsiColorCodeRx = regexp.MustCompile("\x1B" + `\[[0-9;]*m`) - -func (t *pipeTUI) Print(w io.Writer, msg string) { - w.Write([]byte(AnsiColorCodeRx.ReplaceAllString(msg, ""))) -} - -func (t *pipeTUI) ReadLine(prompt string) (string, error) { - str, err := t.i.stdinBuf.ReadString('\n') - str = strings.TrimSpace(str) - if err == nil { - fmt.Fprintf(t.i.stderr, "%s %s\n", strings.TrimSpace(prompt), str) - } - return str, err -} - -func (t *pipeTUI) Confirm(question string) (bool, error) { - question = strings.TrimSpace(question) - str, err := t.ReadLine(question) - if err != nil { - return false, err - } - //recognize /[01tTfF]|true|false/ - ok, err := strconv.ParseBool(str) - if err != nil { - //recognize /[yY]|yes|no/ - ok = strings.HasPrefix(question, "y") || strings.HasPrefix(question, "Y") - } - fmt.Fprintf(t.i.stderr, "%s -> %v (%s)\n", question, ok, str) - return ok, nil -} - -//Query for the pipe TUI is limited to exact matches of the choice text, or -//matching by choice shortcut. -func (t *pipeTUI) Query(prompt string, choices ...Choice) (string, error) { - str, err := t.i.stdinBuf.ReadString('\n') - if err != nil { - return str, err - } - str = strings.TrimSpace(str) - - //prefer exact match on choice.Text - for _, choice := range choices { - if choice.Text == str { - fmt.Fprintf(t.i.stderr, "%s -> %s\n", prompt, choice.Text) - return choice.Return, nil - } - } - //allow match on choice.Shortcut - for _, choice := range choices { - if string(choice.Shortcut) == str { - fmt.Fprintf(t.i.stderr, "%s -> %s\n", prompt, choice.Text) - return choice.Return, nil - } - } - fmt.Fprintf(t.i.stderr, "%s -> [%s]\n", prompt, str) - return "", errors.New("cannot match input with available choices") -} |
