// Package cli is the command-line boundary for plumb: it parses flags, drives// the loader, resolves the destination, runs the pure core, and writes output.// All diagnostics flow through the streams passed to Run so the behavior is// testable.
package cliimport ()// Sentinel errors for destination resolution, so its failures are classified in// tests with errors.Is rather than by matching message text.var (errNoPackages = errors.New("no packages scanned")errAmbiguousDestination = errors.New("ambiguous destination")errSyntheticImportPath = errors.New("no real import path")errInvalidImportPath = errors.New("invalid import path")errPackageNameRequired = errors.New("package name required")errInvalidPackageName = errors.New("invalid package name")errPackageNameMismatch = errors.New("package name does not match destination"))// syntheticImportPath is the placeholder import path the go/packages loader// assigns to a package it synthesizes from file arguments (e.g. `plumb a.go`)// rather than resolving from an import-path pattern. It is syntactically a valid// path, so it passes module.CheckImportPath; plumb must reject it separately// because it is not a real, importable destination to qualify against.constsyntheticImportPath = "command-line-arguments"constusage = `plumb is a demand-based compile-time dependency injection generator.Usage: plumb [-package-name=<name>] [-import-path=<path>] [-output=<file>] [-v] [packages...]plumb scans the matched packages for //plumb:<name> directives and writes onegenerated function per set. With no package pattern it scans the currentdirectory.`// Run executes plumb with the given arguments and streams, returning the process// exit code. It never calls os.Exit itself. Output is split across three writers:// stdout carries the generated source (when -output is unset), stderr carries// diagnostics and the tolerated-error notes, and report carries the -v// discovery-and-inference report. That report is the deterministic,// plumb-controlled stream, kept separate from the variable diagnostics so it can// be consumed on its own.// The command wires report to the same destination as stderr.func ( []string, , , io.Writer) int { := flag.NewFlagSet("plumb", flag.ContinueOnError) .SetOutput() .Usage = func() { _, _ = fmt.Fprint(, usage) } := .String("package-name", "", "package name for the generated `package` clause") := .String("import-path", "", "import `path` of the destination package") := .String("output", "", "output `file` (default: standard output)") := .Bool("v", false, "print a discovery-and-inference report to standard error")if := .Parse(); != nil {iferrors.Is(, flag.ErrHelp) {return0// explicitly requested help is a success, not a usage error }return2 } , := gopackages.Load(.Args(), "")if != nil {fail(, .Error())return1 } , , := resolveDestination(.Packages, *, *)if != nil {fail(, .Error())return1 } := gen.Options{ImportPath: ,PackageName: ,OutputBase: baseName(*), } , := gen.Generate(, .Packages)if != nil {fail(, .Error())// Surface any type errors tolerated at load time, so a genuine provider // type problem (not just a stale generated file) is not swallowed when // generation then fails for a related reason.noteToleratedErrors(, .TypeErrors)return1 }if * {// On success the load errors are tolerated, but under -v surface them so a // not-fully-type-correct input (a malformed body, a type used only // internally) is visible rather than silently swallowed.noteToleratedErrors(, .TypeErrors) _, _ = fmt.Fprint(, .Report) }if := writeOutput(*, .Source, ); != nil {fail(, .Error())return1 }return0}// resolveDestination determines the destination import path and emitted package// name from the flags and scanned packages, inferring each when its flag is// omitted and rejecting the ambiguous or contradictory combinations.func ( []*discover.Package, , string) (, string, error) { = if == "" {switchlen() {case1: = [0].PkgPathcase0:return"", "", fmt.Errorf("%w: cannot infer -import-path", errNoPackages)default:return"", "", fmt.Errorf("%w: more than one package scanned; -import-path is required", errAmbiguousDestination) } }if == syntheticImportPath {// The synthetic path can arrive two ways: inferred from a file-argument load // (the user gave no -import-path), or passed verbatim. Say which, so the // advice fits: "pass -import-path" is wrong when they already did.if == "" {return"", "", fmt.Errorf("%w: could not infer one from the arguments; pass -import-path", errSyntheticImportPath) }return"", "", fmt.Errorf("%w: %q is not a real, importable package", errSyntheticImportPath, syntheticImportPath) }if := module.CheckImportPath(); != nil {return"", "", fmt.Errorf("%w: %q: %v", errInvalidImportPath, , ) }// Find the destination among scanned packages (same-package generation).var *discover.Packagefor , := range {if .PkgPath == { = break } } = if == "" {if == nil {return"", "", fmt.Errorf("%w: -package-name is required when generating into a package that is not scanned (%q)", errPackageNameRequired, ) } = .Name }if !token.IsIdentifier() || == "_" {return"", "", fmt.Errorf("%w: %q must be a valid Go identifier", errInvalidPackageName, ) }// Same-package mode: the destination's real name is known, and the generated // file must share it. Honoring a conflicting flag would emit an uncompilable // package clause, so reject the mismatch rather than emit broken code. (Checked // after validity so a malformed value is still reported as invalid.)if != "" && != nil && != .Name {return"", "", fmt.Errorf("%w: -package-name %q contradicts the scanned destination package %q; omit it in same-package mode", errPackageNameMismatch, , .Name) }return , , nil}func (, string, io.Writer) error {if == "" { , := io.WriteString(, )return }// Write atomically so an interrupted run cannot leave a truncated file where // the previous good output was. plumb does not create intermediate directories; // writefile.Write preserves that (its temp-file create fails on a missing parent).returnwritefile.Write(, )}func ( string) string {if == "" {return"" }returnfilepath.Base()}func ( io.Writer, string) { _, _ = fmt.Fprintf(, "plumb: %s\n", )}// noteHeader is the fixed line noteToleratedErrors prints above the tolerated// go/types errors. It is deterministic (unlike the error bodies below it), so// tests key on it to confirm the note surfaced.constnoteHeader = "plumb: note: tolerated load errors:"// noteToleratedErrors prints the load-time type errors plumb tolerated, under a// single stderr note header with one indented line per error. They are surfaced// on failure (a tolerated error may be the real cause) and under -v on success// (so a not-fully-type-correct load is visible).//// The errors print in the order the loader encountered them, which is not stable// across package load orders. That is deliberate: unlike the generated source// and the -v report (which plumb guarantees byte-identical across read orders),// these notes are a diagnostic aid, and showing the errors in the order they// actually occurred is more useful than imposing an artificial, stable sort.func ( io.Writer, []error) {iflen() == 0 {return } _, _ = fmt.Fprintln(, noteHeader)for , := range { _, _ = fmt.Fprintf(, " %s\n", ) }}
The pages are generated with Goldsv0.8.4. (GOOS=linux GOARCH=amd64)