github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/utils/exec/exec.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: 2021-Present The Jackal Authors 3 4 // Package exec provides a wrapper around the os/exec package 5 package exec 6 7 import ( 8 "bytes" 9 "context" 10 "errors" 11 "fmt" 12 "io" 13 "os" 14 "os/exec" 15 "runtime" 16 "strings" 17 "sync" 18 ) 19 20 // Config is a struct for configuring the Cmd function. 21 type Config struct { 22 Print bool 23 Dir string 24 Env []string 25 CommandPrinter func(format string, a ...any) 26 Stdout io.Writer 27 Stderr io.Writer 28 } 29 30 // Shell represents the desired shell to use for a given command 31 type Shell struct { 32 Windows string `json:"windows,omitempty" jsonschema:"description=(default 'powershell') Indicates a preference for the shell to use on Windows systems (note that choosing 'cmd' will turn off migrations like touch -> New-Item),example=powershell,example=cmd,example=pwsh,example=sh,example=bash,example=gsh"` 33 Linux string `json:"linux,omitempty" jsonschema:"description=(default 'sh') Indicates a preference for the shell to use on Linux systems,example=sh,example=bash,example=fish,example=zsh,example=pwsh"` 34 Darwin string `json:"darwin,omitempty" jsonschema:"description=(default 'sh') Indicates a preference for the shell to use on macOS systems,example=sh,example=bash,example=fish,example=zsh,example=pwsh"` 35 } 36 37 // PrintCfg is a helper function for returning a Config struct with Print set to true. 38 func PrintCfg() Config { 39 return Config{Print: true} 40 } 41 42 // Cmd executes a given command with given config. 43 func Cmd(command string, args ...string) (string, string, error) { 44 return CmdWithContext(context.TODO(), Config{}, command, args...) 45 } 46 47 // CmdWithPrint executes a given command with given config and prints the command. 48 func CmdWithPrint(command string, args ...string) error { 49 _, _, err := CmdWithContext(context.TODO(), PrintCfg(), command, args...) 50 return err 51 } 52 53 // CmdWithContext executes a given command with given config. 54 func CmdWithContext(ctx context.Context, config Config, command string, args ...string) (string, string, error) { 55 if command == "" { 56 return "", "", errors.New("command is required") 57 } 58 59 // Set up the command. 60 cmd := exec.CommandContext(ctx, command, args...) 61 cmd.Dir = config.Dir 62 cmd.Env = append(os.Environ(), config.Env...) 63 64 // Capture the command outputs. 65 cmdStdout, _ := cmd.StdoutPipe() 66 cmdStderr, _ := cmd.StderrPipe() 67 68 var ( 69 stdoutBuf, stderrBuf bytes.Buffer 70 errStdout, errStderr error 71 wg sync.WaitGroup 72 ) 73 74 stdoutWriters := []io.Writer{ 75 &stdoutBuf, 76 } 77 78 stdErrWriters := []io.Writer{ 79 &stderrBuf, 80 } 81 82 // Add the writers if requested. 83 if config.Stdout != nil { 84 stdoutWriters = append(stdoutWriters, config.Stdout) 85 } 86 87 if config.Stderr != nil { 88 stdErrWriters = append(stdErrWriters, config.Stderr) 89 } 90 91 // Print to stdout if requested. 92 if config.Print { 93 stdoutWriters = append(stdoutWriters, os.Stdout) 94 stdErrWriters = append(stdErrWriters, os.Stderr) 95 } 96 97 // Bind all the writers. 98 stdout := io.MultiWriter(stdoutWriters...) 99 stderr := io.MultiWriter(stdErrWriters...) 100 101 // If we're printing, print the command. 102 if config.Print && config.CommandPrinter != nil { 103 config.CommandPrinter("%s %s", command, strings.Join(args, " ")) 104 } 105 106 // Start the command. 107 if err := cmd.Start(); err != nil { 108 return "", "", err 109 } 110 111 // Add to waitgroup for each goroutine. 112 wg.Add(2) 113 114 // Run a goroutine to capture the command's stdout live. 115 go func() { 116 _, errStdout = io.Copy(stdout, cmdStdout) 117 wg.Done() 118 }() 119 120 // Run a goroutine to capture the command's stderr live. 121 go func() { 122 _, errStderr = io.Copy(stderr, cmdStderr) 123 wg.Done() 124 }() 125 126 // Wait for the goroutines to finish (if any). 127 wg.Wait() 128 129 // Abort if there was an error capturing the command's outputs. 130 if errStdout != nil { 131 return "", "", fmt.Errorf("failed to capture the stdout command output: %w", errStdout) 132 } 133 if errStderr != nil { 134 return "", "", fmt.Errorf("failed to capture the stderr command output: %w", errStderr) 135 } 136 137 // Wait for the command to finish and return the buffered outputs, regardless of whether we printed them. 138 return stdoutBuf.String(), stderrBuf.String(), cmd.Wait() 139 } 140 141 // LaunchURL opens a URL in the default browser. 142 func LaunchURL(url string) error { 143 switch runtime.GOOS { 144 case "linux": 145 return exec.Command("xdg-open", url).Start() 146 case "windows": 147 return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() 148 case "darwin": 149 return exec.Command("open", url).Start() 150 } 151 152 return nil 153 } 154 155 // GetOSShell returns the shell and shellArgs based on the current OS 156 func GetOSShell(shellPref Shell) (string, []string) { 157 var shell string 158 var shellArgs []string 159 powershellShellArgs := []string{"-Command", "$ErrorActionPreference = 'Stop';"} 160 shShellArgs := []string{"-e", "-c"} 161 162 switch runtime.GOOS { 163 case "windows": 164 shell = "powershell" 165 if shellPref.Windows != "" { 166 shell = shellPref.Windows 167 } 168 169 shellArgs = powershellShellArgs 170 if shell == "cmd" { 171 // Change shellArgs to /c if cmd is chosen 172 shellArgs = []string{"/c"} 173 } else if !IsPowershell(shell) { 174 // Change shellArgs to -c if a real shell is chosen 175 shellArgs = shShellArgs 176 } 177 case "darwin": 178 shell = "sh" 179 if shellPref.Darwin != "" { 180 shell = shellPref.Darwin 181 } 182 183 shellArgs = shShellArgs 184 if IsPowershell(shell) { 185 // Change shellArgs to -Command if pwsh is chosen 186 shellArgs = powershellShellArgs 187 } 188 case "linux": 189 shell = "sh" 190 if shellPref.Linux != "" { 191 shell = shellPref.Linux 192 } 193 194 shellArgs = shShellArgs 195 if IsPowershell(shell) { 196 // Change shellArgs to -Command if pwsh is chosen 197 shellArgs = powershellShellArgs 198 } 199 default: 200 shell = "sh" 201 shellArgs = shShellArgs 202 } 203 204 return shell, shellArgs 205 } 206 207 // IsPowershell returns whether a shell name is powershell 208 func IsPowershell(shellName string) bool { 209 return shellName == "powershell" || shellName == "pwsh" 210 }