shithub: hugo

Download patch

ref: 3f0f7eed68f44486c1e053bbce25c46c1d52a12f
parent: 6959b7fa80f22aead6fa8c9b8ff3c4b8cc222a30
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Wed Dec 2 06:42:53 EST 2015

Improve error handling in commands

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 #1502

--- a/commands/benchmark.go
+++ b/commands/benchmark.go
@@ -28,9 +28,12 @@
 	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)
 	},
 }
 
@@ -41,13 +44,13 @@
 	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()
@@ -62,7 +65,7 @@
 		f, err := os.Create(cpuProfilefile)
 
 		if err != nil {
-			panic(err)
+			return err
 		}
 
 		pprof.StartCPUProfile(f)
@@ -71,5 +74,7 @@
 			_ = buildSite()
 		}
 	}
+
+	return nil
 
 }
--- a/commands/check.go
+++ b/commands/check.go
@@ -23,9 +23,13 @@
 	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()
+
 	},
 }
--- a/commands/convert.go
+++ b/commands/convert.go
@@ -36,7 +36,7 @@
 	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{
@@ -44,11 +44,8 @@
 	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]))
 	},
 }
 
@@ -57,11 +54,8 @@
 	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]))
 	},
 }
 
@@ -70,11 +64,8 @@
 	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]))
 	},
 }
 
@@ -87,7 +78,9 @@
 }
 
 func convertContents(mark rune) (err error) {
-	InitializeConfig()
+	if err := InitializeConfig(); err != nil {
+		return err
+	}
 	site := &hugolib.Site{}
 
 	if err := site.Initialise(); err != nil {
--- a/commands/genautocomplete.go
+++ b/commands/genautocomplete.go
@@ -31,16 +31,19 @@
 
 	$ . /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
 	},
 }
 
--- a/commands/gendoc.go
+++ b/commands/gendoc.go
@@ -32,7 +32,7 @@
 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
 		}
@@ -55,6 +55,8 @@
 		jww.FEEDBACK.Println("Generating Hugo command-line documentation in", gendocdir, "...")
 		cobra.GenMarkdownTreeCustom(cmd.Root(), gendocdir, prepender, linkHandler)
 		jww.FEEDBACK.Println("Done.")
+
+		return nil
 	},
 }
 
--- a/commands/genman.go
+++ b/commands/genman.go
@@ -18,7 +18,7 @@
 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",
@@ -37,6 +37,8 @@
 		cmd.Root().GenManTree(header, genmandir)
 
 		jww.FEEDBACK.Println("Done.")
+
+		return nil
 	},
 }
 
--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -40,8 +40,44 @@
 	"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",
@@ -52,10 +88,15 @@
 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()
+
 	},
 }
 
@@ -68,9 +109,17 @@
 //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)
 	}
 }
@@ -184,7 +233,7 @@
 }
 
 // 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 == "" {
@@ -195,9 +244,9 @@
 	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)
 		}
 	}
 
@@ -320,7 +369,7 @@
 	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)
 		}
 	}
 
@@ -330,6 +379,8 @@
 		jww.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n",
 			helpers.HugoReleaseVersion(), minVersion)
 	}
+
+	return nil
 }
 
 func watchConfig() {
@@ -344,17 +395,18 @@
 	})
 }
 
-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")))
@@ -361,6 +413,8 @@
 		jww.FEEDBACK.Println("Press Ctrl+C to stop")
 		utils.CheckErr(NewWatcher(0))
 	}
+
+	return nil
 }
 
 func copyStatic() error {
@@ -483,7 +537,6 @@
 	var wg sync.WaitGroup
 
 	if err != nil {
-		fmt.Println(err)
 		return err
 	}
 
--- a/commands/import.go
+++ /dev/null
@@ -1,31 +1,0 @@
-// Copyright © 2015 Steve Francia <spf@spf13.com>.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package commands
-
-import (
-	"github.com/spf13/cobra"
-)
-
-var importCmd = &cobra.Command{
-	Use:   "import",
-	Short: "Import your site from others.",
-	Long: `Import your site from other web site generators like Jekyll.
-
-Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
-	Run: nil,
-}
-
-func init() {
-	importCmd.AddCommand(importJekyllCmd)
-}
--- a/commands/import_jekyll.go
+++ b/commands/import_jekyll.go
@@ -35,6 +35,19 @@
 	jww "github.com/spf13/jwalterweatherman"
 )
 
+func init() {
+	importCmd.AddCommand(importJekyllCmd)
+}
+
+var importCmd = &cobra.Command{
+	Use:   "import",
+	Short: "Import your site from others.",
+	Long: `Import your site from other web site generators like Jekyll.
+
+Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
+	RunE: nil,
+}
+
 var importJekyllCmd = &cobra.Command{
 	Use:   "jekyll",
 	Short: "hugo import from Jekyll",
@@ -41,28 +54,25 @@
 	Long: `hugo import from Jekyll.
 
 Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
-	Run: importFromJekyll,
+	RunE: importFromJekyll,
 }
 
-func importFromJekyll(cmd *cobra.Command, args []string) {
+func importFromJekyll(cmd *cobra.Command, args []string) error {
 	jww.SetLogThreshold(jww.LevelTrace)
 	jww.SetStdoutThreshold(jww.LevelWarn)
 
 	if len(args) < 2 {
-		jww.ERROR.Println(`Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.")
-		return
+		return newUserError(`Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.")
 	}
 
 	jekyllRoot, err := filepath.Abs(filepath.Clean(args[0]))
 	if err != nil {
-		jww.ERROR.Println("Path error:", args[0])
-		return
+		return newUserError("Path error:", args[0])
 	}
 
 	targetDir, err := filepath.Abs(filepath.Clean(args[1]))
 	if err != nil {
-		jww.ERROR.Println("Path error:", args[1])
-		return
+		return newUserError("Path error:", args[1])
 	}
 
 	createSiteFromJekyll(jekyllRoot, targetDir)
@@ -82,8 +92,7 @@
 
 		relPath, err := filepath.Rel(jekyllRoot, path)
 		if err != nil {
-			jww.ERROR.Println("Get rel path error:", path)
-			return err
+			return newUserError("Get rel path error:", path)
 		}
 
 		relPath = filepath.ToSlash(relPath)
@@ -106,7 +115,7 @@
 	err = filepath.Walk(jekyllRoot, callback)
 
 	if err != nil {
-		fmt.Println(err)
+		return err
 	} else {
 		fmt.Println("Congratulations!", fileCount, "posts imported!")
 		fmt.Println("Now, start Hugo by yourself: \n" +
@@ -113,6 +122,8 @@
 			"$ git clone https://github.com/spf13/herring-cove.git " + args[1] + "/themes/herring-cove")
 		fmt.Println("$ cd " + args[1] + "\n$ hugo server -w --theme=herring-cove")
 	}
+
+	return nil
 }
 
 func createSiteFromJekyll(jekyllRoot, targetDir string) {
--- a/commands/limit_darwin.go
+++ b/commands/limit_darwin.go
@@ -30,12 +30,13 @@
 	Short: "Check system ulimit settings",
 	Long: `Hugo will inspect the current ulimit settings on the system.
     This is primarily to ensure that Hugo can watch enough files on some OSs`,
-	Run: func(cmd *cobra.Command, args []string) {
+	RunE: func(cmd *cobra.Command, args []string) error {
 		var rLimit syscall.Rlimit
 		err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
 		if err != nil {
-			jww.ERROR.Println("Error Getting Rlimit ", err)
+			return newSystemError("Error Getting Rlimit ", err)
 		}
+
 		jww.FEEDBACK.Println("Current rLimit:", rLimit)
 
 		jww.FEEDBACK.Println("Attempting to increase limit")
@@ -43,13 +44,15 @@
 		rLimit.Cur = 999999
 		err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
 		if err != nil {
-			jww.ERROR.Println("Error Setting rLimit ", err)
+			return newSystemError("Error Setting rLimit ", err)
 		}
 		err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
 		if err != nil {
-			jww.ERROR.Println("Error Getting rLimit ", err)
+			return newSystemError("Error Getting rLimit ", err)
 		}
 		jww.FEEDBACK.Println("rLimit after change:", rLimit)
+
+		return nil
 	},
 }
 
--- a/commands/list.go
+++ b/commands/list.go
@@ -33,7 +33,7 @@
 	Long: `Listing out various types of content.
 
 List requires a subcommand, e.g. ` + "`hugo list drafts`.",
-	Run: nil,
+	RunE: nil,
 }
 
 var listDraftsCmd = &cobra.Command{
@@ -40,15 +40,18 @@
 	Use:   "drafts",
 	Short: "List all drafts",
 	Long:  `List all of the drafts in your content directory.`,
-	Run: func(cmd *cobra.Command, args []string) {
+	RunE: func(cmd *cobra.Command, args []string) error {
 
-		InitializeConfig()
+		if err := InitializeConfig(); err != nil {
+			return err
+		}
+
 		viper.Set("BuildDrafts", true)
 
 		site := &hugolib.Site{}
 
 		if err := site.Process(); err != nil {
-			fmt.Println("Error Processing Source Content", err)
+			return newSystemError("Error Processing Source Content", err)
 		}
 
 		for _, p := range site.Pages {
@@ -58,6 +61,8 @@
 
 		}
 
+		return nil
+
 	},
 }
 
@@ -66,15 +71,18 @@
 	Short: "List all posts dated in the future",
 	Long: `List all of the posts in your content directory which will be
 posted in the future.`,
-	Run: func(cmd *cobra.Command, args []string) {
+	RunE: func(cmd *cobra.Command, args []string) error {
 
-		InitializeConfig()
+		if err := InitializeConfig(); err != nil {
+			return err
+		}
+
 		viper.Set("BuildFuture", true)
 
 		site := &hugolib.Site{}
 
 		if err := site.Process(); err != nil {
-			fmt.Println("Error Processing Source Content", err)
+			return newSystemError("Error Processing Source Content", err)
 		}
 
 		for _, p := range site.Pages {
@@ -83,6 +91,8 @@
 			}
 
 		}
+
+		return nil
 
 	},
 }
--- a/commands/list_config.go
+++ b/commands/list_config.go
@@ -25,8 +25,11 @@
 	Use:   "config",
 	Short: "Print the site configuration",
 	Long:  `Print the site configuration, both default and custom settings.`,
-	Run: func(cmd *cobra.Command, args []string) {
-		InitializeConfig()
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if err := InitializeConfig(); err != nil {
+			return err
+		}
+
 		allSettings := viper.AllSettings()
 
 		var separator string
@@ -49,5 +52,7 @@
 				fmt.Printf("%s%s%+v\n", k, separator, allSettings[k])
 			}
 		}
+
+		return nil
 	},
 }
--- a/commands/new.go
+++ b/commands/new.go
@@ -55,7 +55,7 @@
 
 If archetypes are provided in your theme or site, they will be used.`,
 
-	Run: NewContent,
+	RunE: NewContent,
 }
 
 var newSiteCmd = &cobra.Command{
@@ -64,7 +64,7 @@
 	Long: `Create a new site in the provided directory.
 The new site will have the correct structure, but no content or theme yet.
 Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
-	Run: NewSite,
+	RunE: NewSite,
 }
 
 var newThemeCmd = &cobra.Command{
@@ -74,12 +74,14 @@
 New theme is a skeleton. Please add content to the touched files. Add your
 name to the copyright line in the license and adjust the theme.toml file
 as you see fit.`,
-	Run: NewTheme,
+	RunE: NewTheme,
 }
 
 // NewContent adds new content to a Hugo site.
-func NewContent(cmd *cobra.Command, args []string) {
-	InitializeConfig()
+func NewContent(cmd *cobra.Command, args []string) error {
+	if err := InitializeConfig(); err != nil {
+		return err
+	}
 
 	if cmd.Flags().Lookup("format").Changed {
 		viper.Set("MetaDataFormat", configFormat)
@@ -86,8 +88,7 @@
 	}
 
 	if len(args) < 1 {
-		cmd.Usage()
-		jww.FATAL.Fatalln("path needs to be provided")
+		return newUserError("path needs to be provided")
 	}
 
 	createpath := args[0]
@@ -100,10 +101,8 @@
 		kind = contentType
 	}
 
-	err := create.NewContent(kind, createpath)
-	if err != nil {
-		jww.ERROR.Println(err)
-	}
+	return create.NewContent(kind, createpath)
+
 }
 
 func doNewSite(basepath string, force bool) error {
@@ -146,32 +145,31 @@
 }
 
 // NewSite creates a new hugo site and initializes a structured Hugo directory.
-func NewSite(cmd *cobra.Command, args []string) {
+func NewSite(cmd *cobra.Command, args []string) error {
 	if len(args) < 1 {
-		cmd.Usage()
-		jww.FATAL.Fatalln("path needs to be provided")
+		return newUserError("path needs to be provided")
 	}
 
 	createpath, err := filepath.Abs(filepath.Clean(args[0]))
 	if err != nil {
-		cmd.Usage()
-		jww.FATAL.Fatalln(err)
+		return newUserError(err)
 	}
 
 	forceNew, _ := cmd.Flags().GetBool("force")
-	if err := doNewSite(createpath, forceNew); err != nil {
-		cmd.Usage()
-		jww.FATAL.Fatalln(err)
-	}
+
+	return doNewSite(createpath, forceNew)
+
 }
 
 // NewTheme creates a new Hugo theme.
-func NewTheme(cmd *cobra.Command, args []string) {
-	InitializeConfig()
+func NewTheme(cmd *cobra.Command, args []string) error {
+	if err := InitializeConfig(); err != nil {
+		return err
+	}
 
 	if len(args) < 1 {
-		cmd.Usage()
-		jww.FATAL.Fatalln("theme name needs to be provided")
+
+		return newUserError("theme name needs to be provided")
 	}
 
 	createpath := helpers.AbsPathify(filepath.Join("themes", args[0]))
@@ -229,10 +227,12 @@
 
 	err = helpers.WriteToDisk(filepath.Join(createpath, "LICENSE.md"), bytes.NewReader(by), hugofs.SourceFs)
 	if err != nil {
-		jww.FATAL.Fatalln(err)
+		return nil
 	}
 
 	createThemeMD(createpath)
+
+	return nil
 }
 
 func mkdir(x ...string) {
--- a/commands/server.go
+++ b/commands/server.go
@@ -57,7 +57,7 @@
 automatically rebuild the site. It will then live reload any open browser pages
 and push the latest content to them. As most Hugo sites are built in a fraction
 of a second, you will be able to save and see your changes nearly instantly.`,
-	//Run: server,
+	//RunE: server,
 }
 
 type filesOnlyFs struct {
@@ -90,10 +90,10 @@
 	serverCmd.Flags().BoolVarP(&NoTimes, "noTimes", "", false, "Don't sync modification time of files")
 	serverCmd.Flags().String("memstats", "", "log memory usage to this file")
 	serverCmd.Flags().Int("meminterval", 100, "interval to poll memory usage (requires --memstats)")
-	serverCmd.Run = server
+	serverCmd.RunE = server
 }
 
-func server(cmd *cobra.Command, args []string) {
+func server(cmd *cobra.Command, args []string) error {
 	InitializeConfig()
 
 	if cmd.Flags().Lookup("disableLiveReload").Changed {
@@ -116,8 +116,7 @@
 		jww.ERROR.Println("port", serverPort, "already in use, attempting to use an available port")
 		sp, err := helpers.FindAvailablePort()
 		if err != nil {
-			jww.ERROR.Println("Unable to find alternative port to use")
-			jww.ERROR.Fatalln(err)
+			return newSystemError("Unable to find alternative port to use:", err)
 		}
 		serverPort = sp.Port
 	}
@@ -126,7 +125,7 @@
 
 	BaseURL, err := fixURL(BaseURL)
 	if err != nil {
-		jww.ERROR.Fatal(err)
+		return err
 	}
 	viper.Set("BaseURL", BaseURL)
 
@@ -146,7 +145,9 @@
 		viper.Set("PublishDir", "/")
 	}
 
-	build(serverWatch)
+	if err := build(serverWatch); err != nil {
+		return err
+	}
 
 	// Watch runs its own server as part of the routine
 	if serverWatch {
@@ -160,12 +161,15 @@
 
 		jww.FEEDBACK.Printf("Watching for changes in %s/{%s}\n", baseWatchDir, rootWatchDirs)
 		err := NewWatcher(serverPort)
+
 		if err != nil {
-			fmt.Println(err)
+			return err
 		}
 	}
 
 	serve(serverPort)
+
+	return nil
 }
 
 func serve(port int) {
--- a/commands/undraft.go
+++ b/commands/undraft.go
@@ -20,7 +20,6 @@
 
 	"github.com/spf13/cobra"
 	"github.com/spf13/hugo/parser"
-	jww "github.com/spf13/jwalterweatherman"
 )
 
 var undraftCmd = &cobra.Command{
@@ -29,18 +28,19 @@
 	Long: `Undraft changes the content's draft status from 'True' to 'False'
 and updates the date to the current date and time.
 If the content's draft status is 'False', nothing is done.`,
-	Run: Undraft,
+	RunE: Undraft,
 }
 
 // Publish publishes the specified content by setting its draft status
 // to false and setting its publish date to now. If the specified content is
 // not a draft, it will log an error.
-func Undraft(cmd *cobra.Command, args []string) {
-	InitializeConfig()
+func Undraft(cmd *cobra.Command, args []string) error {
+	if err := InitializeConfig(); err != nil {
+		return err
+	}
 
 	if len(args) < 1 {
-		cmd.Usage()
-		jww.FATAL.Fatalln("a piece of content needs to be specified")
+		return newUserError("a piece of content needs to be specified")
 	}
 
 	location := args[0]
@@ -47,8 +47,7 @@
 	// open the file
 	f, err := os.Open(location)
 	if err != nil {
-		jww.ERROR.Print(err)
-		return
+		return err
 	}
 
 	// get the page from file
@@ -55,27 +54,24 @@
 	p, err := parser.ReadFrom(f)
 	f.Close()
 	if err != nil {
-		jww.ERROR.Print(err)
-		return
+		return err
 	}
 
 	w, err := undraftContent(p)
 	if err != nil {
-		jww.ERROR.Printf("an error occurred while undrafting %q: %s", location, err)
-		return
+		return newSystemError("an error occurred while undrafting %q: %s", location, err)
 	}
 
 	f, err = os.OpenFile(location, os.O_WRONLY|os.O_TRUNC, 0644)
 	if err != nil {
-		jww.ERROR.Printf("%q not be undrafted due to error opening file to save changes: %q\n", location, err)
-		return
+		return newSystemError("%q not be undrafted due to error opening file to save changes: %q\n", location, err)
 	}
 	defer f.Close()
 	_, err = w.WriteTo(f)
 	if err != nil {
-		jww.ERROR.Printf("%q not be undrafted due to save error: %q\n", location, err)
+		return newSystemError("%q not be undrafted due to save error: %q\n", location, err)
 	}
-	return
+	return nil
 }
 
 // undraftContent: if the content is a draft, change its draft status to
--- a/commands/version.go
+++ b/commands/version.go
@@ -32,7 +32,7 @@
 	Use:   "version",
 	Short: "Print the version number of Hugo",
 	Long:  `All software has versions. This is Hugo's.`,
-	Run: func(cmd *cobra.Command, args []string) {
+	RunE: func(cmd *cobra.Command, args []string) error {
 		if hugolib.BuildDate == "" {
 			setBuildDate() // set the build date from executable's mdate
 		} else {
@@ -43,6 +43,8 @@
 		} else {
 			fmt.Printf("Hugo Static Site Generator v%s-%s BuildDate: %s\n", helpers.HugoVersion(), strings.ToUpper(hugolib.CommitHash), hugolib.BuildDate)
 		}
+
+		return nil
 	},
 }
 
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -254,9 +254,11 @@
 	return nil
 }
 
-func (s *Site) Analyze() {
-	s.Process()
-	s.ShowPlan(os.Stdout)
+func (s *Site) Analyze() error {
+	if err := s.Process(); err != nil {
+		return err
+	}
+	return s.ShowPlan(os.Stdout)
 }
 
 func (s *Site) prepTemplates() {
--