pkg/prog: Expand godoc.

This commit is contained in:
Qi Xiao 2024-06-09 17:02:09 +01:00
parent 44bc8d04a6
commit 0cd215e638

View File

@ -1,10 +1,62 @@
// Package prog provides the entry point to Elvish. Its subpackages correspond /*
// to subprograms of Elvish. Package prog supports building testable, composable programs.
package prog
// This package sets up the basic environment and calls the appropriate The main abstraction of this package is the [Program] interface, which can be
// "subprogram", one of the daemon, the terminal interface, or the web combined using [Composite] and run with [Run].
// interface.
# Testability
The easy way to write a Go program is as follows:
- Write code in the main function of the main package.
- Access process-level context via globals defined in the [os] package,
like [os.Args], [os.Stdin], [os.Stdout] and [os.Stderr].
- Call [os.Exit] if the program should exit with a non-zero status.
- Declare flags as global variables, with functions like [flag.String].
Programs written this way are hard to test, since they rely on process-level
states that are hard or impossible to emulate in tests.
With this package, the way to write a Go program is becomes a matter of
creating a type that implements the [Program] interface:
- Write the "main" function in the Run method.
- Context is available as arguments.
- Return an error constructed from [Exit] to exit with a non-zero status.
- Declare flags as fields of the type, and register them in the
RegisterFlags method.
The [Program] can be run using the [Run] function, which takes care of
parsing flags, calling the Run method, and converting the error return value
into an exit code. Since the [Run] function itself takes the standard files
and command-line arguments as its function arguments, these can be emulated
easily in tests.
# Composability
Another advantage of this approach is composability. Elvish contains multiple
independent subprograms. The default subprogram is the shell; but if you run
Elvish with "elvish -buildinfo" or "elvish -daemon", they will invoke the
buildinfo or daemon subprogram instead of the shell.
Subprograms can also do something in addition to, rather than in place of,
other subprograms. One example is profiling support, which declares its own
flags like -cpuprofile and runs some extra code before and after other
subprograms to start and stop profiling.
Using this package, all the different subprograms can be implemented
separately and then composed into one using [Composite].
Other than keeping the codebase cleaner, this also enables an easy way to
provide alternative main packages that include or exclude a certain
subprogram. For example, profiling support requires importing a lot of
additional packages from the standard library, which increases the binary
size. As a result, the profiling subprogram is not included in the default
main package [src.elv.sh/cmd/elvish], but it is included in the alternative
main package [src.elv.sh/cmd/withpprof/elvish]. Binaries built from the
former main package is meaningfully smaller than the latter.
*/
package prog
import ( import (
"flag" "flag"
@ -34,8 +86,15 @@ func usage(out io.Writer, fs *flag.FlagSet) {
fs.PrintDefaults() fs.PrintDefaults()
} }
// Run parses command-line flags and runs the first applicable subprogram. It // Run parses command-line flags and runs the [Program], returning the exit
// returns the exit status of the program. // status.
//
// It is supposed to be used from main functions like this:
//
// func main() {
// program := ...
// os.Exit(prog.Run([3]*os.File{os.Stdin, os.Stdout, os.Stderr}, os.Args, program))
// }
func Run(fds [3]*os.File, args []string, p Program) int { func Run(fds [3]*os.File, args []string, p Program) int {
fs := flag.NewFlagSet("elvish", flag.ContinueOnError) fs := flag.NewFlagSet("elvish", flag.ContinueOnError)
// Error and usage will be printed explicitly. // Error and usage will be printed explicitly.
@ -97,8 +156,9 @@ func Run(fds [3]*os.File, args []string, p Program) int {
return 2 return 2
} }
// Composite returns a Program that tries each of the given programs, // Composite returns a [Program] that tries each of the given subprograms,
// terminating at the first one that doesn't return NotSuitable(). // terminating after running the first one that doesn't return an error created
// with [NextProgram].
func Composite(programs ...Program) Program { func Composite(programs ...Program) Program {
return composite(programs) return composite(programs)
} }
@ -128,10 +188,10 @@ func (cp composite) Run(fds [3]*os.File, args []string) error {
return NextProgram(cleanups...) return NextProgram(cleanups...)
} }
// NextProgram returns a special error that may be returned by [Program.Run] // NextProgram returns a special error that may be returned by the Run method of
// that is part of a [Composite] program, indicating that the next program // a [Program] that is part of a [Composite] program, indicating that the next
// should be tried. It can carry a list of cleanup functions that should be run // program should be tried. It can carry a list of cleanup functions that should
// in reverse order before the [Composite] program finishes. // be run in reverse order before the composite program finishes.
func NextProgram(cleanups ...func([3]*os.File)) error { return nextProgramError{cleanups} } func NextProgram(cleanups ...func([3]*os.File)) error { return nextProgramError{cleanups} }
type nextProgramError struct{ cleanups []func([3]*os.File) } type nextProgramError struct{ cleanups []func([3]*os.File) }
@ -142,18 +202,18 @@ func (e nextProgramError) Error() string {
return "internal error: no suitable subprogram" return "internal error: no suitable subprogram"
} }
// BadUsage returns a special error that may be returned by Program.Run. It // BadUsage returns a special error that may be returned by a [Program]'s Run
// causes the main function to print out a message, the usage information and // method. It causes the main function to print out a message, the usage
// exit with 2. // information and exit with 2.
func BadUsage(msg string) error { return badUsageError{msg} } func BadUsage(msg string) error { return badUsageError{msg} }
type badUsageError struct{ msg string } type badUsageError struct{ msg string }
func (e badUsageError) Error() string { return e.msg } func (e badUsageError) Error() string { return e.msg }
// Exit returns a special error that may be returned by Program.Run. It causes // Exit returns a special error that may be returned by a [Program]'s Run
// the main function to exit with the given code without printing any error // method. It causes the main function to exit with the given code without
// messages. Exit(0) returns nil. // printing any error messages. Exit(0) returns nil.
func Exit(exit int) error { func Exit(exit int) error {
if exit == 0 { if exit == 0 {
return nil return nil