Creating System Command Calls

Command

cmd := exec.Command("/bin/bash", "-c", "command")

Method prototype

func Command(name string, arg ...string) *Cmd {}

CommandContext

ctx, cancelFun := context.WithTimeout(context.Background, 5 * time.Second)
cmd := exec.CommandContext(ctx, "/bin/bash", "-c", "command")

Method prototype

func CommandContext(ctx context.Context, name string, arg ...string) *Cmd {}

With this approach, you can use the context to forcibly terminate a running command.

You might find it strange why the name parameter is /bin/bash instead of directly entering the command. In fact, after we enter a command in the terminal and press Enter, it needs to be parsed by the Shell. In most runtime environments today, this is bash. In other words, when you enter ls -la in the terminal, what the system actually executes is /bin/bash -c “ls -la”.

Execution Methods

Run

The Run method is synchronous, meaning that when the code executes the Run method, it will block and wait for the command to complete and return.

command := exec.Command("ls", "-la")
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
command.Stdout = stdout
command.Stderr = stderr
if err := command.Run(); err != nil {
  log.Println(err)
}

log.Println(stdout.String(), stderr.String())

Start

The Start method is asynchronous and returns immediately. You can call the Wait method to “wait” for the command to complete. The Run method is an example:

func (c *Cmd) Run() error {
  if err := c.Start(); err != nil {
    return err
  }
  return c.Wait()
}

Output

This method only returns the content of stdout, which is the standard output after command execution.

stdout, err := cmd.Output()

CombinedOutput

This method combines stdout and stderr together and returns them.

output, err := cmd.CombinedOutput()

Using Pipes to Connect Multiple Commands

ps := exec.Command("/bin/bash", "-c",  "ps -ef")
grep := exec.Command("/bin/bash", "-c", "grep -i ssh")
// Create pipe
r, w := io.Pipe()
defer r.Close()
defer w.Close()
ps.Stdout = w // ps writes to one end of the pipe
grep.Stdin = r // grep reads from the other end of the pipe
var buffer bytes.Buffer
grep.Stdout = &buffer // grep's output goes to buffer

_ = ps.Start()
_ = grep.Start()
ps.Wait()
w.Close()
grep.Wait()
io.Copy(os.Stdout, &buffer) // copy buffer to system standard output

From the code above, we can see that the principle is to input one command’s stdout into another command’s stdin.

Running Commands as a Specific User

In business scenarios, we may need to run a command as a specific user. Here’s how to do it:

import (
	"os/user"
	"strconv"
	"syscall"
)

cmd := exec.CommandContext(ctx, "/bin/bash", "-c", "command")
cmd.Env = env
cmd.Dir = dir
cmd.SysProcAttr = &syscall.SysProcAttr{
  Setpgid: true,
}

sysuser, err := user.Lookup(username) // Get user information by username
if err != nil {
  return err
}
uid, err := strconv.Atoi(sysuser.Uid) // Convert UID type to uint32
gid, err := strconv.Atoi(sysuser.Gid) // Convert GID type to uint32

// Set Credential
cmd.SysProcAttr.Credential = &syscall.Credential{
  Uid:         uint32(uid),
  Gid:         uint32(gid),
  Groups:      nil,
  NoSetGroups: false,
}

If the user exists, the command will run as the specified user.

Forcibly Terminating a Command

cmd := exec.Command("/bin/bash", "-c", "sleep 5")
if err := cmd.Process.Kill(); err != nil {
  return err
}

If you’re outside a goroutine, it’s recommended to use the CommandContext method to create commands, so you can control the command’s lifecycle through the context.

I hope this is helpful, Happy hacking…