summaryrefslogtreecommitdiffstats
path: root/pkg/commands/oscommands/exec_live.go
blob: 21c1c8d27be934b3b291c6951fbb9217fc85bc0a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package oscommands

import (
	"bufio"
	"bytes"
	"io"
	"os/exec"
	"regexp"
	"strings"
	"unicode/utf8"

	"github.com/go-errors/errors"
	"github.com/jesseduffield/lazygit/pkg/utils"
)

// RunAndDetectCredentialRequest detect a username / password / passphrase question in a command
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
func (self *cmdObjRunner) RunAndDetectCredentialRequest(cmdObj ICmdObj, promptUserForCredential func(string) string) error {
	ttyText := ""
	err := self.RunCommandWithOutputLive(cmdObj, func(word string) string {
		ttyText = ttyText + " " + word

		prompts := map[string]string{
			`.+'s password:`:                         "password",
			`Password\s*for\s*'.+':`:                 "password",
			`Username\s*for\s*'.+':`:                 "username",
			`Enter\s*passphrase\s*for\s*key\s*'.+':`: "passphrase",
		}

		for pattern, askFor := range prompts {
			if match, _ := regexp.MatchString(pattern, ttyText); match {
				ttyText = ""
				return promptUserForCredential(askFor)
			}
		}

		return ""
	})
	return err
}

type cmdHandler struct {
	stdoutPipe io.Reader
	stdinPipe  io.Writer
	close      func() error
}

// RunCommandWithOutputLiveAux runs a command and return every word that gets written in stdout
// Output is a function that executes by every word that gets read by bufio
// As return of output you need to give a string that will be written to stdin
// NOTE: If the return data is empty it won't write anything to stdin
func (self *cmdObjRunner) RunCommandWithOutputLiveAux(
	cmdObj ICmdObj,
	// handleOutput takes a word from stdout and returns a string to be written to stdin.
	// See RunAndDetectCredentialRequest above for how this is used to check for a username/password request
	handleOutput func(string) string,
	startCmd func(cmd *exec.Cmd) (*cmdHandler, error),
) error {
	cmdWriter := self.guiIO.newCmdWriterFn()
	self.log.WithField("command", cmdObj.ToString()).Info("RunCommand")
	if cmdObj.ShouldLog() {
		self.logCmdObj(cmdObj)
	}
	cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd()

	var stderr bytes.Buffer
	cmd.Stderr = io.MultiWriter(cmdWriter, &stderr)

	handler, err := startCmd(cmd)
	if err != nil {
		return err
	}

	defer func() {
		if closeErr := handler.close(); closeErr != nil {
			self.log.Error(closeErr)
		}
	}()

	tr := io.TeeReader(handler.stdoutPipe, cmdWriter)

	go utils.Safe(func() {
		scanner := bufio.NewScanner(tr)
		scanner.Split(scanWordsWithNewLines)
		for scanner.Scan() {
			text := scanner.Text()
			output := strings.Trim(text, " ")
			toInput := handleOutput(output)
			if toInput != "" {
				_, _ = handler.stdinPipe.Write([]byte(toInput))
			}
		}
	})

	err = cmd.Wait()
	if err != nil {
		return errors.New(stderr.String())
	}

	return nil
}

// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
// For specific comments about this function take a look at: bufio.ScanWords
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
	start := 0
	for width := 0; start < len(data); start += width {
		var r rune
		r, width = utf8.DecodeRune(data[start:])
		if !isSpace(r) {
			break
		}
	}
	for width, i := 0, start; i < len(data); i += width {
		var r rune
		r, width = utf8.DecodeRune(data[i:])
		if isSpace(r) {
			return i + width, data[start:i], nil
		}
	}
	if atEOF && len(data) > start {
		return len(data), data[start:], nil
	}
	return start, nil, nil
}

// isSpace is also copied from the bufio package and has been modified to also captures new lines
// For specific comments about this function take a look at: bufio.isSpace
func isSpace(r rune) bool {
	if r <= '\u00FF' {
		switch r {
		case ' ', '\t', '\v', '\f':
			return true
		case '\u0085', '\u00A0':
			return true
		}
		return false
	}
	if '\u2000' <= r && r <= '\u200a' {
		return true
	}
	switch r {
	case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
		return true
	}
	return false
}