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