Golang System Command Execution

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…