@ 261101.7

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

args.go

new file mode 100644@@ -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+}