Repository navigation
Expand file tree
/
Copy pathmain.go
More file actions
136 lines (120 loc) · 4.65 KB
/
Copy pathmain.go
File metadata and controls
136 lines (120 loc) · 4.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// Command embed-dotenvctl runs the whole dotenvctl verb tree inside another Go
// program: dotenvcmd.Execute in-process, the exit code as a return value, the
// host's own name in the lines the CLI prints about itself, and the verbs
// mounted as a subcommand of a host cobra tree.
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/spf13/cobra"
"github.com/ubgo/textdata/dotenv/cli/dotenvcmd"
)
// hostProgram is the host binary's name, and hostEnvVerb the subcommand it
// mounts the dotenv verbs under; users type "myapp env get KEY".
const (
hostProgram = "myapp"
hostEnvVerb = "env"
)
// envFileName is the file the demo creates in its temp directory.
const envFileName = ".env"
// missingFile is a relative name that does not exist: dotenvctl reads a
// missing file as empty, so a get on it fails with a message that does not
// carry a temp path, which keeps the output stable.
const missingFile = "missing.env"
// sampleEnv holds one plain and one derived value, so --expand has work to do.
const sampleEnv = "HOST=localhost\nURL=http://${HOST}:8080\n"
func main() {
dir, err := os.MkdirTemp("", "embed-dotenvctl-")
if err == nil {
err = run(os.Stdout, dir)
// Cleanup is best-effort: the run's result matters more than a
// leftover temp directory.
_ = os.RemoveAll(dir)
}
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// run executes every step of the guide against a .env file in dir.
// The caller owns dir (main makes a temp one, the test passes t.TempDir()),
// so nothing is written outside it.
func run(w io.Writer, dir string) error {
envFile := filepath.Join(dir, envFileName)
// 0600 because env files hold credentials.
if err := os.WriteFile(envFile, []byte(sampleEnv), 0o600); err != nil {
return err
}
fmt.Fprintln(w, "exit:", runVerb(w, "-f", envFile, "get", "URL", "--expand"))
fmt.Fprintln(w, "exit:", runVerb(w, "-f", missingFile, "get", "PORT"))
fmt.Fprintln(w, "exit:", runAsHost(w, "nosuchverb"))
fmt.Fprintln(w, "exit:", runHostTree(w, hostEnvVerb, "-f", envFile, "set", "PORT=8080", "--json"))
fmt.Fprintln(w, "exit:", runHostTree(w, hostEnvVerb, "-f", missingFile, "get", "PORT"))
return nil
}
// runVerb runs one dotenvctl command line in-process. args are what a user
// types after "dotenvctl"; both streams go to w so the demo shows failures
// too. Execute returns the exit code (0 ok, 1 failed, 2 usage) and never
// calls os.Exit, so the host decides what a failure means.
//
//ds:def id=runverb-hyve6qvu
func runVerb(w io.Writer, args ...string) int {
return dotenvcmd.Execute(args, w, w)
}
// runAsHost is runVerb for a host that mounts the verbs under its own words.
// WithProgramName puts "myapp env" in the failure prefix, the --help hint,
// the help Usage line and --version, so no message names a dotenvctl binary
// the user does not have.
//
//ds:def id=runashost-f5w6cdqm
func runAsHost(w io.Writer, args ...string) int {
return dotenvcmd.Execute(args, w, w, dotenvcmd.WithProgramName(hostProgram+" "+hostEnvVerb))
}
// envCommand mounts the verbs as "myapp env" in a host cobra tree. dotenvcmd
// has no cobra Command to graft, so the host delegates: flag parsing is off
// (dotenvctl parses its own flags) and RunE hands the raw args to Execute.
// The exit code travels back in an exitCodeError.
//
//ds:def id=envcommand-whd27a9r
func envCommand() *cobra.Command {
return &cobra.Command{
Use: hostEnvVerb + " <verb> [args]",
Short: "Edit .env files (the dotenvctl verbs)",
DisableFlagParsing: true,
RunE: func(c *cobra.Command, args []string) error {
code := dotenvcmd.Execute(args, c.OutOrStdout(), c.ErrOrStderr(),
dotenvcmd.WithProgramName(hostProgram+" "+hostEnvVerb))
if code != dotenvcmd.ExitOK {
return exitCodeError(code)
}
return nil
},
}
}
// exitCodeError carries a delegated exit code out of the host tree. The
// failure was already printed by the dotenv verbs, so the host prints nothing.
type exitCodeError int
// Error implements error.
func (e exitCodeError) Error() string { return fmt.Sprintf("exit %d", int(e)) }
// runHostTree builds the host root with the env subcommand and runs one
// command line through it, returning the exit code main would pass to
// os.Exit.
//
//ds:def id=runhosttree-3jzzrrdy
func runHostTree(w io.Writer, args ...string) int {
root := &cobra.Command{Use: hostProgram, SilenceUsage: true, SilenceErrors: true}
root.AddCommand(envCommand())
root.SetArgs(args)
root.SetOut(w)
root.SetErr(w)
if err := root.Execute(); err != nil {
if code, ok := err.(exitCodeError); ok {
return int(code)
}
fmt.Fprintf(w, "%s: %v\n", hostProgram, err)
return dotenvcmd.ExitUsage
}
return dotenvcmd.ExitOK
}