Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ For something a bit more challenging, let's try counting the number of lines in
numErrors, err := script.File("test.txt").Match("Error").CountLines()
```

You can find a complete, runnable version of this in [examples/count_errors.go](examples/count_errors.go), along with other examples in the [`examples/`](examples) directory.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my books I use a listing link convention something like this:

Suggested change
You can find a complete, runnable version of this in [examples/count_errors.go](examples/count_errors.go), along with other examples in the [`examples/`](examples) directory.
(Listing [`count_errors`](examples/count_errors.go))

We can use that style for all the other linked examples to come, and perhaps this first time mention the examples directory:

Suggested change
You can find a complete, runnable version of this in [examples/count_errors.go](examples/count_errors.go), along with other examples in the [`examples/`](examples) directory.
You can find a complete, runnable version of this and other examples in the [`examples/`](examples) directory.

What do you think?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the (Listing name) convention — I'll use the directory-mention version for this first link and that style for the rest, once we settle the layout.


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`)?

```go
Expand Down
6 changes: 6 additions & 0 deletions examples/app.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
2026-06-19 12:00:00 [INFO] Starting application...
2026-06-19 12:01:05 [WARN] Low memory warning
2026-06-19 12:02:10 [ERROR] Database connection failed
2026-06-19 12:03:15 [INFO] Retrying database connection...
2026-06-19 12:04:20 [ERROR] Retries exhausted. Could not connect to database.
2026-06-19 12:05:00 [INFO] Shutting down application.
26 changes: 26 additions & 0 deletions examples/count_errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//go:build ignore

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we actually need this. I mean, it's absolutely correct in the sense that this file isn't part of the script package. On the other hand, one thing about example programs is that people will copy and paste them exactly as is—that's what they're for, after all. Including this line would stop their program working, which might be very puzzling for beginners.

Does it do us any harm to omit the build tag in examples?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question — I tried it, and removing the build tag does break things: with both count_errors.go and filter_csv.go declaring func main() in the same examples/ directory, go build ./... and go vet ./... fail with "main redeclared". go run file.go handles one file fine, but a directory-wide build compiles them as one package, so the tag was working around exactly that.

To keep them copy-paste-able without the tag, my instinct is to give each example its own subdirectory (examples/count_errors/main.go, examples/filter_csv/main.go) — scales cleanly and go build ./... stays happy. But it's your call on the layout. Which would you prefer?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we're working against the grain of Go a little bit here, aren't we? It's actually quite awkward to include a copypastable program in a Go module that doesn't cause a main conflict.

Maybe what we should do instead is use Go's built-in example mechanism. For example (sorry), we could add this to script_test.go:

func Example_count_lines() {
	count, err := script.File("examples/app.log").Match("ERROR").CountLines()
	if err != nil {
		panic(err)
	}
	fmt.Printf("Number of ERROR lines: %d\n", count)
}

func Example_filter_csv() {
	// Column splits on whitespace, so turn the comma into a space first,
	// then take the first field (the server name).
	_, err := script.File("examples/servers.csv").Match("DOWN").Replace(",", " ").Column(1).Stdout()
	if err != nil {
		panic(err)
	}
}

As you probably know, because the function names start with Example, they'll be built as part of the autogenerated documentation, and because they don't include the names of any other identifier, they'll be treated as package-level examples:

Screenshot 2026-06-29 at 13 25 11

We can put the data files in the testdata folder—I don't think the public pkgsite instance will let you read these, but that's okay. People can copy and paste the code and it'll work.


// Counts the lines containing "ERROR" in a log file.
//
// Run it from the repository root:
//
// go run examples/count_errors.go
//
// Equivalent shell command:
//
// grep ERROR examples/app.log | wc -l
package main

import (
"fmt"

"github.com/bitfield/script"
)

func main() {
count, err := script.File("examples/app.log").Match("ERROR").CountLines()
if err != nil {
panic(err)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, panicking on errors makes sense for an example because we don't want to clutter up the program with error handling paperwork. But, again, I've learned over the years that whatever you show people is exactly what they'll do, word for word. It's no good saying “Obviously for real programs you wouldn't use panic here,” because that's not obvious to beginners, and they are precisely the people we're writing for.

Instead, even when it's not directly relevant to the principle I'm demonstrating, I've got into the habit of writing the code that shows what I think is best practice:

Suggested change
panic(err)
fmt.Fprintln(os.Stderr, err)
os.Exit(1)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do — switching to fmt.Fprintln(os.Stderr, err) + os.Exit(1) when I apply the restructure.

}
fmt.Printf("Number of ERROR lines: %d\n", count)
}
25 changes: 25 additions & 0 deletions examples/filter_csv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//go:build ignore

// Prints the names of servers whose status is "DOWN", read from a CSV file.
//
// Run it from the repository root:
//
// go run examples/filter_csv.go
//
// Equivalent shell command:
//
// grep DOWN examples/servers.csv | cut -d, -f1
package main

import (
"github.com/bitfield/script"
)

func main() {
// Column splits on whitespace, so turn the comma into a space first,
// then take the first field (the server name).
_, err := script.File("examples/servers.csv").Match("DOWN").Replace(",", " ").Column(1).Stdout()
if err != nil {
panic(err)
}
}
5 changes: 5 additions & 0 deletions examples/servers.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
server1,UP
server2,DOWN
server3,DOWN
server4,UP
server5,DOWN