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  }