# Plan 9, Acme, and Structural Regex Reference A practical reference for Plan 9 concepts, the acme editor, structural regular expressions, and how to apply these ideas on Linux. --- ## 1. Acme Interaction Model Acme is a mouse-centric editor built around three buttons and a uniform interaction model. Every window is text. Every command is text. The editor is a file server. ### Mouse Buttons | Button | Name | Action | |--------|------|--------| | 1 | Left | Select text (click to place, drag to select) | | 2 | Middle | Execute: run the selected/clicked text as a command | | 3 | Right | Plumb or search: opens file, jumps to line, or searches | Chording: hold button 1 then click 2 to cut, hold button 1 then click 3 to paste. ### The Tag Bar Each window has a tag bar at the top containing the filename and a set of built-in commands: `Del Snarf Get Put Look`. Clicking any of these with button 2 executes them. You can type additional commands into the tag bar. ### Middle-Click to Execute Any text in any window can be executed by middle-clicking. Acme looks up the text as a command in `$PATH`. Output appears in a new window or the scratch column. ``` middle-click "ls -la" -> runs ls -la, output in new window middle-click "go build" -> runs go build in the current directory middle-click "Edit ,d" -> deletes all text in current window ``` ### Right-Click to Plumb / Search Right-clicking text sends it to the plumber. The plumber inspects the text and decides what to do: open a file, jump to a line, load a URL, open a man page. If no plumber rule matches, acme searches for the text in the current window. ``` right-click "main.go:42" -> opens main.go, jumps to line 42 right-click "http://example.com" -> opens browser right-click "open" -> searches for "open" in current file right-click "fork(2)" -> opens man page for fork section 2 ``` --- ## 2. Key Commands These are typed into the tag bar or any window and executed with button 2. ### Navigation ``` :42 jump to line 42 in current window :/pattern/ jump to next match of pattern Edit = show current filename and line number ``` ### Window and Session Management ``` win open a new shell window inside acme Dump save the current acme session layout to a file Load restore a saved session layout New open a new empty window Del close current window ``` ### Display ``` Font /lib/font/bit/lucsans/unicode.8.font set font Font /usr/local/plan9/font/fixed/unicode.6x13.font ``` ### File Operations ``` Get reload file from disk (button 2 on "Get" in tag) Put write file to disk (button 2 on "Put" in tag) ``` --- ## 3. Keyboard Shortcuts Acme uses mostly standard Unix line-editing keys plus a few additions. | Shortcut | Action | |----------|--------| | Ctrl-U | Delete from cursor to start of line | | Ctrl-W | Delete previous word | | Ctrl-A | Move to start of line | | Ctrl-E | Move to end of line | | Ctrl-F | Fetch/complete filename | | Ctrl-H | Backspace | | Ctrl-I | Tab | | Ctrl-J | Newline | | Escape | Select text typed since last mouse click (press again to delete it) | Escape is the undo primitive in acme. Typing then pressing Escape selects what you typed. Pressing Escape again deletes it. There is no conventional undo stack. The workflow is: type text, verify it, keep or Escape-delete it. For larger operations, use Edit commands which are composable and reversible by design. --- ## 4. Acme Filesystem API (9P) Acme exposes itself as a 9P file server. Every window is a directory under the mount point. This is how shell scripts and programs interact with acme. ### Listing Windows ```sh 9 ls acme # list all acme window directories ls $NAMESPACE/acme # direct mount point access ``` Each window directory contains: ``` acme/ / addr current address (read/write) body window content (read/write) ctl control messages (write) data window data at addr (read/write) errors error output destination event mouse/keyboard events (read) tag tag bar content (read/write) xdata extended data ``` ### Reading and Writing ```sh # read window content cat acme/3/body # write to a window echo 'hello world' > acme/3/body # set address then read at that address echo '0,.' > acme/3/addr cat acme/3/data # append text to body echo ', a/new line/' | 9p write acme/3/ctl ``` ### Control Messages Write these strings to the `ctl` file: ``` clean mark file as unmodified dirty mark file as modified del delete window get reload from file put write to file name foo rename window show bring window to front ``` Example: ```sh echo 'name /tmp/scratch' > acme/$winid/ctl ``` ### The $winid Variable When acme runs a command, it sets `$winid` to the ID of the window that executed the command. Scripts use this to target the correct window. ```sh # append text to the window that ran this script echo 'done' >> acme/$winid/body # show current file and line cat acme/$winid/ctl | head -1 ``` ### Event Loop (acme scripting) Programs can read from `acme//event` to receive keyboard and mouse events. This is how tools like `acmego`, `acme-lsp`, and language servers attach to acme. Event format: ` ` ``` Mx 10 20 0 4 test # mouse execution of "test" from position 10 to 20 ``` --- ## 5. The Edit Command Language `Edit` is acme's structural editor, derived from sam. Type `Edit ` and execute it with button 2. ### Address Syntax An address specifies a region of text to operate on. | Address | Meaning | |---------|---------| | `0` | before first character | | `$` | after last character | | `.` | current selection | | `1` | line 1 | | `42` | line 42 | | `1,42` | lines 1 through 42 | | `,` | entire file (shorthand for `0,$`) | | `/pattern/` | next match of pattern after `.` | | `?pattern?` | previous match of pattern before `.` | | `+/pattern/` | next match after `.` (explicit forward) | | `-/pattern/` | previous match before `.` | | `/foo/,/bar/` | from next "foo" to next "bar" | | `2,/end/` | from line 2 to next "end" | ### Basic Commands ``` Edit s/old/new/ substitute first match in . Edit s/old/new/g substitute all matches in . Edit ,s/old/new/g substitute all matches in file Edit d delete . Edit ,d delete entire file Edit a/text/ append text after . Edit i/text/ insert text before . Edit c/text/ change . to text Edit p print . to acme output window Edit = print filename and line number of . ``` Using external commands: ``` Edit ,wc -l pipe file to wc -l (count lines, no change) Edit ,|sort replace file with sorted content Edit ,|sort -u replace file with sorted unique content Edit /TODO/+1,/DONE/-1|sort sort lines between TODO and DONE markers ``` ### Structural Regular Expressions The key insight: instead of matching characters, you match structure. `x` and `y` let you loop over matches and non-matches. #### x -- Extract and Apply `x/pattern/command` -- for each match of pattern in the address, set `.` to that match and run command. ``` Edit ,x/TODO.*\n/p print all TODO lines Edit ,x/TODO.*\n/d delete all TODO lines Edit ,x/^/a/> / prepend "> " to every line (quote a file) Edit ,x/\n/a/,/ replace newlines with commas Edit ,x/[A-Z][a-z]+/ select each capitalized word (. cycles through them) ``` #### y -- Non-Matching Regions `y/pattern/command` -- for each region between matches, set `.` to that region and run command. ``` Edit ,y/\t/s/ /\t/g in non-tab regions, replace spaces with tabs ``` #### g and v -- Conditionals `g/pattern/command` -- if `.` contains a match, run command. `v/pattern/command` -- if `.` does not contain a match, run command. ``` Edit ,x/.*\n/g/TODO/p print lines containing TODO Edit ,x/.*\n/v/TODO/p print lines not containing TODO Edit ,x/.*\n/g/^$/d delete blank lines Edit ,x/.*\n/v/^#/d delete lines that are not comments ``` #### Nesting and Composition Commands compose. The output address of one command feeds the next. ``` # Delete all blank lines in a function body Edit /^func/,/^}/x/.*\n/g/^$/d # In all struct definitions, sort the fields Edit ,x/^type.*struct \{/,/^\}/x/^\t[A-Z]/,/\n/|sort # Replace all occurrences of "foo" that appear on lines containing "bar" Edit ,x/.*\n/g/bar/s/foo/baz/g # Number only non-blank lines Edit ,x/^.+$/= # Uppercase all words in comments Edit ,x/(#|\/\/).*\n/x/[a-z]+/|tr a-z A-Z ``` #### Swapping Two Strings Use a temporary placeholder to avoid double-substitution: ``` Edit ,s/alpha/PLACEHOLDER/g Edit ,s/beta/alpha/g Edit ,s/PLACEHOLDER/beta/g ``` Or with braces (compound command -- all applied to same address): ``` Edit ,x/alpha|beta/{ s/alpha/PLACEHOLDER/ s/beta/alpha/ s/PLACEHOLDER/beta/ } ``` The brace form applies each sub-command to `.` sequentially within the same address context. ### Cross-File Commands: X and Y `X/pattern/command` -- for each open window whose filename matches pattern, run command. `Y/pattern/command` -- for each open window whose filename does not match, run command. ``` Edit X/\.go$/,s/fmt\.Print\b/fmt.Println/g replace in all open .go files Edit X/\.md$/,x/.*\n/g/TODO/p print TODO lines from all open markdown files Edit Y/\.go$/Del close all windows that are not .go files ``` --- ## 6. Plumber Rules The plumber is Plan 9's dispatcher. It reads rules from `$HOME/lib/plumbing` and decides what to open or do based on text you right-click. ### Default Rules (plan9port) ``` # compiler errors: file.go:42:5: message type is text data matches '([a-zA-Z0-9_\-./]+):([0-9]+).*' arg isfile $1 plumb to edit plumb client window acme data set $file attr add addr=$2 ``` ``` # URLs type is text data matches 'https?://[a-zA-Z0-9_\-./~:?#&=+%@,;]+' plumb to web plumb start chrome $0 ``` ``` # man pages: fork(2), open(2) type is text data matches '([a-zA-Z0-9_]+)\(([0-9])\)' plumb to man plumb start sh -c 'man '$2' '$1 ``` ``` # git SHA: 40-char hex type is text data matches '[0-9a-f]{40}' plumb to git plumb start sh -c 'git show '$0' | acme -' ``` ``` # image files type is text data matches '.*\.(png|jpg|jpeg|gif|bmp)' arg isfile $0 plumb to image plumb start display $0 ``` ### Custom Rule Example To open `.json` files in a formatter: ``` type is text data matches '.*\.json' arg isfile $0 plumb to edit plumb client window acme data set $0 plumb start window jq . $0 ``` ### Reloading Plumber Rules ```sh cat $HOME/lib/plumbing | 9p write plumb/rules ``` --- ## 7. Acme + Git Workflow These are conventional shell scripts used inside acme. Middle-click to execute. ### gl -- Git Log in Acme Window ```sh #!/usr/bin/env rc # gl: show git log, clickable SHAs git log --oneline | sed 's/^/ /' | acme - ``` ### glo -- Git Log with Details ```sh #!/usr/bin/env rc # glo: full log git log --format='%H %ad %s' --date=short | acme - ``` ### gv -- Git Show a Commit ```sh #!/usr/bin/env rc # gv: git show $1 git show $1 | acme - ``` ### gbl -- Git Blame Current File ```sh #!/usr/bin/env rc # gbl: blame current file git blame $% | acme - ``` ### gd -- Git Diff ```sh #!/usr/bin/env rc # gd: diff working tree git diff | acme - ``` ### Interactive Rebase with Acme as EDITOR ```sh EDITOR=E git rebase -i HEAD~5 ``` `E` is the plan9port editor wrapper that opens a file in the current acme instance and waits for it to close. This lets you edit the rebase todo list inside acme. ### Committing from Acme ```sh # create commit message file, edit in acme, then commit echo '' > /tmp/commitmsg E /tmp/commitmsg git commit -F /tmp/commitmsg ``` --- ## 8. sam -d for Batch Editing `sam` is the predecessor to acme. `sam -d` runs without any display, reading commands from stdin. This is ideal for scripted file editing. ### Basic Usage ```sh sam -d file.go <<'EOF' ,s/oldname/newname/g w EOF ``` ### Piped Workflow ```sh echo ',s/http:/https:/g w' | sam -d *.md ``` ### Structural Edits via sam -d ```sh # delete all blank lines from file.txt sam -d file.txt <<'EOF' ,x/^$/d w EOF # prepend shebang to all .sh files missing one for f in *.sh; do sam -d $f <<'EOF' 0g/^#!/ i/#!/usr/bin/env bash\n/ w EOF done ``` ### ssam -- Stream sam `ssam` is a wrapper that applies sam commands to stdin, outputting to stdout. Useful in pipelines. ```sh # remove trailing whitespace from all lines cat file.go | ssam ',x/[ \t]+$/d' # add semicolons to end of lines not already ending in one cat file.go | ssam ',x/[^;]\n/a/;/' ``` --- ## 9. rc Shell `rc` is the Plan 9 shell. It is simpler, more consistent, and more composable than sh or bash. ### Lists Are First-Class Variables hold lists natively. No word splitting, no glob rescan. ```rc files = (main.go util.go server.go) for (f in $files) echo $f # append to a list files = ($files new.go) ``` Compare with bash where `files="main.go util.go"` is just a string and requires careful quoting. ### No Rescan In bash, `$var` undergoes glob expansion after assignment. In rc, it does not. ```rc pattern = '*.go' echo $pattern # prints *.go literally, not expanded ls $pattern # expands here, where you want it ``` ### Simpler Quoting Single quotes quote literally. Double quotes do not exist as a quoting mechanism. `''` inside single quotes produces a literal single quote. ```rc echo 'it''s fine' # prints: it's fine echo 'no $expansion' # prints: no $expansion ``` ### String Concatenation with ^ The `^` operator concatenates strings and lists. ```rc base = /usr/local echo $base^/bin # /usr/local/bin prefix = foo names = (bar baz) echo $prefix^$names # foobar foobaz (list expansion) ``` ### File Descriptor Manipulation ```rc cmd >[2=1] # redirect stderr to stdout (equivalent of 2>&1) cmd >[2]/dev/null # discard stderr cmd <[0]/dev/null # read stdin from /dev/null ``` ### Function Definitions ```rc fn greet { echo hello $1 } greet world # prints: hello world ``` ### Environment and Path ```rc path = (/usr/local/bin /usr/bin /bin) # path is a list home = /home/kris ``` ### Comparison with sh | Feature | sh/bash | rc | |---------|---------|-----| | Lists | Strings with splitting | Native first-class | | Glob rescan | Yes (dangerous) | No | | Quoting | Complex | Simple | | Concatenation | Juxtaposition | `^` operator | | stderr redirect | `2>&1` | `>[2=1]` | | Functions | `foo() { }` | `fn foo { }` | --- ## 10. Rob Pike's 5 Rules of Programming These rules appear in Pike's notes and talks. They are about systems, not just code. 1. **You can't tell where a program is going to spend its time.** Bottlenecks occur in surprising places. Don't guess. Measure. 2. **Measure. Don't tune for speed until you've measured, and even then don't unless one part of the code overwhelms the rest.** Premature optimization is wasted effort on the wrong problem. 3. **Fancy algorithms are slow when n is small, and n is usually small.** Simple algorithms win in practice. Fancy algorithms have big constants. 4. **Fancy algorithms are buggier than simple ones, and they're much harder to implement.** Use simple algorithms and simple data structures. A hash table or a linked list is almost always enough. 5. **Data dominates. If you've chosen the right data structures and organized things well, the algorithms will almost always be self-evident.** Program in terms of your data structures. Code follows data. Rule 5 is the deepest. When the data structure is right, the program writes itself. --- ## 11. cat -v Considered Harmful This is the title of a 1983 paper by Rob Pike and Brian Kernighan. The principle applies far beyond `cat`. ### The Argument `cat -v` makes non-printing characters visible by converting them to printable representations. It seems helpful. It is harmful because: - It destroys data: the output of `cat -v` is not the same bytes as the input - Programs that need visible control characters should handle them themselves - Using `cat -v` in a pipeline corrupts the data for downstream programs - It teaches a wrong model: that control characters are inherently problems to be escaped ### The Broader Principle Tools that silently transform data violate composability. Unix pipes work because tools pass data through unchanged. When a tool modifies data "for display", it breaks the pipeline. Applied more broadly: do not add smarts to transport layers. The file should contain what the file contains. The display layer can render it however it wants. Do not conflate storage and display. ### Practical Implications - Do not use `cat -A`, `cat -e`, `cat -t` in scripts. Use `xxd` or `od` for binary inspection. - Do not use tools that silently mangle line endings, encodings, or whitespace. - Prefer tools that are transparent about their transforms. - When writing tools, separate the concerns: read data, process data, present data. Do not mix presentation into processing. --- ## 12. Factotum Credential Management Philosophy Factotum is the Plan 9 credential manager and authentication agent. It embodies a specific philosophy about how credentials should work. ### The Model Factotum is a file server (`/mnt/factotum`). Programs authenticate by reading from and writing to factotum's files. The kernel mediates all access. No program ever sees a raw password; they see tokens that factotum provides. ### Core Principles **Credentials belong to the agent, not the process.** A program does not hold a password. It asks factotum "can I authenticate as kris to this host?" and factotum says yes or no, or asks the user. **Authentication is a protocol.** Programs speak a challenge-response protocol with factotum. This works for passwords, keys, tickets, and anything else that can be plugged in. **Single point of trust.** You type your password once to factotum at login. Everything else delegates to it. No password ever appears in environment variables, files, or process arguments. **Delegation, not copying.** A process can be granted the ability to authenticate on your behalf without being given the credential itself. Factotum can grant limited, scoped authority. ### Linux Analogy: ssh-agent `ssh-agent` follows the same philosophy for SSH keys. You add your key once (`ssh-add`), and then all SSH connections negotiate through the agent. The private key never leaves the agent. The difference is that ssh-agent only does SSH. Factotum is general: it handles any protocol you plug in. ### Applying This Philosophy on Linux - Use `ssh-agent` and `ssh-add`. Never put private keys in environment variables. - Use `pass` or `gopass` for passwords. They never appear in shell history. - Use `gpg-agent` for GPG operations. Keys live in the agent. - Avoid tools that require `--password` flags on the command line (they appear in `ps` output). - Prefer tools that read credentials from sockets or files with mode 600. --- ## 13. Plan 9 Ideas on Linux Plan 9 is not widely deployed, but its ideas are available on Linux through plan9port and disciplined practice. ### plan9port plan9port is a port of Plan 9 userland tools to Unix. ```sh # install on Arch Linux paru -S plan9port # or from source git clone https://github.com/9fans/plan9port cd plan9port && ./INSTALL ``` Key tools it provides: `acme`, `sam`, `9p`, `plumber`, `rc`, `mk`, `9`, `factotum` (partial). ### Running Acme on Linux ```sh # set PLAN9 environment variable export PLAN9=/usr/local/plan9 export PATH=$PLAN9/bin:$PATH # start acme acme ``` ### sam -d on Linux Available immediately after installing plan9port. Excellent for scripted edits. ```sh # edit multiple files with structural regex for f in src/*.go; do sam -d $f <<'EOF' ,x/.*\n/g/FIXME/s/FIXME/TODO/g w EOF done ``` ### ssam on Linux Also available via plan9port. Use it for stream processing: ```sh git diff --stat | ssam ',x/[0-9]+/p' | sort -n ``` ### The Plumber on Linux The plumber requires a running plumb daemon. ```sh # start plumber plumber # reload rules cat $HOME/lib/plumbing | 9p write plumb/rules ``` Acme sends right-click events to the plumber automatically when both are running. ### Structural Thinking Without Plan 9 Tools Even without acme, you can apply structural regex thinking: **With ripgrep:** ```sh # find all lines with TODO in .go files, then process rg 'TODO' --files-with-matches | xargs sam -d <<'EOF' ,x/.*\n/g/TODO/s/TODO(\([^)]+\))?/FIXME/g w EOF ``` **With awk:** ```sh # awk can do g/v logic: print blocks between markers awk '/BEGIN/,/END/' file.txt awk '!/^#/ && NF > 0' file.txt # non-comment, non-blank lines ``` **With perl -0777:** ```sh # perl with -0777 slurps whole file: enables cross-line regex perl -0777 -i -pe 's/foo.*?bar/REPLACED/gs' file.txt ``` **sed with ranges:** ```sh sed -n '/^func /,/^}/p' main.go # print function bodies sed '/^func /,/^}/s/TODO/FIXME/g' main.go ``` ### mk vs make `mk` is the Plan 9 build tool. It is simpler than make: no implicit rules, no magic variables, consistent behavior. plan9port ships `mk`. Using it on Linux: ```sh # Mkfile (note: capital M) all:V: go build ./... test:V: go test ./... clean:V: rm -f bin/* ``` The `:V:` suffix marks a target as a virtual target (like `.PHONY` in make). ### Namespace Manipulation Plan 9 has per-process namespaces. On Linux, containers and `unshare` provide a rough equivalent. ```sh # run a command in a new mount namespace unshare --mount sh # Linux namespaces are Plan 9 namespaces, implemented later and less cleanly ``` ### The Discipline The Plan 9 approach applied to Linux+dwm: - Keep tools small and composable. Prefer pipelines over monoliths. - Write programs that read stdin and write stdout. Make them scriptable. - Use text as the universal interface. Avoid binary formats for configuration. - Prefer convention over configuration. Agree on file locations and formats. - Name things well. A good name is documentation. - When something is hard to explain, it probably has the wrong interface. - The editor is a tool, not a religion. But use one that can be scripted. - Prefer mk over make, rc over bash, sam over sed for structural edits. - Measure before optimizing. Data structures before algorithms. --- ## Quick Reference Card ### Edit Commands Cheat Sheet ``` ,p print whole file ,d delete whole file ,s/a/b/g replace a with b globally ,x/pat/cmd for each match of pat, run cmd ,x/.*\n/g/pat/p print lines matching pat ,x/.*\n/v/pat/d delete lines not matching pat ,|cmd pipe whole file through cmd ,cmd pipe file to cmd (no replace) /pat/+1 line after next match of pat /pat/-1 line before next match of pat X/\.go$/cmd run cmd in all open .go windows ``` ### sam -d Cheat Sheet ```sh sam -d file <<'EOF' ,s/old/new/g w q EOF echo ',s/old/new/g w' | sam -d file ``` ### rc Cheat Sheet ```rc files = (a b c) # list $#files # count: 3 $files(2) # second element: b fn f { echo $* } # function cmd >[2=1] # stderr to stdout path = (/usr/bin /bin) # set path ``` ### Plumber Rule Structure ``` type is text data matches 'regex' arg isfile $0 # optional: verify it's a file plumb to destination plumb start command $0 ```