Scripts are line-based, one command per line. Lines beginning with # are comments that delimit log sections. Blank lines are ignored. Scripts are typically embedded in txtar archives alongside their input files.
Each line is parsed as a sequence of space-separated words. A complete line has the form:
[!] [?] [cond]... command args... [&] [&name&]
| Element | Meaning |
|---|---|
! |
Expect the command to fail. The script stops if it succeeds. |
? |
Accept either success or failure; continue regardless. |
[cond] |
Run the rest of the line only if cond is true. |
[!cond] |
Run the rest of the line only if cond is false. |
& (trailing) |
Run the command in the background (anonymous). |
&name& (trailing) |
Run the command in the background with the given name. |
Multiple condition guards on one line are AND-ed -- the command runs only when all conditions are satisfied.
[linux] [amd64] exec ./mybin
! exec false
? exec might-not-exist
Words are split on spaces and tabs. Additional rules:
- Single quotes disable both splitting and variable expansion.
'hello world'is one argument. - Doubled single quote produces a literal quote:
'Don''t'becomesDon't. $VARand${VAR}expand environment variables. Undefined variables expand to the empty string.${/}expands to the OS path separator (/or\).${:}expands to the OS list separator (:or;).#at any unquoted position ends the line (comment).
env GREETING='hello world'
echo $GREETING
exec ./prog -path=src${/}main.go
echo 'it''s a test' # this part is a comment
A trailing & or &name& runs a command in the background. Only commands marked Async in their usage (currently exec) support this.
exec server &
exec worker &build&
wait-- wait for all background commands to finish.wait build-- wait only for the command started with&build&.
When a command is sent to the background, the stdout and stderr buffers are cleared immediately (they no longer correspond to the last foreground command). After wait returns, stdout and stderr contain the concatenated output of the collected background commands in start order.
exec slow-server &srv&
exec curl http://localhost:8080
stdout 'OK'
wait srv
exec program [args...] [&] [&name&]
Run an external program found via the script's $PATH. Does not terminate the script on its own (unlike a Unix shell). Can be run in the background.
exec go version
stdout 'go version'
wait [name]
Wait for background commands to complete. With no arguments, waits for all. With a name, waits for only the background command started with &name&.
exec server &srv&
exec client
wait srv
stop [msg]
Halt script execution immediately. The test passes (no error is reported). The optional message is written to the log.
[!exec:git] stop 'git not found, skipping'
exec git status
sleep duration
Pause execution for the given duration (a Go time.Duration string like 1s, 500ms, 2m). Can be run in the background.
sleep 100ms
skip [msg]
Skip the current test. Available only via the scripttest package. Equivalent to calling t.Skip().
[short] skip 'skipping in short mode'
exec long-running-test
echo string...
Write arguments to the stdout buffer, separated by spaces, followed by a newline.
echo hello world
stdout 'hello world'
cat files...
Read the named files and write their concatenated contents to the stdout buffer.
cat config.json
stdout '"debug": true'
env [key[=value]...]
With no arguments, print the entire script environment to the log. With key=value arguments, set variables. With bare key arguments, print key=value to stdout.
env HOME=/tmp/test
env HOME
stdout 'HOME=/tmp/test'
stdin file
Set standard input for the next exec command. The file contents become stdin, consumed after the next exec.
stdin input.json
exec jsonfmt
stdout [-count=N] [-q] 'pattern'
Assert that the stdout buffer from the last command matches the given Go regexp. With -count=N, require exactly N matches. With -q, suppress printing matched lines.
exec echo hello world
stdout 'hello'
stdout -count=1 'hello'
stderr [-count=N] [-q] 'pattern'
Assert that the stderr buffer from the last command matches the given Go regexp. Same flags as stdout.
! exec false-cmd
stderr 'error'
grep [-count=N] [-q] 'pattern' file
Assert that the contents of file match the given Go regexp. Does not modify stdout or stderr buffers.
grep 'package main' main.go
grep -count=2 'import' main.go
cmp [-q] file1 file2
Compare two files for exact byte equality. file1 can be the literal stdout or stderr to compare against the corresponding buffer. On mismatch, prints a unified diff unless -q is given.
exec generate-config
cmp stdout expected.json
cmpenv [-q] file1 file2
Like cmp, but expands environment variables in both files before comparing.
env USER=testuser
cmpenv stdout expected.txt
exists [-readonly] [-exec] file...
Check that the named files exist. With -readonly, also verify they are not writable. With -exec, verify they are executable.
exists go.mod go.sum
exists -exec ./bin/mytool
cd dir
Change the script's working directory.
mkdir subdir
cd subdir
exec pwd
mkdir path...
Create directories, including any necessary parents (like mkdir -p).
mkdir -p src/pkg/util
cp src... dst
Copy files. src can be the literal stdout or stderr to copy from the corresponding buffer. When copying multiple sources, dst must be a directory.
exec generate-config
cp stdout config.json
cp a.txt b.txt outdir/
mv old new
Rename a file or directory.
mv draft.txt final.txt
rm path...
Remove files or directories recursively.
rm tmp/
rm old.log
chmod perm paths...
Change the permissions of named files or directories. Only numerical permissions are supported.
chmod 0755 script.sh
exec script.sh
symlink path -> target
Create a symbolic link. The -> token is required between path and target.
symlink link.txt -> original.txt
replace [old new]... file
Replace strings in a file. The old and new arguments are unquoted as if in quoted Go strings (so \n becomes a newline, etc.).
replace '\\n' '\n' output.txt
unquote file...
Remove txtar quoting from files in place. Each line's leading > prefix is stripped. This is useful when a file embedded in a txtar archive needs to contain lines starting with -- that would otherwise be interpreted as archive markers.
unquote script.txt
help [-v] name...
Print documentation for commands and conditions. Enclose conditions in brackets: help [GOOS]. Pass -v for full detail.
help exec
help [exec]
help -v
update file
Write the current stdout buffer into the named file within the txtar archive. Available only via the scripttest package. This is a no-op unless the test is run with -testscript.update.
exec generate-output
update expected.txt
Conditions appear in square brackets before a command and control whether the line executes.
| Condition | True when |
|---|---|
[GOOS:linux] |
runtime.GOOS == "linux" |
[GOOS:darwin] |
runtime.GOOS == "darwin" |
[GOARCH:amd64] |
runtime.GOARCH == "amd64" |
[GOARCH:arm64] |
runtime.GOARCH == "arm64" |
[exec:prog] |
Executable prog is in the test binary's PATH (cached) |
[short] |
testing.Short() is true (scripttest only) |
[verbose] |
testing.Verbose() is true (scripttest only) |
[root] |
Running as root (os.Geteuid() == 0) |
Negation: prefix the condition name with !.
[!GOOS:windows] exec ./unix-only.sh
[exec:docker] exec docker ps
[!short] exec slow-integration-test
Multiple conditions on one line are AND-ed:
[GOOS:linux] [GOARCH:amd64] exec ./linux-amd64-binary
Register custom commands using the engine's AddCmd method:
import (
script "github.com/veggiemonk/testscript"
"github.com/veggiemonk/testscript/scripttest"
)
engine := scripttest.DefaultEngine()
engine.AddCmd("greet", script.Command(
script.CmdUsage{
Summary: "print a greeting",
Args: "name",
},
func(s *script.State, args ...string) (script.WaitFunc, error) {
if len(args) != 1 {
return nil, script.ErrUsage
}
return func(*script.State) (string, string, error) {
return "hello, " + args[0] + "\n", "", nil
}, nil
}))Then use it in scripts:
greet world
stdout 'hello, world'
Three constructors are available for registering conditions.
A static true/false condition. Does not accept a suffix.
engine.AddCond("ci", script.BoolCondition(
"running in CI",
os.Getenv("CI") == "true",
))[ci] env VERBOSE=1
Evaluated once per unique suffix, then cached. The function does not receive a *State because results are shared across all script states.
engine.AddCond("exec", script.CachedCondition(
"<suffix> names an executable in the test binary's PATH",
func(name string) (bool, error) {
_, err := exec.LookPath(name)
return err == nil, nil
}))[exec:curl] exec curl http://example.com
Evaluated each time it appears. Receives both the *State and the suffix, so it can vary based on script state.
engine.AddCond("env", script.PrefixCondition(
"environment variable <suffix> is set",
func(s *script.State, suffix string) (bool, error) {
_, ok := s.LookupEnv(suffix)
return ok, nil
}))[env:DEBUG] echo debug mode enabled
Comment lines starting with # delimit log sections. Each section is timed independently.
# Set up the environment
env HOME=/tmp/test
mkdir work
# Run the build
exec go build ./...
When the engine's Quiet mode is enabled, log output for successful sections is discarded -- only the section header and elapsed time are kept. If a command in a section fails, the full log (including every command and its output) is preserved for that section. This keeps test output concise while still providing full detail on failure.
- Test Scripts in Go -- John Arundel
- Testscript: A Hidden Testing Gem -- Encore
- Discussion: go-internal#297