# bitfield/script: shell-style pipelines for Go programs

> bitfield/script turns shell pipeline idioms into a Go library, so file reads, line counts, subprocesses and HTTP fetches compose with method chaining. It suits Go developers who keep rewriting the same grep-and-count plumbing.

**bitfield/script** — Making it easy to write shell-like scripts in Go

- Repository: https://github.com/bitfield/script
- Website: https://bitfieldconsulting.com/subscribe
- Stars: 7,041 · Forks: 369
- Language: Go
- License: MIT
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/bitfield-script

## The problem bitfield/script targets

Shell scripts are good at one thing: composing a sequence of small operations over a stream of data. Read a file, keep the lines matching a pattern, count them, print the first ten. In Go, the same job means opening a file, wrapping it in a bufio.Scanner, tracking an error on every line, and remembering to close the handle. The README states the project's aim plainly: it 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."

The audience is the Go developer who already writes those tools. If you maintain internal CLIs that grep logs, concatenate argument files, or pipe HTTP responses into a JSON query, this library is aimed at you. It is not aimed at people who want to replace bash. It gives you the pipeline shape inside a compiled Go program, where you keep static types, error values and a single binary to ship.

## How pipes, stages and errors fit together

The core abstraction is a pipe. Every operation returns a pipe, and you chain methods on it. The README's own progression starts with `script.File("test.txt").String()` and builds to `script.Args().Concat().Match("Error").First(10).Stdout()`. Sources include `File`, `Stdin`, `Args`, `Echo` and `Get`. Intermediate stages include `Match`, `FilterLine`, `Concat`, `First`, `Tee`, `JQ` and `ExecForEach`. Sinks such as `String`, `CountLines`, `Stdout` and `AppendFile` end the chain and hand you a result.

The error model is the part worth understanding before you adopt it. The README says that when any pipe stage encounters an error, it produces no output to subsequent stages, and that the pipe "remembers" the error for later retrieval through the `Error` method or a sink like `String`, which returns an error alongside the result. HTTP is folded into the same rule: response status codes outside 200-299 are treated as errors. That means a failed fetch yields an empty pipeline rather than a panic, and you only learn why when you check the error. The README is explicit that checking it is the Go convention: `_, err := script.Do(req).Stdout()` followed by a `log.Fatal`.

Extensibility is deliberately small. `Filter` takes a reader and a writer, so an operation the library does not ship can be written inline. The README's example copies the reader into the writer and appends a byte count. That is the escape hatch. If your transformation does not fit the reader-writer shape, the library is not hiding a second mechanism for you.

## Installing bitfield/script and writing a first pipeline

There are no release notes in the repository listing, so pin whatever version `go get` resolves and read the package documentation for that version. The import path is the module path, and the README opens with it.

```go
import "github.com/bitfield/script"
```

Add it to a module the usual way. The module declares `go 1.25.0` in its `go.mod`, so a toolchain at least that new is what the project builds against.

```bash
go mod init example.com/logscan
go get github.com/bitfield/script
```

A first real use is the grep-and-count task. The program below reads the files named on the command line, concatenates them, keeps lines matching `Error`, and counts them. Save it as `main.go` and run `go run . testdata/*` against your own files.

```go
package main

import (
	"fmt"
	"log"

	"github.com/bitfield/script"
)

func main() {
	n, err := script.Args().Concat().Match("Error").CountLines()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(n)
}
```

You should see a single integer on stdout. If you pass no arguments, `script.Args()` has nothing to read and the count will be zero rather than an error, which is the pipeline behaving as a shell would. To see the matching lines instead of the count, swap `CountLines()` for `Stdout()` and check its error, since `Stdout` returns both an error and a byte count that the README says you usually do not care about.

## Where the pipeline model gets in your way

The library is a pipeline, not a shell. There are no variables, no conditionals, no loops, and no quoting rules, because those are Go's job now. If your script is mostly branching on exit codes and parsing flags, you are writing Go with extra steps and the pipeline buys you little.

Error propagation is the sharper edge. Because a failing stage produces no output and stores the error for later, a chain can silently produce an empty result if you forget to check. The README's own 404 example makes this concrete: the program simply prints nothing. That is a reasonable design, but it means every chain needs a sink that returns an error and a caller that inspects it. Code that ignores the returned error will look like it works until the day a file is missing or a server returns an error status.

Subprocess handling has its own shape. `ExecCommand` runs the command concurrently and does not wait for it to finish before returning output, which the README illustrates with `ping` running indefinitely and lines appearing as they are produced. That is what you want for streaming, and it is not what you want if you assumed the call blocks until the process exits. The README does not document rollback, cancellation semantics, or what happens to a long-running child when the parent exits, so treat process lifecycle as something you verify yourself. On Windows the repository carries a separate `script_windows_test.go`, a hint that some behaviour is platform-specific, though the README does not spell out which operations differ.

## bitfield/script against os/exec and plain Go

The obvious alternative is the standard library: `os.ReadFile`, `bufio.Scanner`, `os/exec` and `net/http`, wired together by hand. The difference is not capability, since the library is built on those same primitives. It is composition. With `os/exec` you construct a `Cmd`, attach `StdoutPipe`, start it, scan the pipe, wait, and check `cmd.Wait()`. With bitfield/script that is `script.ExecCommand("ping", "127.0.0.1").Stdout()`. The standard library keeps every step visible and gives you full control over process groups, contexts and exit codes; the library trades that control for a chain you can read at a glance.

A second alternative is embedding a real shell through `mvdan.cc/sh/v3`, which appears in the module's requirements. That route runs actual shell syntax, with its quoting, expansions and builtins, at the cost of shipping an interpreter and accepting shell semantics. bitfield/script sits between the two: more structure than a shell string, less ceremony than hand-written Go. The README quotes Simon Willison calling the API design "one absolutely superb" piece of work, which tells you the appeal is the surface, not a feature the standard library lacks.

## Maintenance, licensing and what to check before you depend on it

The repository is not archived and the last push was on 2026-09-06, so it is being touched recently enough that you are not adopting an abandoned tree. That is a statement about commit activity, not a promise of support. There are no retrieved releases, so there is no changelog in this material to read for breaking changes; you are depending on the `master` branch's state at whatever commit you pin.

The licence is MIT, which is permissive and imposes few obligations beyond preserving the copyright notice and licence text. That is a summary of the identifier, not legal advice; check the `LICENSE` file in the repository for the terms that actually bind you. The dependency list is short: `gojq` for the `JQ` stage, `mvdan.cc/sh/v3`, and a few indirect modules. That is a modest supply chain for a library that shells out and makes HTTP requests, but note that `gojq` is what powers `JQ`, so its behaviour is your behaviour when a query is wrong.

Upgrade cost is the usual Go story. Because the API is method chaining on pipes, a signature change ripples through every chain in your codebase. Pin a version in `go.mod`, run `go doc github.com/bitfield/script` after upgrading, and diff the exported surface before you move a production tool onto it.

## Conclusion

Adopt bitfield/script if you write Go tools that mostly read files, filter lines, run subprocesses or fetch URLs, and you want the pipeline to read like the shell command you already had in mind. Skip it if you need a full shell interpreter, complex control flow, or you are not already a Go shop. Before committing, verify the pipe behaviour you depend on: whether the stage you use treats non-2xx HTTP responses as errors, how ExecCommand streams output, and where the README stops documenting error handling. Run go doc github.com/bitfield/script against the exact version you pin, since the release history is not listed and the API is what you will live with.

## FAQ

### What is bitfield/script in Go?

It is a Go library for doing the tasks shell scripts are good at, such as reading files, executing subprocesses, counting lines and matching strings. It composes those operations as a pipeline of chained methods, mirroring how a shell composes a sequence of operations on a stream of data.

### How do I install bitfield/script?

Add it to a Go module with go get github.com/bitfield/script and import it as github.com/bitfield/script. The module's go.mod declares go 1.25.0, so the toolchain needs to be at least that new.

### How does bitfield/script handle errors in a pipeline?

When any pipe stage encounters an error it produces no output to later stages, and the pipe remembers the error for retrieval through the Error method or a sink such as String. HTTP status codes outside 200-299 count as errors, so a failed request yields no output until you check the returned error.

### Does bitfield/script wait for an external command to finish?

No. According to the README, ExecCommand runs the command concurrently and does not wait for it to complete before returning output, so reading from the pipe with Stdout shows each line as it is produced.

## Sources

- [bitfield/script on GitHub](https://github.com/bitfield/script)
- [Issues](https://github.com/bitfield/script/issues)
- [License: MIT](https://github.com/bitfield/script/blob/master/LICENSE)
- [Project website](https://bitfieldconsulting.com/subscribe)
- [README](https://github.com/bitfield/script/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/bitfield-script
