1// Package cmd contains chezmoi's commands.
2package cmd
3
4import (
5	"bufio"
6	"bytes"
7	"errors"
8	"fmt"
9	"io"
10	"os"
11	"regexp"
12	"strconv"
13	"strings"
14
15	"github.com/charmbracelet/glamour"
16	"github.com/rs/zerolog"
17	"github.com/spf13/cobra"
18	"go.etcd.io/bbolt"
19	"go.uber.org/multierr"
20
21	"github.com/twpayne/chezmoi/v2/docs"
22)
23
24// Command annotations.
25const (
26	doesNotRequireValidConfig    = "chezmoi_does_not_require_valid_config"
27	modifiesConfigFile           = "chezmoi_modifies_config_file"
28	modifiesDestinationDirectory = "chezmoi_modifies_destination_directory"
29	modifiesSourceDirectory      = "chezmoi_modifies_source_directory"
30	persistentStateMode          = "chezmoi_persistent_state_mode"
31	requiresConfigDirectory      = "chezmoi_requires_config_directory"
32	requiresSourceDirectory      = "chezmoi_requires_source_directory"
33	requiresWorkingTree          = "chezmoi_requires_working_tree"
34	runsCommands                 = "chezmoi_runs_commands"
35)
36
37// Persistent state modes.
38const (
39	persistentStateModeEmpty         = "empty"
40	persistentStateModeReadOnly      = "read-only"
41	persistentStateModeReadMockWrite = "read-mock-write"
42	persistentStateModeReadWrite     = "read-write"
43)
44
45var (
46	noArgs = []string(nil)
47
48	commandsRx       = regexp.MustCompile(`^## Commands`)
49	commandRx        = regexp.MustCompile("^### `(\\S+)`")
50	exampleRx        = regexp.MustCompile("^#### `.+` examples")
51	optionRx         = regexp.MustCompile("^#### `(-\\w|--\\w+)`")
52	endOfCommandsRx  = regexp.MustCompile("^## ")
53	horizontalRuleRx = regexp.MustCompile(`^---`)
54	trailingSpaceRx  = regexp.MustCompile(` +\n`)
55
56	helps map[string]*help
57)
58
59// An ExitCodeError indicates the the main program should exit with the given
60// code.
61type ExitCodeError int
62
63func (e ExitCodeError) Error() string { return "" }
64
65// A VersionInfo contains a version.
66type VersionInfo struct {
67	Version string
68	Commit  string
69	Date    string
70	BuiltBy string
71}
72
73type help struct {
74	long    string
75	example string
76}
77
78func init() {
79	reference, err := docs.FS.ReadFile("REFERENCE.md")
80	if err != nil {
81		panic(err)
82	}
83	helps, err = extractHelps(bytes.NewReader(reference))
84	if err != nil {
85		panic(err)
86	}
87}
88
89// MarshalZerologObject implements
90// github.com/rs/zerolog.LogObjectMarshaler.MarshalZerologObject.
91func (v VersionInfo) MarshalZerologObject(e *zerolog.Event) {
92	e.Str("version", v.Version)
93	e.Str("commit", v.Commit)
94	e.Str("date", v.Date)
95	e.Str("builtBy", v.BuiltBy)
96}
97
98// Main runs chezmoi and returns an exit code.
99func Main(versionInfo VersionInfo, args []string) int {
100	if err := runMain(versionInfo, args); err != nil {
101		if s := err.Error(); s != "" {
102			fmt.Fprintf(os.Stderr, "chezmoi: %s\n", s)
103		}
104		errExitCode := ExitCodeError(1)
105		_ = errors.As(err, &errExitCode)
106		return int(errExitCode)
107	}
108	return 0
109}
110
111// boolAnnotation returns whether cmd is annotated with key.
112func boolAnnotation(cmd *cobra.Command, key string) bool {
113	value, ok := cmd.Annotations[key]
114	if !ok {
115		return false
116	}
117	boolValue, err := strconv.ParseBool(value)
118	if err != nil {
119		panic(err)
120	}
121	return boolValue
122}
123
124// example returns command's example.
125func example(command string) string {
126	help, ok := helps[command]
127	if !ok {
128		return ""
129	}
130	return help.example
131}
132
133// extractHelps returns the helps parse from r.
134func extractHelps(r io.Reader) (map[string]*help, error) {
135	longStyleConfig := glamour.ASCIIStyleConfig
136	longStyleConfig.Code.StylePrimitive.BlockPrefix = ""
137	longStyleConfig.Code.StylePrimitive.BlockSuffix = ""
138	longStyleConfig.Emph.BlockPrefix = ""
139	longStyleConfig.Emph.BlockSuffix = ""
140	longStyleConfig.H4.Prefix = ""
141	longTermRenderer, err := glamour.NewTermRenderer(
142		glamour.WithStyles(longStyleConfig),
143		glamour.WithWordWrap(80),
144	)
145	if err != nil {
146		return nil, err
147	}
148
149	examplesStyleConfig := glamour.ASCIIStyleConfig
150	examplesStyleConfig.Document.Margin = nil
151	examplesTermRenderer, err := glamour.NewTermRenderer(
152		glamour.WithStyles(examplesStyleConfig),
153		glamour.WithWordWrap(80),
154	)
155	if err != nil {
156		return nil, err
157	}
158
159	type stateType int
160	const (
161		stateFindCommands stateType = iota
162		stateFindFirstCommand
163		stateInCommand
164		stateFindExample
165		stateInExample
166	)
167
168	var (
169		state   = stateFindCommands
170		builder = &strings.Builder{}
171		h       *help
172	)
173
174	saveAndReset := func() error {
175		var termRenderer *glamour.TermRenderer
176		switch state {
177		case stateInCommand, stateFindExample:
178			termRenderer = longTermRenderer
179		case stateInExample:
180			termRenderer = examplesTermRenderer
181		default:
182			panic(fmt.Sprintf("%d: invalid state", state))
183		}
184		s, err := termRenderer.Render(builder.String())
185		if err != nil {
186			return err
187		}
188		s = trailingSpaceRx.ReplaceAllString(s, "\n")
189		s = strings.Trim(s, "\n")
190		switch state {
191		case stateInCommand, stateFindExample:
192			h.long = "Description:\n" + s
193		case stateInExample:
194			h.example = s
195		default:
196			panic(fmt.Sprintf("%d: invalid state", state))
197		}
198		builder.Reset()
199		return nil
200	}
201
202	helps := make(map[string]*help)
203	s := bufio.NewScanner(r)
204FOR:
205	for s.Scan() {
206		switch state {
207		case stateFindCommands:
208			if commandsRx.MatchString(s.Text()) {
209				state = stateFindFirstCommand
210			}
211		case stateFindFirstCommand:
212			if m := commandRx.FindStringSubmatch(s.Text()); m != nil {
213				h = &help{}
214				helps[m[1]] = h
215				state = stateInCommand
216			}
217		case stateInCommand, stateFindExample, stateInExample:
218			switch m := commandRx.FindStringSubmatch(s.Text()); {
219			case m != nil:
220				if err := saveAndReset(); err != nil {
221					return nil, err
222				}
223				h = &help{}
224				helps[m[1]] = h
225				state = stateInCommand
226			case optionRx.MatchString(s.Text()):
227				state = stateFindExample
228			case exampleRx.MatchString(s.Text()):
229				if err := saveAndReset(); err != nil {
230					return nil, err
231				}
232				state = stateInExample
233			case endOfCommandsRx.MatchString(s.Text()):
234				if err := saveAndReset(); err != nil {
235					return nil, err
236				}
237				break FOR
238			case horizontalRuleRx.MatchString(s.Text()):
239				if err := saveAndReset(); err != nil {
240					return nil, err
241				}
242				state = stateFindFirstCommand
243			case state != stateFindExample:
244				if _, err := builder.WriteString(s.Text()); err != nil {
245					return nil, err
246				}
247				if err := builder.WriteByte('\n'); err != nil {
248					return nil, err
249				}
250			}
251		}
252	}
253	if err := s.Err(); err != nil {
254		return nil, err
255	}
256	return helps, nil
257}
258
259// markPersistentFlagsRequired marks all of flags as required for cmd.
260func markPersistentFlagsRequired(cmd *cobra.Command, flags ...string) {
261	for _, flag := range flags {
262		if err := cmd.MarkPersistentFlagRequired(flag); err != nil {
263			panic(err)
264		}
265	}
266}
267
268// mustLongHelp returns the long help for command or panics if no long help
269// exists.
270func mustLongHelp(command string) string {
271	help, ok := helps[command]
272	if !ok {
273		panic(fmt.Sprintf("missing long help for command %s", command))
274	}
275	return help.long
276}
277
278// runMain runs chezmoi's main function.
279func runMain(versionInfo VersionInfo, args []string) (err error) {
280	var config *Config
281	if config, err = newConfig(
282		withVersionInfo(versionInfo),
283	); err != nil {
284		return err
285	}
286	defer func() {
287		err = multierr.Append(err, config.close())
288	}()
289	err = config.execute(args)
290	if errors.Is(err, bbolt.ErrTimeout) {
291		// Translate bbolt timeout errors into a friendlier message. As the
292		// persistent state is opened lazily, this error could occur at any
293		// time, so it's easiest to intercept it here.
294		err = errors.New("timeout obtaining persistent state lock, is another instance of chezmoi running?")
295	}
296	return
297}
298