summaryrefslogtreecommitdiff
path: root/pkg/cli
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/cli')
-rw-r--r--pkg/cli/command.go63
-rw-r--r--pkg/cli/interface.go157
-rw-r--r--pkg/cli/query.go77
-rw-r--r--pkg/cli/ui.go2
4 files changed, 269 insertions, 30 deletions
diff --git a/pkg/cli/command.go b/pkg/cli/command.go
new file mode 100644
index 0000000..5b3ded5
--- /dev/null
+++ b/pkg/cli/command.go
@@ -0,0 +1,63 @@
+/*******************************************************************************
+*
+* 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
+//Interface 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(),
+ )
+}
+
+func (c Command) run(stdout, stderr io.Writer) error {
+ cmd := exec.Command(c.Program[0], c.Program[1:]...)
+ 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
new file mode 100644
index 0000000..7ebde9d
--- /dev/null
+++ b/pkg/cli/interface.go
@@ -0,0 +1,157 @@
+/*******************************************************************************
+*
+* 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"
+ "io"
+ "os"
+ "sort"
+ "strings"
+)
+
+//NewInterface creates an Interface instance.
+func NewInterface(stdin, stdout, stderr *os.File) (*Interface, error) {
+ //TODO: check terminal.IsTerminal(int(stdin.Fd())) and choose the TUI instance accordingly
+ return &Interface{
+ stdin: stdin,
+ stdout: stdout,
+ stderr: stderr,
+ stdinBuf: bufio.NewReader(stdin),
+ tui: &terminalTUI{},
+ }, nil
+}
+
+//Interface wraps access to the CLI, including input, output and subprocesses.
+type Interface struct {
+ //TODO: flag isStdinTerminal that disables color output and swaps out the TUI instance
+ stdin io.Reader
+ stdout io.Writer
+ stderr io.Writer
+ stdinBuf *bufio.Reader
+ tui TUI
+ //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.Interface, 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)
+}
+
+func (i *Interface) 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 *Interface) 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 *Interface) 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 *Interface) 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 *Interface) Run(c Command) error {
+ return c.run(i.safeStdout(), i.stderr)
+}
+
+//CaptureStdout executes the given command on the same stderr and captures its stdout.
+func (i *Interface) CaptureStdout(c Command) (string, error) {
+ var buf bytes.Buffer
+ err := c.run(&buf, i.stderr)
+ return string(buf.Bytes()), err
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// output
+
+//ShowResult displays the result of a computation on stdout.
+func (i *Interface) 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 *Interface) ShowResultsSorted(strs []string) {
+ sort.Strings(strs)
+ for _, str := range strs {
+ i.ShowResult(str)
+ }
+}
+
+//ShowProgress displays a progress message on stderr.
+func (i *Interface) ShowProgress(str string) {
+ i.stderr.Write(StyledText{
+ Styled(">> ", AnsiNormal, AnsiBold, AnsiCyan),
+ Styled(strings.TrimSpace(str)+"\n", AnsiCyan),
+ }.DisplayString(true))
+}
+
+//ShowWarning displays a warning message on stderr.
+func (i *Interface) ShowWarning(str string) {
+ i.stderr.Write(StyledText{
+ Styled("!! ", AnsiNormal, AnsiBold, AnsiYellow),
+ Styled(strings.TrimSpace(str)+"\n", AnsiYellow),
+ }.DisplayString(true))
+}
+
+//ShowError displays an error message on stderr.
+func (i *Interface) ShowError(err string) {
+ i.stderr.Write(StyledText{
+ Styled("!! ", AnsiNormal, AnsiBold, AnsiRed),
+ Styled(strings.TrimSpace(err)+"\n", AnsiRed),
+ }.DisplayString(true))
+}
+
+//ShowUsage displays a usage synopsis on stderr.
+func (i *Interface) 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
index 40baaec..1f5d16c 100644
--- a/pkg/cli/query.go
+++ b/pkg/cli/query.go
@@ -21,29 +21,47 @@ package cli
import (
"fmt"
"io"
- "os"
"regexp"
"strings"
terminal "golang.org/x/crypto/ssh/terminal"
)
-//Confirm displays a yes/no question and returns whether the user answered "yes".
-func Confirm(question string) bool {
- os.Stdout.Write([]byte(strings.TrimSpace(question) + " [y/n] "))
+//cannot use `var errInterrupted = errors.New("Interrupted!")` because golint
+//complains about the formatting of the error message
+type errInterrupted struct{}
- buf := buffer{Input: os.Stdin}
+func (e errInterrupted) Error() string {
+ return "Interrupted!"
+}
+
+type terminalTUI struct {
+ i *Interface
+}
+
+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":
- os.Stdout.Write([]byte("-> yes\n"))
- return true
+ out.Write([]byte("-> yes\n"))
+ return true, nil
case "n", "N":
- os.Stdout.Write([]byte("-> no\n"))
- return false
+ out.Write([]byte("-> no\n"))
+ return false, nil
case "\x03": // Ctrl-C
- fmt.Fprintln(os.Stderr, "\nInterrupted!")
- os.Exit(255)
+ return false, errInterrupted{}
}
}
}
@@ -55,32 +73,32 @@ type Choice struct {
Shortcut byte
//The display string that describes this choice.
Text string
+ //The string to return from Interface.Query().
+ Return string
}
func (c Choice) hasShortcut() bool {
return c.Shortcut != '\000'
}
-//Query displays a question and a set of answers and allows the user to select
-//one of the answers. Returns the selected Choice instance, as well as its
-//index in the original choices list (starting from 0).
-func Query(prompt string, choices ...Choice) (Choice, int) {
+func (t terminalTUI) Query(prompt string, choices ...Choice) (string, error) {
if len(choices) == 0 {
- return Choice{}, -1
+ panic("no choices")
}
//disable line wrap; unexpected wrapping would confuse our cursor-moving code
- os.Stdout.Write([]byte("\x1B[?7l"))
- defer os.Stdout.Write([]byte("\x1B[?7h"))
+ out := t.i.safeStdout()
+ out.Write([]byte("\x1B[?7l"))
+ defer out.Write([]byte("\x1B[?7h"))
//display question
- os.Stdout.Write([]byte(strings.TrimSuffix(prompt, "\n") + "\n"))
+ out.Write([]byte(strings.TrimSuffix(prompt, "\n") + "\n"))
selected := 0
- buf := buffer{Input: os.Stdin}
+ buf := buffer{Input: t.i.stdin}
OUTER:
for {
- displayChoices(os.Stdout, choices, selected)
+ displayChoices(out, choices, selected)
input := buf.getNextInput()
if len(input) == 1 {
@@ -107,29 +125,28 @@ OUTER:
selected = len(choices) - 1
}
case "\x03": // Ctrl-C
- fmt.Fprintln(os.Stderr, "Interrupted!")
- os.Exit(255)
+ return "", errInterrupted{}
}
//prepare to re-render choices
- removeDisplayLines(len(choices))
+ removeDisplayLines(out, len(choices))
}
//clear query display
- removeDisplayLines(len(choices) + 1)
+ removeDisplayLines(out, len(choices)+1)
//display question + chosen answer
- fmt.Fprintf(os.Stdout, "%s -> %s\n",
+ fmt.Fprintf(out, "%s -> %s\n",
strings.TrimSuffix(prompt, "\n"),
strings.TrimSpace(choices[selected].Text),
)
- return choices[selected], selected
+ return choices[selected].Return, nil
}
-func removeDisplayLines(n int) {
+func removeDisplayLines(stdout io.Writer, n int) {
for idx := 0; idx < n; idx++ {
- os.Stdout.Write([]byte("\x1B[A\x1B[2K"))
+ stdout.Write([]byte("\x1B[A\x1B[2K"))
}
}
@@ -193,7 +210,7 @@ func (b *buffer) getNextInput() []byte {
defer terminal.Restore(0, oldState)
//fill buffer some more
- n, err := os.Stdin.Read(b.buf[b.fill:])
+ n, err := b.Input.Read(b.buf[b.fill:])
if err != nil {
panic(err)
}
diff --git a/pkg/cli/ui.go b/pkg/cli/ui.go
index c4eff69..60ae553 100644
--- a/pkg/cli/ui.go
+++ b/pkg/cli/ui.go
@@ -29,6 +29,8 @@ type AnsiStyle string
const (
//AnsiNormal reverts all styles to default.
AnsiNormal AnsiStyle = "0"
+ //AnsiBold makes the text bold.
+ AnsiBold AnsiStyle = "1"
//AnsiInverse swaps foreground and background color.
AnsiInverse AnsiStyle = "7"
//AnsiBlack is a foreground color.