Skip to content

Commit

Permalink
Refactor doctor command to include versions, fixes #160
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne committed Jan 24, 2019
1 parent cf305f8 commit 2d3eff1
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 58 deletions.
307 changes: 249 additions & 58 deletions cmd/doctor.go
Expand Up @@ -4,9 +4,9 @@ import (
"fmt"
"os"
"os/exec"
"runtime"
"text/tabwriter"
"regexp"

"github.com/blang/semver"
"github.com/spf13/cobra"
vfs "github.com/twpayne/go-vfs"
)
Expand All @@ -16,75 +16,266 @@ var doctorCommand = &cobra.Command{
Args: cobra.NoArgs,
Use: "doctor",
Short: "Check your system for potential problems",
Long: `Check your system for potential problems. Such as:
* Does the configuration file exist
* Does the destination directory exist
* Does the source directory exist
* Is Hashicorp Vault installed?
* Is Bitwarden installed?
* Is LastPass installed?
`,
RunE: makeRunE(config.runDoctorCommand),
RunE: makeRunE(config.runDoctorCommandE),
}

func init() {
rootCommand.AddCommand(doctorCommand)
const (
okPrefix = " ok: "
warningPrefix = "warning: "
errorPrefix = " ERROR: "
)

type vcsInfo struct {
versionArgs []string
versionRegexp *regexp.Regexp
}

func (c *Config) runDoctorCommand(fs vfs.FS, args []string) error {
exitWithErr := false
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
var (
vcsInfos = map[string]vcsInfo{
"bzr": vcsInfo{
versionArgs: []string{"--version"},
versionRegexp: regexp.MustCompile(`^Bazaar (bzr) (\d+\.\d+\.\d+)`),
},
"cvs": vcsInfo{
versionArgs: []string{"--version"},
versionRegexp: regexp.MustCompile(`^Concurrent Versions System \(CVS\) (\d+\.\d+\.\d+)`),
},
"git": vcsInfo{
versionArgs: []string{"version"},
versionRegexp: regexp.MustCompile(`^git version (\d+\.\d+\.\d+)`),
},
"hg": vcsInfo{
versionArgs: []string{"version"},
versionRegexp: regexp.MustCompile(`^Mercurial Distributed SCM \(version (\d+\.\d+\.\d+\))`),
},
"svn": vcsInfo{
versionArgs: []string{"--version"},
versionRegexp: regexp.MustCompile(`^svn, version (\d+\.\d+\.\d+)`),
},
}
)

fmt.Fprintf(w, "Arch:\t%s\n", runtime.GOARCH)
fmt.Fprintf(w, "OS:\t%s\n", runtime.GOOS)
type doctorBinaryCheck struct {
name string
binaryName string
path string
minVersion *semver.Version
mustSucceed bool
versionArgs []string
versionRegexp *regexp.Regexp
version *semver.Version
}

for _, dir := range []struct {
description string
path string
requireErrCode bool
}{
{description: "Configuration File", path: configFile, requireErrCode: false},
{description: "Destination Directory", path: config.DestDir, requireErrCode: true},
{description: "Source Directory", path: config.SourceDir, requireErrCode: true},
} {
info, err := fs.Stat(dir.path)
if os.IsNotExist(err) {
fmt.Fprintf(w, "%s:\tDoes Not Exist\t(%s)\n", dir.description, dir.path)
if dir.requireErrCode {
exitWithErr = true
}
} else {
fmt.Fprintf(w, "%s:\tExists\t(%s %s)\n", dir.description, info.Mode().Perm().String(), dir.path)
}
}
type doctorDirectoryCheck struct {
name string
path string
dontWantPerm os.FileMode
info os.FileInfo
}

type doctorFileCheck struct {
name string
path string
mustSucceed bool
info os.FileInfo
}

for _, binary := range []struct {
description string
name string
func init() {
rootCommand.AddCommand(doctorCommand)
}

func (c *Config) runDoctorCommandE(fs vfs.FS, args []string) error {
allOK := true
for _, check := range []interface {
Check() (bool, error)
Enabled() bool
MustSucceed() bool
Result() string
}{
{description: "1Password", name: config.OnePassword.Op},
{description: "Bitwarden", name: config.Bitwarden.Bw},
{description: "Editor", name: config.getEditor()},
{description: "LastPass", name: config.LastPass.Lpass},
{description: "Pass", name: config.Pass.Pass},
{description: "Vault", name: config.Vault.Vault},
{description: "VCS", name: config.SourceVCSCommand},
&doctorDirectoryCheck{
name: "source directory",
path: c.SourceDir,
dontWantPerm: 077,
},
&doctorDirectoryCheck{
name: "destination directory",
path: c.DestDir,
},
&doctorFileCheck{
name: "configuration file",
path: c.configFile,
},
&doctorBinaryCheck{
name: "editor",
binaryName: c.getEditor(),
mustSucceed: true,
},
&doctorBinaryCheck{
name: "source VCS command",
binaryName: c.SourceVCSCommand,
versionArgs: vcsInfos[c.SourceVCSCommand].versionArgs,
versionRegexp: vcsInfos[c.SourceVCSCommand].versionRegexp,
},
&doctorBinaryCheck{
name: "1Password CLI",
binaryName: c.OnePassword.Op,
versionArgs: []string{"--version"},
versionRegexp: regexp.MustCompile(`^(\d+\.\d+\.\d+)`),
},
&doctorBinaryCheck{
name: "Bitwarden CLI",
binaryName: c.Bitwarden.Bw,
versionArgs: []string{"--version"},
versionRegexp: regexp.MustCompile(`^(\d+\.\d+\.\d+)`),
},
&doctorBinaryCheck{
name: "LastPass CLI",
binaryName: c.LastPass.Lpass,
versionArgs: []string{"--version"},
versionRegexp: regexp.MustCompile(`^LastPass CLI v(\d+\.\d+\.\d+)`),
// chezmoi uses lpass show --json which was added in
// https://github.com/lastpass/lastpass-cli/commit/e5a22e2eeef31ab6c54595616e0f57ca0a1c162d
// and the first tag containing that commit is v1.3.0~6.
minVersion: &semver.Version{Major: 1, Minor: 3, Patch: 0},
},
&doctorBinaryCheck{
name: "pass CLI",
binaryName: c.Pass.Pass,
versionArgs: []string{"version"},
versionRegexp: regexp.MustCompile(`(?m)=\s*v(\d+\.\d+\.\d+)`),
},
&doctorBinaryCheck{
name: "Vault CLI",
binaryName: c.Vault.Vault,
versionArgs: []string{"version"},
versionRegexp: regexp.MustCompile(`^Vault\s+v(\d+\.\d+\.\d+)`),
},
} {
path, err := exec.LookPath(binary.name)
if err == nil {
info, err := fs.Stat(path)
if !os.IsNotExist(err) {
fmt.Fprintf(w, "%s:\tInstalled\t(%s %s)\n", binary.description, info.Mode().Perm().String(), path)
} else {
fmt.Fprintf(w, "%s:\tError\t(%s)\n", binary.description, err.Error())
if !check.Enabled() {
continue
}
ok, err := check.Check()
var prefix string
switch {
case ok:
prefix = okPrefix
case !ok && !check.MustSucceed():
prefix = warningPrefix
default:
prefix = errorPrefix
}
if _, err := fmt.Printf("%s%s\n", prefix, check.Result()); err != nil {
return err
}
if err != nil {
if _, err := fmt.Println(err); err != nil {
return err
}
} else {
fmt.Fprintf(w, "%s:\tNot Found\t(looking for: %s)\n", binary.description, binary.name)
}
}
w.Flush()
if exitWithErr {
if !allOK {
os.Exit(1)
}
return nil
}

func (c *doctorBinaryCheck) Check() (bool, error) {
var err error
c.path, err = exec.LookPath(c.binaryName)
if err != nil {
return false, nil
}

if c.versionRegexp != nil {
output, err := exec.Command(c.path, c.versionArgs...).CombinedOutput()
if err != nil {
return false, err
}
m := c.versionRegexp.FindSubmatch(output)
if m == nil {
return false, fmt.Errorf("%s: could not extract version from %q", c.path, output)
}
version, err := semver.Parse(string(m[1]))
if err != nil {
return false, err
}
c.version = &version
if c.minVersion != nil && c.version.LT(*c.minVersion) {
return false, nil
}
}

return true, nil
}

func (c *doctorBinaryCheck) Enabled() bool {
return c.binaryName != ""
}

func (c *doctorBinaryCheck) MustSucceed() bool {
return c.mustSucceed
}

func (c *doctorBinaryCheck) Result() string {
if c.path == "" {
return fmt.Sprintf("%s (%s, not found)", c.binaryName, c.name)
}
s := fmt.Sprintf("%s (%s", c.path, c.name)
if c.version != nil {
s += ", version " + c.version.String()
if c.minVersion != nil && c.version.LT(*c.minVersion) {
s += ", want version >=" + c.minVersion.String()
}
}
s += ")"
return s
}

func (c *doctorDirectoryCheck) Check() (bool, error) {
var err error
c.info, err = os.Stat(c.path)
if err != nil && os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, err
}
if c.info.Mode()&os.ModePerm&c.dontWantPerm != 0 {
return false, nil
}
return true, nil
}

func (c *doctorDirectoryCheck) Enabled() bool {
return true
}

func (c *doctorDirectoryCheck) MustSucceed() bool {
return true
}

func (c *doctorDirectoryCheck) Result() string {
return fmt.Sprintf("%s (%s, perm %03o)", c.path, c.name, c.info.Mode()&os.ModePerm)
}

func (c *doctorFileCheck) Check() (bool, error) {
var err error
c.info, err = os.Stat(c.path)
if err != nil && os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, err
}
return true, nil
}

func (c *doctorFileCheck) Enabled() bool {
return true
}

func (c *doctorFileCheck) MustSucceed() bool {
return c.mustSucceed
}

func (c *doctorFileCheck) Result() string {
return fmt.Sprintf("%s (%s)", c.path, c.name)
}
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -2,6 +2,7 @@ module github.com/twpayne/chezmoi

require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/blang/semver v3.5.1+incompatible
github.com/d4l3k/messagediff v1.2.1
github.com/danieljoos/wincred v1.0.1 // indirect
github.com/godbus/dbus v4.1.0+incompatible // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
@@ -1,6 +1,8 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
Expand Down

0 comments on commit 2d3eff1

Please sign in to comment.