261101.4 · 5735ff673a95384397d923bbcec92a411a6328d1
main.go splits by purpose
main.go held argument parsing, endpoint and token resolution and opening a browser, beside the program's start. They move to args.go, endpoint.go and browser.go, and main_test.go's tests go with the code they cover. main.go keeps main, the reporter, detaching and error codes.
Declarations moved unchanged, as a sorted comparison of every line outside the import blocks confirms. git blame -C -C traces them.
reading declarations
Changes to args.go. All files
@@ -0,0 +1,292 @@+package main++import (+ "net/netip"+ "strings"+ "syscall"+)++type config struct {+ dir string+ addr string+ sharing bool+ share string // the address --share names; "" takes the default route's+ secret bool+ token string // the secret path's token, from WWW_SECRET or drawn at random+ ep netip.AddrPort+ fg bool+ noopen bool+ persist bool // write a persistent lock, so the tree is served after reboot+ debug bool // show the build stamp and the index's times+ noBug bool // hide the bug button and refuse reports+ cache string // the memory for cached git answers, eg 32M; "" takes defaultCache+ disk string // the disk for online repos, eg 4G; "" takes defaultDisk+ public bool // serve only online repos, to the open web+ help bool+ version bool+ restart bool+ force bool // --restart takes servers of other binaries too+ list bool+ kill bool+ commit string // the commit --commit looks up+}++// usageText is written in docopt notation, except that --share takes its+// HOST optionally, as GNU optional arguments do, which docopt cannot say.+// TestUsageIsDocopt checks it with docopt-ng. A pattern goes on over+// indented lines.+var usageText = strings.NewReplacer("@NAME@", progName, "@ENV@", envPrefix).Replace(`Usage:+ @NAME@ [-p] [--foreground] [--no-open] [-a ADDR] [--debug] [--no-bug]+ [--cache=SIZE] [--disk=SIZE] [--] [DIR]+ @NAME@ [-p] [--foreground] [--no-open] --share[=HOST] [--secret] [--debug]+ [--no-bug] [--cache=SIZE] [--disk=SIZE] [--] [DIR]+ @NAME@ --restart [-f] [--no-open] [--debug] [--no-bug]+ [-a ADDR | --share[=HOST]] [--] [DIR]+ @NAME@ -l [-a ADDR | --share[=HOST]] [--] [DIR]+ @NAME@ --kill [-a ADDR | --share[=HOST]]+ @NAME@ --commit=REV [--] [DIR]+ @NAME@ --public --disk=SIZE (-a ADDR | --share[=HOST]) [--debug]+ [--cache=SIZE]+ @NAME@ -h | --help+ @NAME@ -V | --version++Options:+ -a ADDR, --addr=ADDR serve on this loopback address, IP or IP:PORT; the+ port defaults to 80 when @NAME@ may bind it, else+ 8080, and the IP to 127.31.N.1, N = uid - 1000+ --share[=HOST] share the tree on the address of the default route,+ or on HOST, an address or name beyond loopback with+ an optional :PORT, eg 192.168.1.5, 0.0.0.0 or+ mate.peer.home; anyone who reaches it reads every+ file in the tree that other users could read+ --secret serve a shared tree under the token in @ENV@SECRET,+ or a random one, so only those holding the link+ reach it+ --foreground when no server runs on the address yet, serve in the+ foreground until Ctrl-C+ --no-open print the URL without opening a browser+ -p, --persist write a .LOCK instead of a .lock: the tree is served+ again after a reboot or a stopped server, by the+ next server on its address or by --restart+ --debug show what serves debugging: above each page's+ header, small and faint, the binary's build time,+ the seconds since, its commit and the page's render+ time; on the index, each server's build and start+ and each lock's time; with --restart, on the new+ servers+ --no-bug hide the bug button at the foot of each page, which+ files a numbered bug in this program, not in the+ tree, with the page and the server's state; a share+ never shows it; with --restart, on the new servers+ --cache=SIZE memory a new server may spend on cached git answers,+ eg 64M; 0 caches nothing [default: 32M]+ --disk=SIZE disk a new server may fill with online repos, eg 4G;+ it drops the repos read longest ago [default: 2G]+ --restart replace every server this binary started with one+ running it now, serving the same trees, and start+ one for its persistent trees that have none; with an+ address from -a or --share, only the server there,+ whichever binary runs it; with DIR, serve it too and+ open it unless a page of it was open+ -f, --force with --restart, take the servers and persistent+ trees of other binaries too, eg the package's+ -l, --list print each served tree's directory, URL and the+ binary that serves it as keyval; with DIR, only the+ trees at or below it+ --kill stop every server and print the trees it served,+ as with --list; with an address from -a or --share,+ only the server there+ --commit=REV print as keyval the commit REV names in DIR's repo:+ its full hash, its stardate and its subject; REV is+ a stardate, eg 261024.4, a hash or any name git+ takes+ --public serve only repos fetched from a forge, never a tree+ here, in the foreground, to anyone who reaches the+ address, eg through a proxy on -a; it lists no+ cache, shows no stop or bug button and opens nothing+ -V, --version print when the binary was built and its commit+ -h, --help print this help+`)++// switches maps each long boolean option to its field; shorts names the+// ones with a short form.+func (c *config) switches() map[string]*bool {+ return map[string]*bool{+ "foreground": &c.fg,+ "no-open": &c.noopen,+ "persist": &c.persist,+ "debug": &c.debug,+ "no-bug": &c.noBug,+ "help": &c.help,+ "version": &c.version,+ "restart": &c.restart,+ "force": &c.force,+ "list": &c.list,+ "kill": &c.kill,+ "public": &c.public,+ }+}++// -f and -n are reserved for --force and --dry-run in every tool of the+// user's, so --foreground and --no-open have no short form.+var shorts = map[byte]string{'p': "persist", 'h': "help", 'V': "version", 'l': "list", 'f': "force"}++// parseArgs reads options the GNU way. Short ones combine, as in -pl, and+// take a value joined or apart: -aIP or -a IP. Long ones take --addr IP or+// --addr=IP. Options may follow the directory, and "--" ends them.+func parseArgs(args []string) (config, error) {+ var c config+ sw := c.switches()+ var dirs []string+ value := func(i *int, opt, joined string) (string, error) {+ if joined != "" {+ return joined, nil+ }+ if *i+1 == len(args) {+ return "", &errno{syscall.EINVAL, "option " + opt + " needs a value"}+ }+ *i+++ return args[*i], nil+ }+ for i := 0; i < len(args); i++ {+ a := args[i]+ switch {+ case a == "--":+ dirs = append(dirs, args[i+1:]...)+ i = len(args)+ case strings.HasPrefix(a, "--"):+ name, val, hasVal := strings.Cut(a[2:], "=")+ if name == "addr" {+ if hasVal && val == "" {+ return c, &errno{syscall.EINVAL, "option --addr needs a value"}+ }+ v, err := value(&i, "--addr", val)+ if err != nil {+ return c, err+ }+ c.addr = v+ continue+ }+ // --share, like --secret, takes its address only joined; alone+ // it takes the default route's.+ if name == "share" {+ if hasVal && val == "" {+ return c, &errno{syscall.EINVAL, "option --share= needs an address"}+ }+ c.sharing, c.share = true, val+ continue+ }+ // --secret takes no token: one on the command line shows in+ // ps to every local user and stays in shell history.+ if name == "secret" {+ if hasVal {+ return c, &errno{syscall.EINVAL, "--secret takes no token, since ps shows it; set " + secretEnv}+ }+ c.secret = true+ continue+ }+ if name == "commit" {+ v, err := value(&i, "--commit", val)+ if err != nil {+ return c, err+ }+ if v == "" {+ return c, &errno{syscall.EINVAL, "option --commit needs a value"}+ }+ c.commit = v+ continue+ }+ if name == "cache" || name == "disk" {+ v, err := value(&i, "--"+name, val)+ if err != nil {+ return c, err+ }+ if _, err := parseSize(v); err != nil {+ return c, err+ }+ if name == "cache" {+ c.cache = v+ } else {+ c.disk = v+ }+ continue+ }+ p, ok := sw[name]+ switch {+ case !ok:+ return c, &errno{syscall.EINVAL, "unknown option --" + name}+ case hasVal:+ return c, &errno{syscall.EINVAL, "option --" + name + " takes no value"}+ }+ *p = true+ case len(a) > 1 && a[0] == '-':+ for j := 1; j < len(a); j++ {+ if a[j] == 'a' {+ v, err := value(&i, "-a", a[j+1:])+ if err != nil {+ return c, err+ }+ c.addr = v+ break+ }+ name, ok := shorts[a[j]]+ if !ok {+ return c, &errno{syscall.EINVAL, "unknown option -" + string(a[j])}+ }+ *sw[name] = true+ }+ default:+ dirs = append(dirs, a)+ }+ }+ if c.commit != "" && (c.restart || c.list || c.kill || c.fg || c.noopen || c.persist || c.debug || c.noBug ||+ c.sharing || c.secret || c.addr != "" || c.cache != "" || c.disk != "" || c.public) {+ return c, &errno{syscall.EINVAL, "--commit takes a directory and no other option"}+ }+ // A public server runs under a service manager, so it stays in the+ // foreground, opens nothing and files no bugs; it serves no tree.+ if c.public && (c.restart || c.list || c.kill || c.fg || c.noopen || c.persist || c.noBug || c.secret || len(dirs) > 0) {+ return c, &errno{syscall.EINVAL, "--public takes an address, --disk, --cache and --debug, and no directory"}+ }+ if c.public && c.disk == "" {+ return c, &errno{syscall.EINVAL, "--public needs --disk, since any visitor can make it clone"}+ }+ // The default address is the user's own, where a reader's server runs.+ if c.public && c.addr == "" && !c.sharing {+ return c, &errno{syscall.EINVAL, "--public needs -a or --share"}+ }+ if c.force && !c.restart {+ return c, &errno{syscall.EINVAL, "--force goes with --restart"}+ }+ if c.restart && c.fg {+ return c, &errno{syscall.EINVAL, "--restart takes no --foreground"}+ }+ if c.persist && (c.restart || c.list || c.kill) {+ return c, &errno{syscall.EINVAL, "--persist goes with a start, not with --restart, --list or --kill"}+ }+ if c.list && (c.restart || c.fg) {+ return c, &errno{syscall.EINVAL, "--list takes no --restart or --foreground"}+ }+ if c.kill && (len(dirs) > 0 || c.fg || c.restart || c.list) {+ return c, &errno{syscall.EINVAL, "--kill takes no directory, --foreground, --restart or --list"}+ }+ if c.addr != "" && c.sharing {+ return c, &errno{syscall.EINVAL, "-a and --share name one address each; give one"}+ }+ if c.secret && (!c.sharing || c.restart || c.list || c.kill) {+ return c, &errno{syscall.EINVAL, "--secret goes with --share, and not with --restart, --list or --kill"}+ }+ switch len(dirs) {+ case 0:+ // --list without a directory lists every tree, and --restart+ // without one serves no tree beyond those it restarts.+ if !c.list && !c.kill && !c.restart && !c.public {+ c.dir = "."+ }+ case 1:+ c.dir = dirs[0]+ default:+ return c, &errno{syscall.EINVAL, "one directory at most, got " + strings.Join(dirs, " ")}+ }+ return c, nil+}