Golang application executes Shell command practical tutorial

In this article, learn how to execute Shell commands (such as ls, mkdir, or grep) in a Golang program, how to pass I/O to running commands through stdin and stdout, and manage long-running commands. For a better understanding, several examples are provided for different scenarios from shallow to deep, and I hope you can understand it easily.

exec package

Use the official os/exec package to execute external commands. When you execute shell commands, you need to run code outside the Go application, so these commands need to be run in subprocesses. As shown below:

Each command runs as a subprocess in the Go application and exposes stdin and stdout attributes, which we can use to read and write process data.

Run basic Shell commands

Run a simple command and read data from its output by creating an instance of *exec.Cmd. In the following example, ls is used to list the files in the current directory and its output is printed from the code:

// create a new *Cmd instance
// here we pass the command as the first argument and the arguments to pass to the command as the
// remaining arguments in the function
cmd := exec.Command("ls", "./")

// The `Output` method executes the command and
// collects the output, returning its value
out, err := cmd.Output()
if err != nil {
  // if there was any error, print it here
  fmt.Println("could not run command: ", err)
// otherwise, print the output from running the command
fmt.Println("Output: ", string(out))

Because the program is run in the current directory, the files in the root directory of the project are output:

> go run shellcommands/main.go

Output:  LICENSE

When running exec, the program does not spawn a shell, but runs the given command directly, which means that no shell-based processing, such as glob patterns or expansion, is done. For example, when running the ls ./*.md command, it does not output readme.md as if we run the command in that shell.

Execute long-running commands

The preceding example executes the ls command and returns the result immediately, but what happens when the command output is continuous or takes a long time to execute? For example, running the ping command will periodically obtain continuous results:

ping www.baidu.com 
PING www.a.shifen.com ( 56(84) bytes of data.
64 bytes from ( icmp_seq=1 ttl=128 time=11.1 ms
64 bytes from ( icmp_seq=2 ttl=128 time=58.8 ms
64 bytes from ( icmp_seq=3 ttl=128 time=28.2 ms
64 bytes from ( icmp_seq=4 ttl=128 time=11.1 ms
64 bytes from ( icmp_seq=5 ttl=128 time=11.5 ms
64 bytes from ( icmp_seq=6 ttl=128 time=53.6 ms
64 bytes from ( icmp_seq=7 ttl=128 time=10.2 ms
64 bytes from ( icmp_seq=8 ttl=128 time=10.4 ms
64 bytes from ( icmp_seq=9 ttl=128 time=15.8 ms
64 bytes from ( icmp_seq=10 ttl=128 time=16.5 ms
64 bytes from ( icmp_seq=11 ttl=128 time=10.9 ms
^C64 bytes from icmp_seq=12 ttl=128 time=9.92 ms

If you try to execute this kind of command using cmd.Output, you will not get any results, because the Output method waits for the end of command execution, while ping executes indefinitely. Therefore, you need to customize the Stdout property to read continuous output:

cmd := exec.Command("ping", "google.com")

// pipe the commands output to the applications
// standard output
cmd.Stdout = os.Stdout

// Run still runs the command and waits for completion
// but the output is instantly piped to Stdout
if err := cmd.Run(); err != nil {
  fmt.Println("could not run command: ", err)

Run the program again, and the output is similar to that executed in the Shell.

By directly assigning the Stdout property, we can capture output throughout the command lifecycle and process it as soon as it is received. The io interaction between processes is shown in the following figure:

custom write output

Instead of using os.Stdout, custom write output can also be created by implementing the io.Writer interface.

The following custom code adds a "received output: " prefix to each output block:

type customOutput struct{}

func (c customOutput) Write(p []byte) (int, error) {
	fmt.Println("received output: ", string(p))
	return len(p), nil

Now assign a custom write output instance to the command output:

cmd.Stdout = customOutput{}

Run the program again and you will get the following output.

Use Stdin to pass input to commands

The preceding example does not give any input to the command (or provides limited input as a parameter), and in most scenarios the input information is passed through the Stdin stream. A typical example is the grep command, which can be piped from one command string to another:

➜  ~ echo "1. pear\n2. grapes\n3. apple\n4. banana\n" | grep apple
3. apple

Here the output of echo is passed to grep as stdin, a group of fruits is input, and only apple is output through grep filtering.

The *Cmd instance provides an input stream for writing, and the following example uses it to pass input to the grep subprocess:

cmd := exec.Command("grep", "apple")

// Create a new pipe, which gives us a reader/writer pair
reader, writer := io.Pipe()
// assign the reader to Stdin for the command
cmd.Stdin = reader
// the output is printed to the console
cmd.Stdout = os.Stdout

go func() {
  defer writer.Close()
  // the writer is connected to the reader via the pipe
  // so all data written here is passed on to the commands
  // standard input
  writer.Write([]byte("1. pear\n"))
  writer.Write([]byte("2. grapes\n"))
  writer.Write([]byte("3. apple\n"))
  writer.Write([]byte("4. banana\n"))

if err := cmd.Run(); err != nil {
  fmt.Println("could not run command: ", err)

Output result:

3. apple

end child process

There are some commands that run indefinitely and need to be able to signal to end. For example, if you run a web service with python3 -m http.server or sleep 10000, the child process will run for a long time or run indefinitely.

To stop a process, a kill signal needs to be sent from the application, which can be achieved by adding a context instance to the command. If the context is cancelled, the command will also terminate execution:

ctx := context.Background()
// The context now times out after 1 second
// alternately, we can call `cancel()` to terminate immediately
ctx, _ = context.WithTimeout(ctx, 1*time.Second)

// sleep 10 second 
cmd := exec.CommandContext(ctx, "sleep", "10")

out, err := cmd.Output()
if err != nil {
  fmt.Println("could not run command: ", err)
fmt.Println("Output: ", string(out))

Run the program and output the result after 1 second:

could not run command:  signal: killed

When the command needs to be run within a limited time or the command does not return a result within a certain period of time, the backup logic is executed.


So far, we have learned various ways to execute and interact with unix shell commands. Here are some things to be aware of when using the os/exec package:

  • Use cmd.Output when you wish to execute simple commands that usually don't provide much output
  • For functions with continuous or long output you should use cmd.Run and interact with it via cmd.Stdout and cmd.Stdin
  • In a production scenario, if the process does not respond within a given time, it must have a timeout and end function, and you can use the cancel context to send a termination command

Tags: Go Linux shell

Posted by D_tunisia on Fri, 27 Jan 2023 03:58:05 +1030