Skip to content

Commit

Permalink
Improve error handling in commands
Browse files Browse the repository at this point in the history
Cobra, the CLI commander in use in Hugo, has some long awaited improvements in the error handling department.
This enables a more centralized error handling approach.

This commit introduces that by changing all the command funcs to `RunE`:

* The core part of the error logging, usage logging and `os.Exit(-1)` is now performed in one place and that one place only.
* The usage text is now only shown on invalid arguments etc. (user errors)

Fixes gohugoio#1502
  • Loading branch information
bep authored and tychoish committed Aug 13, 2017
1 parent 27b3cf9 commit 53a3aab
Show file tree
Hide file tree
Showing 17 changed files with 219 additions and 155 deletions.
17 changes: 11 additions & 6 deletions commands/benchmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ var benchmark = &cobra.Command{
Short: "Benchmark hugo by building a site a number of times.",
Long: `Hugo can build a site many times over and analyze the running process
creating a benchmark.`,
Run: func(cmd *cobra.Command, args []string) {
InitializeConfig()
bench(cmd, args)
RunE: func(cmd *cobra.Command, args []string) error {
if err := InitializeConfig(); err != nil {
return err
}

return bench(cmd, args)
},
}

Expand All @@ -41,13 +44,13 @@ func init() {
benchmark.Flags().IntVarP(&benchmarkTimes, "count", "n", 13, "number of times to build the site")
}

func bench(cmd *cobra.Command, args []string) {
func bench(cmd *cobra.Command, args []string) error {

if memProfilefile != "" {
f, err := os.Create(memProfilefile)

if err != nil {
panic(err)
return err
}
for i := 0; i < benchmarkTimes; i++ {
_ = buildSite()
Expand All @@ -62,7 +65,7 @@ func bench(cmd *cobra.Command, args []string) {
f, err := os.Create(cpuProfilefile)

if err != nil {
panic(err)
return err
}

pprof.StartCPUProfile(f)
Expand All @@ -72,4 +75,6 @@ func bench(cmd *cobra.Command, args []string) {
}
}

return nil

}
10 changes: 7 additions & 3 deletions commands/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ var check = &cobra.Command{
Short: "Check content in the source directory",
Long: `Hugo will perform some basic analysis on the content provided
and will give feedback.`,
Run: func(cmd *cobra.Command, args []string) {
InitializeConfig()
RunE: func(cmd *cobra.Command, args []string) error {
if err := InitializeConfig(); err != nil {
return err
}
site := hugolib.Site{}
site.Analyze()

return site.Analyze()

},
}
27 changes: 10 additions & 17 deletions commands/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,16 @@ var convertCmd = &cobra.Command{
Long: `Convert your content (e.g. front matter) to different formats.
See convert's subcommands toJSON, toTOML and toYAML for more information.`,
Run: nil,
RunE: nil,
}

var toJSONCmd = &cobra.Command{
Use: "toJSON",
Short: "Convert front matter to JSON",
Long: `toJSON converts all front matter in the content directory
to use JSON for the front matter.`,
Run: func(cmd *cobra.Command, args []string) {
err := convertContents(rune([]byte(parser.JSON_LEAD)[0]))
if err != nil {
jww.ERROR.Println(err)
}
RunE: func(cmd *cobra.Command, args []string) error {
return convertContents(rune([]byte(parser.JSON_LEAD)[0]))
},
}

Expand All @@ -57,11 +54,8 @@ var toTOMLCmd = &cobra.Command{
Short: "Convert front matter to TOML",
Long: `toTOML converts all front matter in the content directory
to use TOML for the front matter.`,
Run: func(cmd *cobra.Command, args []string) {
err := convertContents(rune([]byte(parser.TOML_LEAD)[0]))
if err != nil {
jww.ERROR.Println(err)
}
RunE: func(cmd *cobra.Command, args []string) error {
return convertContents(rune([]byte(parser.TOML_LEAD)[0]))
},
}

Expand All @@ -70,11 +64,8 @@ var toYAMLCmd = &cobra.Command{
Short: "Convert front matter to YAML",
Long: `toYAML converts all front matter in the content directory
to use YAML for the front matter.`,
Run: func(cmd *cobra.Command, args []string) {
err := convertContents(rune([]byte(parser.YAML_LEAD)[0]))
if err != nil {
jww.ERROR.Println(err)
}
RunE: func(cmd *cobra.Command, args []string) error {
return convertContents(rune([]byte(parser.YAML_LEAD)[0]))
},
}

Expand All @@ -87,7 +78,9 @@ func init() {
}

func convertContents(mark rune) (err error) {
InitializeConfig()
if err := InitializeConfig(); err != nil {
return err
}
site := &hugolib.Site{}

if err := site.Initialise(); err != nil {
Expand Down
9 changes: 6 additions & 3 deletions commands/genautocomplete.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,19 @@ or just source them in directly:
$ . /etc/bash_completion`,

Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
if autocompleteType != "bash" {
jww.FATAL.Fatalln("Only Bash is supported for now")
return newUserError("Only Bash is supported for now")
}

err := cmd.Root().GenBashCompletionFile(autocompleteTarget)

if err != nil {
jww.FATAL.Fatalln("Failed to generate shell completion file:", err)
return err
} else {
jww.FEEDBACK.Println("Bash completion file for Hugo saved to", autocompleteTarget)
}
return nil
},
}

Expand Down
4 changes: 3 additions & 1 deletion commands/gendoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ of Hugo's command-line interface for http://gohugo.io/.
It creates one Markdown file per command with front matter suitable
for rendering in Hugo.`,

Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
if !strings.HasSuffix(gendocdir, helpers.FilePathSeparator) {
gendocdir += helpers.FilePathSeparator
}
Expand All @@ -55,6 +55,8 @@ for rendering in Hugo.`,
jww.FEEDBACK.Println("Generating Hugo command-line documentation in", gendocdir, "...")
cobra.GenMarkdownTreeCustom(cmd.Root(), gendocdir, prepender, linkHandler)
jww.FEEDBACK.Println("Done.")

return nil
},
}

Expand Down
4 changes: 3 additions & 1 deletion commands/genman.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var genmanCmd = &cobra.Command{
command-line interface. By default, it creates the man page files
in the "man" directory under the current directory.`,

Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
header := &cobra.GenManHeader{
Section: "1",
Manual: "Hugo Manual",
Expand All @@ -37,6 +37,8 @@ in the "man" directory under the current directory.`,
cmd.Root().GenManTree(header, genmandir)

jww.FEEDBACK.Println("Done.")

return nil
},
}

Expand Down
85 changes: 69 additions & 16 deletions commands/hugo.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,44 @@ import (
"github.com/spf13/nitro"
"github.com/spf13/viper"
"gopkg.in/fsnotify.v1"
"regexp"
)

// userError is an error used to signal different error situations in command handling.
type commandError struct {
s string
userError bool
}

func (u commandError) Error() string {
return u.s
}

func (u commandError) isUserError() bool {
return u.userError
}

func newUserError(messages ...interface{}) commandError {
return commandError{s: fmt.Sprintln(messages...), userError: true}
}

func newSystemError(messages ...interface{}) commandError {
return commandError{s: fmt.Sprintln(messages...), userError: false}
}

// catch some of the obvious user errors from Cobra.
// We don't want to show the usage message for every error.
// The below may be to generic. Time will show.
var userErrorRegexp = regexp.MustCompile("argument|flag|shorthand")

func isUserError(err error) bool {
if cErr, ok := err.(commandError); ok && cErr.isUserError() {
return true
}

return userErrorRegexp.MatchString(err.Error())
}

//HugoCmd is Hugo's root command. Every other command attached to HugoCmd is a child command to it.
var HugoCmd = &cobra.Command{
Use: "hugo",
Expand All @@ -52,10 +88,15 @@ Hugo is a Fast and Flexible Static Site Generator
built with love by spf13 and friends in Go.
Complete documentation is available at http://gohugo.io/.`,
Run: func(cmd *cobra.Command, args []string) {
InitializeConfig()
RunE: func(cmd *cobra.Command, args []string) error {
if err := InitializeConfig(); err != nil {
return err
}

watchConfig()
build()

return build()

},
}

Expand All @@ -68,9 +109,17 @@ var Source, CacheDir, Destination, Theme, BaseURL, CfgFile, LogFile, Editor stri
//Execute adds all child commands to the root command HugoCmd and sets flags appropriately.
func Execute() {
HugoCmd.SetGlobalNormalizationFunc(helpers.NormalizeHugoFlags)

HugoCmd.SilenceUsage = true

AddCommands()
if err := HugoCmd.Execute(); err != nil {
// the err is already logged by Cobra

if c, err := HugoCmd.ExecuteC(); err != nil {
if isUserError(err) {
c.Println("")
c.Println(c.UsageString())
}

os.Exit(-1)
}
}
Expand Down Expand Up @@ -184,7 +233,7 @@ func LoadDefaultSettings() {
}

// InitializeConfig initializes a config file with sensible default configuration flags.
func InitializeConfig() {
func InitializeConfig() error {
viper.SetConfigFile(CfgFile)
// See https://github.com/spf13/viper/issues/73#issuecomment-126970794
if Source == "" {
Expand All @@ -195,9 +244,9 @@ func InitializeConfig() {
err := viper.ReadInConfig()
if err != nil {
if _, ok := err.(viper.ConfigParseError); ok {
jww.ERROR.Println(err)
return newSystemError(err)
} else {
jww.ERROR.Println("Unable to locate Config file. Perhaps you need to create a new site. Run `hugo help new` for details", err)
return newSystemError("Unable to locate Config file. Perhaps you need to create a new site. Run `hugo help new` for details", err)
}
}

Expand Down Expand Up @@ -320,7 +369,7 @@ func InitializeConfig() {
themeDir := helpers.GetThemeDir()
if themeDir != "" {
if _, err := os.Stat(themeDir); os.IsNotExist(err) {
jww.FATAL.Fatalln("Unable to find theme Directory:", themeDir)
return newSystemError("Unable to find theme Directory:", themeDir)
}
}

Expand All @@ -330,6 +379,8 @@ func InitializeConfig() {
jww.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n",
helpers.HugoReleaseVersion(), minVersion)
}

return nil
}

func watchConfig() {
Expand All @@ -344,23 +395,26 @@ func watchConfig() {
})
}

func build(watches ...bool) {
err := copyStatic()
if err != nil {
fmt.Println(err)
utils.StopOnErr(err, fmt.Sprintf("Error copying static files to %s", helpers.AbsPathify(viper.GetString("PublishDir"))))
func build(watches ...bool) error {

if err := copyStatic(); err != nil {
return fmt.Errorf("Error copying static files to %s: %s", helpers.AbsPathify(viper.GetString("PublishDir")), err)
}
watch := false
if len(watches) > 0 && watches[0] {
watch = true
}
utils.StopOnErr(buildSite(BuildWatch || watch))
if err := buildSite(BuildWatch || watch); err != nil {
return fmt.Errorf("Error building site: %s", err)
}

if BuildWatch {
jww.FEEDBACK.Println("Watching for changes in", helpers.AbsPathify(viper.GetString("ContentDir")))
jww.FEEDBACK.Println("Press Ctrl+C to stop")
utils.CheckErr(NewWatcher(0))
}

return nil
}

func copyStatic() error {
Expand Down Expand Up @@ -483,7 +537,6 @@ func NewWatcher(port int) error {
var wg sync.WaitGroup

if err != nil {
fmt.Println(err)
return err
}

Expand Down
31 changes: 0 additions & 31 deletions commands/import.go

This file was deleted.

Loading

0 comments on commit 53a3aab

Please sign in to comment.