What is script?
script is a Go library for doing the kind of tasks that shell scripts are good at: reading files, executing subprocesses, counting lines, matching strings, and so on.
Why shouldn't it be as easy to write system administration programs in Go as it is in a typical shell? script aims to make it just that easy.
Shell scripts often compose a sequence of operations on a stream of data (a pipeline). This is how script works, too.
What can I do with it?
Let's see a simple example. Suppose you want to read the contents of a file as a string:
contents, err := script.File("test.txt").String()
That looks straightforward enough, but suppose you now want to count the lines in that file.
numLines, err := script.File("test.txt").CountLines()
For something a bit more challenging, let's try counting the number of lines in the file which match the string "Error":
numErrors, err := script.File("test.txt").Match("Error").CountLines()
But what if, instead of reading a specific file, we want to simply pipe input into this program, and have it output only matching lines (like grep)?
script.Stdin().Match("Error").Stdout()
That was almost too easy! So let's pass in a list of files on the command line, and have our program read them all in sequence and output the matching lines:
script.Args().Concat().Match("Error").Stdout()
Maybe we're only interested in the first 10 matches. No problem:
script.Args().Concat().Match("Error").First(10).Stdout()
What's that? You want to append that output to a file instead of printing it to the terminal? You've got some attitude, mister.
script.Args().Concat().Match("Error").First(10).AppendFile("/var/log/errors.txt")
How does it work?
Those chained function calls look a bit weird. What's going on there?
One of the neat things about the Unix shell, and its many imitators, is the way you can compose operations into a pipeline:
cat test.txt | grep Error | wc -l
The output from each stage of the pipeline feeds into the next, and you can think of each stage as a filter which passes on only certain parts of its input to its output.
By comparison, writing shell-like scripts in raw Go is much less convenient, because everything you do returns a different data type, and you must (or at least should) check errors following every operation.
In scripts for system administration we often want to compose different operations like this in a quick and convenient way. If an error occurs somewhere along the pipeline, we would like to check this just once at the end, rather than after every operation.