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 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
|
package oscommands
import (
"os/exec"
"strings"
"github.com/jesseduffield/gocui"
"github.com/samber/lo"
"github.com/sasha-s/go-deadlock"
)
// A command object is a general way to represent a command to be run on the
// command line.
type ICmdObj interface {
GetCmd() *exec.Cmd
// outputs string representation of command. Note that if the command was built
// using NewFromArgs, the output won't be quite the same as what you would type
// into a terminal e.g. 'sh -c git commit' as opposed to 'sh -c "git commit"'
ToString() string
// outputs args vector e.g. ["git", "commit", "-m", "my message"]
Args() []string
// Set a string to be used as stdin for the command.
SetStdin(input string) ICmdObj
AddEnvVars(...string) ICmdObj
GetEnvVars() []string
// sets the working directory
SetWd(string) ICmdObj
// runs the command and returns an error if any
Run() error
// runs the command and returns the output as a string, and an error if any
RunWithOutput() (string, error)
// runs the command and returns stdout and stderr as a string, and an error if any
RunWithOutputs() (string, string, error)
// runs the command and runs a callback function on each line of the output. If the callback returns true for the boolean value, we kill the process and return.
RunAndProcessLines(onLine func(line string) (bool, error)) error
// Be calling DontLog(), we're saying that once we call Run(), we don't want to
// log the command in the UI (it'll still be logged in the log file). The general rule
// is that if a command doesn't change the git state (e.g. read commands like `git diff`)
// then we don't want to log it. If we are changing something (e.g. `git add .`) then
// we do. The only exception is if we're running a command in the background periodically
// like `git fetch`, which technically does mutate stuff but isn't something we need
// to notify the user about.
DontLog() ICmdObj
// This returns false if DontLog() was called
ShouldLog() bool
// when you call this, then call Run(), we'll stream the output to the cmdWriter (i.e. the command log panel)
StreamOutput() ICmdObj
// returns true if StreamOutput() was called
ShouldStreamOutput() bool
// if you call this before ShouldStreamOutput we'll consider an error with no
// stderr content as a non-error. Not yet supported for Run or RunWithOutput (
// but adding support is trivial)
IgnoreEmptyError() ICmdObj
// returns true if IgnoreEmptyError() was called
ShouldIgnoreEmptyError() bool
PromptOnCredentialRequest(task gocui.Task) ICmdObj
FailOnCredentialRequest() ICmdObj
WithMutex(mutex *deadlock.Mutex) ICmdObj
Mutex() *deadlock.Mutex
GetCredentialStrategy() CredentialStrategy
GetTask() gocui.Task
Clone() ICmdObj
}
type CmdObj struct {
cmd *exec.Cmd
runner ICmdObjRunner
// see DontLog()
dontLog bool
// see StreamOutput()
streamOutput bool
// see IgnoreEmptyError()
ignoreEmptyError bool
// if set to true, it means we might be asked to enter a username/password by this command.
credentialStrategy CredentialStrategy
task gocui.Task
// can be set so that we don't run certain commands simultaneously
mutex *deadlock.Mutex
}
type CredentialStrategy int
const (
// do not expect a credential request. If we end up getting one
// we'll be in trouble because the command will hang indefinitely
NONE CredentialStrategy = iota
// expect a credential request and if we get one, prompt the user to enter their username/password
PROMPT
// in this case we will check for a credential request (i.e. the command pauses to ask for
// username/password) and if we get one, we just submit a newline, forcing the
// command to fail. We use this e.g. for a background `git fetch` to prevent it
// from hanging indefinitely.
FAIL
)
var _ ICmdObj = &CmdObj{}
func (self *CmdObj) GetCmd() *exec.Cmd {
return self.cmd
}
func (self *CmdObj) ToString() string {
// if a given arg contains a space, we need to wrap it in quotes
quotedArgs := lo.Map(self.cmd.Args, func(arg string, _ int) string {
if strings.Contains(arg, " ") {
return `"` + arg + `"`
}
return arg
})
return strings.Join(quotedArgs, " ")
}
func (self *CmdObj) Args() []string {
return self.cmd.Args
}
func (self *CmdObj) SetStdin(input string) ICmdObj {
self.cmd.Stdin = strings.NewReader(input)
return self
}
func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
self.cmd.Env = append(self.cmd.Env, vars...)
return self
}
func (self *CmdObj) GetEnvVars() []string {
return self.cmd.Env
}
func (self *CmdObj) SetWd(wd string) ICmdObj {
self.cmd.Dir = wd
return self
}
func (self *CmdObj) DontLog() ICmdObj {
self.dontLog = true
return self
}
func (self *CmdObj) ShouldLog() bool {
return !self.dontLog
}
func (self *CmdObj) StreamOutput() ICmdObj {
self.streamOutput = true
return self
}
func (self *CmdObj) ShouldStreamOutput() bool {
return self.streamOutput
}
func (self *CmdObj) IgnoreEmptyError() ICmdObj {
self.ignoreEmptyError = true
return self
}
func (self *CmdObj) Mutex() *deadlock.Mutex {
return self.mutex
}
func (self *CmdObj) WithMutex(mutex *deadlock.Mutex) ICmdObj {
self.mutex = mutex
return self
}
func (self *CmdObj) ShouldIgnoreEmptyError() bool {
return self.ignoreEmptyError
}
func (self *CmdObj) Run() error {
return self.runner.Run(self)
}
func (self *CmdObj) RunWithOutput() (string, error) {
return self.runner.RunWithOutput(self)
}
func (self *CmdObj) RunWithOutputs() (string, string, error) {
return self.runner.RunWithOutputs(self)
}
func (self *CmdObj) RunAndProcessLines(onLine func(line string) (bool, error)) error {
return self.runner.RunAndProcessLines(self, onLine)
}
func (self *CmdObj) PromptOnCredentialRequest(task gocui.Task) ICmdObj {
self.credentialStrategy = PROMPT
self.task = task
return self
}
func (self *CmdObj) FailOnCredentialRequest() ICmdObj {
self.credentialStrategy = FAIL
return self
}
func (self *CmdObj) GetCredentialStrategy() CredentialStrategy {
return self.credentialStrategy
}
func (self *CmdObj) GetTask() gocui.Task {
return self.task
}
func (self *CmdObj) Clone() ICmdObj {
clone := &CmdObj{}
*clone = *self
clone.cmd = cloneCmd(self.cmd)
return clone
}
func cloneCmd(cmd *exec.Cmd) *exec.Cmd {
clone := &exec.Cmd{}
*clone = *cmd
return clone
}
|