diff --git a/.travis.yml b/.travis.yml index fa6c5cf..5b42117 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go go: - - 1.9.2 + - 1.13.8 - master notifications: diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index e7aa0f8..0000000 --- a/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM scratch -ADD hostess /hostess -ENTRYPOINT ["/hostess"] diff --git a/Makefile b/Makefile index bc41ebb..70d5dc8 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,7 @@ -PACKAGES=$(go list ./... | grep -v vendor) -prefix=/usr/local -exec_prefix=$(prefix) -bindir=$(exec_prefix)/bin -datarootdir=$(prefix)/share -datadir=$(datarootdir) -mandir=$(datarootdir)/man - -.PHONY: all deps build test gox build-all install clean - all: build test deps: go get golang.org/x/lint/golint - go get github.com/stretchr/testify/assert go get golang.org/x/tools/cmd/cover go get @@ -24,19 +13,17 @@ test: go vet $(PACKAGES) golint $(PACKAGES) -gox: - go get github.com/mitchellh/gox - gox -build-toolchain - build-all: test - which gox || make gox - gox -arch="386 amd64 arm" -os="darwin linux windows" github.com/cbednarski/hostess/cmd/hostess -install: hostess - mkdir -p $(bindir) - cp hostess $(bindir)/hostess + echo FIXME + exit 1 + +install: + go install . clean: rm -f ./hostess rm -f ./hostess_* rm -f ./coverage.* + +.PHONY: all deps build test gox build-all install clean diff --git a/README.md b/README.md index ee9c4c9..6d86d84 100644 --- a/README.md +++ b/README.md @@ -10,67 +10,46 @@ Because sometimes DNS doesn't work in production. And because editing `/etc/hosts` by hand is a pain. Put hostess in your `Makefile` or deploy scripts and call it a day. -## Using Hostess +**Note: 0.5.0 has backwards incompatible changes in the API and CLI.** -### Download and Install +## Installation Download a [precompiled release](https://github.com/cbednarski/hostess/releases) -from GitHub. +from GitHub, or build from source (with a [recent version of Go](https://golang.org/dl)): -Builds are supported for OSX, Linux, Windows, and RaspberryPi. + go get -u github.com/cbednarski/hostess ### Usage - hostess add domain ip # Add or replace a hosts entry for this domain pointing to this IP - hostess aff domain ip # Add or replace a hosts entry in an off state - hostess del domain # (alias rm) Remove a domain from your hosts file - hostess has domain # exit code 0 if the domain is in your hostfile, 1 otherwise - hostess off domain # Disable a domain (but don't remove it completely), exit 1 if entry is missing - hostess on domain # Re-enable a domain that was disabled, exit 1 if entry is missing - hostess list # (alias ls) List domains, target ips, and on/off status - hostess fix # Rewrite your hosts file; use -n to dry run - hostess dump # Dump your hostfile as json - hostess apply # Add entries from a json file +Run `hostess` or `hostess -h` to see a full list of commands. - Flags +### Behavior - -n # Dry run. Show what will happen but don't do it; output to stdout - -4 # Limit operation to ipv4 entries - -6 # Limit operation to ipv6 entries - -hostess will mangle your hosts file. Domains pointing at the same IP will be -grouped together and disabled domains commented out. +On unixes, hostess follows the format specified by `man hosts`, with one line +per IP address: 127.0.0.1 localhost hostname2 hostname3 127.0.1.1 machine.name # 10.10.20.30 some.host -### IPv4 and IPv6 +On Windows, hostess writes each hostname on its own line. -Your hosts file *can* contain overlapping entries where the same hostname points -to both an IPv4 and IPv6 IP. In this case, hostess commands will apply to both -entries. Typically you won't have this kind of overlap and the default behavior -is OK. However, if you need to be more granular you can use `-4` or `-6` to -limit operations to entries associated with that type of IP. + 127.0.0.1 localhost + 127.0.0.1 hostname2 + 127.0.0.1 hostname3 -## Developing Hostess +## Configuration -### Configuration +You can force hostess to behave one way or the other with `HOSTESS_FMT=windows` +or `HOSTESS_FMT=unix`. By default, hostess will read / write to `/etc/hosts`. You can use the `HOSTESS_PATH` environment variable to provide an alternate path (for testing). -### Building from Source - -To build from source you'll need to have go 1.4+ - -#### Install with go get - - go get github.com/cbednarski/hostess/cmd/hostess - -#### Install from source +### IPv4 and IPv6 - git clone https://github.com/cbednarski/hostess - cd hostess - make - make install +Your hosts file _may_ contain overlapping entries where the same hostname points +to both an IPv4 and IPv6 IP. In this case, hostess commands will apply to both +entries. Typically you won't have this kind of overlap and the default behavior +is OK. However, if you need to be more granular you can use `-4` or `-6` to +limit operations to entries associated with that type of IP. diff --git a/cmd/hostess/hostess.go b/cmd/hostess/hostess.go deleted file mode 100644 index ee6c94a..0000000 --- a/cmd/hostess/hostess.go +++ /dev/null @@ -1,125 +0,0 @@ -package main - -import ( - "os" - - "github.com/cbednarski/hostess" - "github.com/codegangsta/cli" -) - -func getCommand() string { - return os.Args[1] -} - -func getArgs() []string { - return os.Args[2:] -} - -const help = `an idempotent tool for managing /etc/hosts - - * Commands will exit 0 or 1 in a sensible way to facilitate scripting. - * Hostess operates on /etc/hosts by default. Specify the HOSTESS_PATH - environment variable to change this. - * Run 'hostess fix -n' to preview changes hostess will make to your hostsfile. - * Report bugs and feedback at https://github.com/cbednarski/hostess` - -func main() { - app := cli.NewApp() - app.Name = "hostess" - app.Authors = []cli.Author{{Name: "Chris Bednarski", Email: "banzaimonkey@gmail.com"}} - app.Usage = help - app.Version = "0.3.0" - - app.Flags = []cli.Flag{ - cli.BoolFlag{ - Name: "f", - Usage: "operate even if there are errors or conflicts", - }, - cli.BoolFlag{ - Name: "n", - Usage: "no-op. Show changes but don't write them.", - }, - cli.BoolFlag{ - Name: "q", - Usage: "quiet operation -- no notices", - }, - cli.BoolFlag{ - Name: "s", - Usage: "silent operation -- no errors (implies -q)", - }, - } - - app.Commands = []cli.Command{ - { - Name: "add", - Usage: "add or replace a hosts entry", - Action: hostess.Add, - Flags: app.Flags, - }, - { - Name: "aff", - Usage: "add or replace a hosts entry in an off state", - Action: hostess.Add, - Flags: app.Flags, - }, - { - Name: "del", - Aliases: []string{"rm"}, - Usage: "delete a hosts entry", - Action: hostess.Del, - Flags: app.Flags, - }, - { - Name: "has", - Usage: "exit 0 if entry exists, 1 if not", - Action: hostess.Has, - Flags: app.Flags, - }, - { - Name: "on", - Usage: "enable a hosts entry (if it exists)", - Action: hostess.OnOff, - Flags: app.Flags, - }, - { - Name: "off", - Usage: "disable a hosts entry (don't delete it)", - Action: hostess.OnOff, - Flags: app.Flags, - }, - { - Name: "list", - Aliases: []string{"ls"}, - Usage: "list entries in the hosts file", - Action: hostess.Ls, - Flags: app.Flags, - }, - { - Name: "fix", - Usage: "reformat the hosts file based on hostess' rules", - Action: hostess.Fix, - Flags: app.Flags, - }, - { - Name: "fixed", - Usage: "exit 0 if the hosts file is formatted, 1 if not", - Action: hostess.Fixed, - Flags: app.Flags, - }, - { - Name: "dump", - Usage: "dump the hosts file as JSON", - Action: hostess.Dump, - Flags: app.Flags, - }, - { - Name: "apply", - Usage: "add hostnames from a JSON file to the hosts file", - Action: hostess.Apply, - Flags: app.Flags, - }, - } - - app.Run(os.Args) - os.Exit(0) -} diff --git a/commands.go b/commands.go deleted file mode 100644 index 112266c..0000000 --- a/commands.go +++ /dev/null @@ -1,289 +0,0 @@ -package hostess - -import ( - "bytes" - "fmt" - "io/ioutil" - "os" - "strings" - - "github.com/codegangsta/cli" -) - -// AnyBool checks whether a boolean CLI flag is set either globally or on this -// individual command. This provides more flexible flag parsing behavior. -func AnyBool(c *cli.Context, key string) bool { - return c.Bool(key) || c.GlobalBool(key) -} - -// ErrCantWriteHostFile indicates that we are unable to write to the hosts file -var ErrCantWriteHostFile = fmt.Errorf( - "Unable to write to %s. Maybe you need to sudo?", GetHostsPath()) - -// MaybeErrorln will print an error message unless -s is passed -func MaybeErrorln(c *cli.Context, message string) { - if !AnyBool(c, "s") { - os.Stderr.WriteString(fmt.Sprintf("%s\n", message)) - } -} - -// MaybeError will print an error message unless -s is passed and then exit -func MaybeError(c *cli.Context, message string) { - MaybeErrorln(c, message) - os.Exit(1) -} - -// MaybePrintln will print a message unless -q or -s is passed -func MaybePrintln(c *cli.Context, message string) { - if !AnyBool(c, "q") && !AnyBool(c, "s") { - fmt.Println(message) - } -} - -// MaybeLoadHostFile will try to load, parse, and return a Hostfile. If we -// encounter errors we will terminate, unless -f is passed. -func MaybeLoadHostFile(c *cli.Context) *Hostfile { - hostsfile, errs := LoadHostfile() - if len(errs) > 0 && !AnyBool(c, "f") { - for _, err := range errs { - MaybeErrorln(c, err.Error()) - } - MaybeError(c, "Errors while parsing hostsfile. Try hostess fix") - } - return hostsfile -} - -// AlwaysLoadHostFile will load, parse, and return a Hostfile. If we encouter -// errors they will be printed to the terminal, but we'll try to continue. -func AlwaysLoadHostFile(c *cli.Context) *Hostfile { - hostsfile, errs := LoadHostfile() - if len(errs) > 0 { - for _, err := range errs { - MaybeErrorln(c, err.Error()) - } - } - return hostsfile -} - -// MaybeSaveHostFile will output or write the Hostfile, or exit 1 and error. -func MaybeSaveHostFile(c *cli.Context, hostfile *Hostfile) { - // If -n is passed, no-op and output the resultant hosts file to stdout. - // Otherwise it's for real and we're going to write it. - if AnyBool(c, "n") { - fmt.Printf("%s", hostfile.Format()) - } else { - err := hostfile.Save() - if err != nil { - MaybeError(c, ErrCantWriteHostFile.Error()) - } - } -} - -// StrPadRight adds spaces to the right of a string until it reaches l length. -// If the input string is already that long, do nothing. -func StrPadRight(s string, l int) string { - r := l - len(s) - if r < 0 { - r = 0 - } - return s + strings.Repeat(" ", r) -} - -// Add command parses and adds or updates a hostname in the -// hosts file. If the aff command is used the hostname will be disabled or -// added in the off state. -func Add(c *cli.Context) { - if len(c.Args()) != 2 { - MaybeError(c, "expected ") - } - - hostsfile := MaybeLoadHostFile(c) - - hostname, err := NewHostname(c.Args()[0], c.Args()[1], true) - if err != nil { - MaybeError(c, fmt.Sprintf("Failed to parse hosts entry: %s", err)) - } - // If the command is aff instead of add then the entry should be disabled - if c.Command.Name == "aff" { - hostname.Enabled = false - } - - replace := hostsfile.Hosts.ContainsDomain(hostname.Domain) - // Note that Add() may return an error, but they are informational only. We - // don't actually care what the error is -- we just want to add the - // hostname and save the file. This way the behavior is idempotent. - hostsfile.Hosts.Add(hostname) - - // If the user passes -n then we'll Add and show the new hosts file, but - // not save it. - if c.Bool("n") || AnyBool(c, "n") { - fmt.Printf("%s", hostsfile.Format()) - } else { - MaybeSaveHostFile(c, hostsfile) - // We'll give a little bit of information about whether we added or - // updated, but if the user wants to know they can use has or ls to - // show the file before they run the operation. Maybe later we can add - // a verbose flag to show more information. - if replace { - MaybePrintln(c, fmt.Sprintf("Updated %s", hostname.FormatHuman())) - } else { - MaybePrintln(c, fmt.Sprintf("Added %s", hostname.FormatHuman())) - } - } -} - -// Del command removes any hostname(s) matching from the hosts file -func Del(c *cli.Context) { - if len(c.Args()) != 1 { - MaybeError(c, "expected ") - } - domain := c.Args()[0] - hostsfile := MaybeLoadHostFile(c) - - found := hostsfile.Hosts.ContainsDomain(domain) - if found { - hostsfile.Hosts.RemoveDomain(domain) - if AnyBool(c, "n") { - fmt.Printf("%s", hostsfile.Format()) - } else { - MaybeSaveHostFile(c, hostsfile) - MaybePrintln(c, fmt.Sprintf("Deleted %s", domain)) - } - } else { - MaybePrintln(c, fmt.Sprintf("%s not found in %s", domain, GetHostsPath())) - } -} - -// Has command indicates whether a hostname is present in the hosts file -func Has(c *cli.Context) { - if len(c.Args()) != 1 { - MaybeError(c, "expected ") - } - domain := c.Args()[0] - hostsfile := MaybeLoadHostFile(c) - - found := hostsfile.Hosts.ContainsDomain(domain) - if found { - MaybePrintln(c, fmt.Sprintf("Found %s in %s", domain, GetHostsPath())) - } else { - MaybeError(c, fmt.Sprintf("%s not found in %s", domain, GetHostsPath())) - } - -} - -// OnOff enables (uncomments) or disables (comments) the specified hostname in -// the hosts file. Exits code 1 if the hostname is missing. -func OnOff(c *cli.Context) { - if len(c.Args()) != 1 { - MaybeError(c, "expected ") - } - domain := c.Args()[0] - hostsfile := MaybeLoadHostFile(c) - - // Switch on / off commands - success := false - if c.Command.Name == "on" { - success = hostsfile.Hosts.Enable(domain) - } else { - success = hostsfile.Hosts.Disable(domain) - } - - if success { - MaybeSaveHostFile(c, hostsfile) - if c.Command.Name == "on" { - MaybePrintln(c, fmt.Sprintf("Enabled %s", domain)) - } else { - MaybePrintln(c, fmt.Sprintf("Disabled %s", domain)) - } - } else { - MaybeError(c, fmt.Sprintf("%s not found in %s", domain, GetHostsPath())) - } -} - -// Ls command shows a list of hostnames in the hosts file -func Ls(c *cli.Context) { - hostsfile := AlwaysLoadHostFile(c) - maxdomain := 0 - maxip := 0 - for _, hostname := range hostsfile.Hosts { - dlen := len(hostname.Domain) - if dlen > maxdomain { - maxdomain = dlen - } - ilen := len(hostname.IP) - if ilen > maxip { - maxip = ilen - } - } - - for _, hostname := range hostsfile.Hosts { - fmt.Printf("%s -> %s %s\n", - StrPadRight(hostname.Domain, maxdomain), - StrPadRight(hostname.IP.String(), maxip), - hostname.FormatEnabled()) - } -} - -const fixHelp = `Programmatically rewrite your hostsfile. - -Domains pointing to the same IP will be consolidated onto single lines and -sorted. Duplicates and conflicts will be removed. Extra whitespace and comments -will be removed. - - hostess fix Rewrite the hostsfile - hostess fix -n Show the new hostsfile. Don't write it to disk. -` - -// Fix command removes duplicates and conflicts from the hosts file -func Fix(c *cli.Context) { - hostsfile := AlwaysLoadHostFile(c) - if bytes.Equal(hostsfile.GetData(), hostsfile.Format()) { - MaybePrintln(c, fmt.Sprintf("%s is already formatted and contains no dupes or conflicts; nothing to do", GetHostsPath())) - os.Exit(0) - } - MaybeSaveHostFile(c, hostsfile) -} - -// Fixed command removes duplicates and conflicts from the hosts file -func Fixed(c *cli.Context) { - hostsfile := AlwaysLoadHostFile(c) - if bytes.Equal(hostsfile.GetData(), hostsfile.Format()) { - MaybePrintln(c, fmt.Sprintf("%s is already formatted and contains no dupes or conflicts", GetHostsPath())) - os.Exit(0) - } else { - MaybePrintln(c, fmt.Sprintf("%s is not formatted. Use hostess fix to format it", GetHostsPath())) - os.Exit(1) - } -} - -// Dump command outputs hosts file contents as JSON -func Dump(c *cli.Context) { - hostsfile := AlwaysLoadHostFile(c) - jsonbytes, err := hostsfile.Hosts.Dump() - if err != nil { - MaybeError(c, err.Error()) - } - fmt.Println(fmt.Sprintf("%s", jsonbytes)) -} - -// Apply command adds hostnames to the hosts file from JSON -func Apply(c *cli.Context) { - if len(c.Args()) != 1 { - MaybeError(c, "Usage should be apply [filename]") - } - filename := c.Args()[0] - - jsonbytes, err := ioutil.ReadFile(filename) - if err != nil { - MaybeError(c, fmt.Sprintf("Unable to read %s: %s", filename, err)) - } - - hostfile := AlwaysLoadHostFile(c) - err = hostfile.Hosts.Apply(jsonbytes) - if err != nil { - MaybeError(c, fmt.Sprintf("Error applying changes to hosts file: %s", err)) - } - - MaybeSaveHostFile(c, hostfile) - MaybePrintln(c, fmt.Sprintf("%s applied", filename)) -} diff --git a/commands/commands.go b/commands/commands.go new file mode 100644 index 0000000..4738ee4 --- /dev/null +++ b/commands/commands.go @@ -0,0 +1,243 @@ +package commands + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/cbednarski/hostess/hostess" +) + +const ( + IPv4 = 1 << iota + IPv6 = 1 << iota +) + +type Options struct { + IPVersion int + Preview bool + Force bool +} + +// ErrCantWriteHostFile indicates that we are unable to write to the hosts file +var ErrCantWriteHostFile = "" + +// ErrorLn will print an error message unless -s is passed +func ErrorLn(message string) { + os.Stderr.WriteString(fmt.Sprintf("%s\n", message)) +} + +// MaybePrintln will print a message unless -q or -s is passed +func MaybePrintln(options *Options, message string) { + if !AnyBool(c, "q") && !AnyBool(c, "s") { + fmt.Println(message) + } +} + +// LoadHostfile will try to load, parse, and return a Hostfile. If we +// encounter errors we will terminate, unless -f is passed. +func LoadHostfile(options *Options) (*hostess.Hostfile, error) { + hosts, errs := hostess.LoadHostfile() + + if len(errs) > 0 && !options.Force { + for _, err := range errs { + ErrorLn(err.Error()) + } + return nil, errors.New("Errors while parsing hostsfile. Try hostess fmt") + } + + return hosts, nil +} + +// SaveOrPreview will display or write the Hostfile +func SaveOrPreview(options *Options, hostfile *hostess.Hostfile) error { + // If -n is passed, no-op and output the resultant hosts file to stdout. + // Otherwise it's for real and we're going to write it. + if options.Preview { + fmt.Printf("%s", hostfile.Format()) + } else { + if err := hostfile.Save(); err != nil { + return fmt.Errorf("Unable to write to %s. Maybe you need to sudo? (error: %s)", hostess.GetHostsPath(), err) + } + } + return nil +} + +// StrPadRight adds spaces to the right of a string until it reaches length. +// If the input string is already that long, do nothing. +func StrPadRight(input string, length int) string { + minimum := len(input) + if length <= minimum { + return input + } + return input + strings.Repeat(" ", length-minimum) +} + +// Add command parses and adds or updates a hostname in the +// hosts file +func Add(options *Options, hostname, ip string) error { + hostsfile, err := LoadHostfile(options) + + newHostname, err := hostess.NewHostname(hostname, ip, true) + if err != nil { + MaybeError(c, fmt.Sprintf("Failed to parse hosts entry: %s", err)) + } + // If the command is aff instead of add then the entry should be disabled + if c.Command.Name == "aff" { + newHostname.Enabled = false + } + + replace := hostsfile.Hosts.ContainsDomain(newHostname.Domain) + // Note that Add() may return an error, but they are informational only. We + // don't actually care what the error is -- we just want to add the + // hostname and save the file. This way the behavior is idempotent. + hostsfile.Hosts.Add(newHostname) + + // If the user passes -n then we'll Add and show the new hosts file, but + // not save it. + if c.Bool("n") || AnyBool(c, "n") { + fmt.Printf("%s", hostsfile.Format()) + } else { + SaveOrPreview(c, hostsfile) + // We'll give a little bit of information about whether we added or + // updated, but if the user wants to know they can use has or ls to + // show the file before they run the operation. Maybe later we can add + // a verbose flag to show more information. + if replace { + fmt.Printf("Updated %s\n", newHostname.FormatHuman()) + } else { + fmt.Printf("Added %s\n", newHostname.FormatHuman()) + } + } +} + +// Remove command removes any hostname(s) matching from the hosts file +func Remove(options *Options, hostname string) error { + hostsfile := LoadHostfile(c) + + found := hostsfile.Hosts.ContainsDomain(hostname) + if found { + hostsfile.Hosts.RemoveDomain(hostname) + if AnyBool(c, "n") { + fmt.Printf("%s", hostsfile.Format()) + } else { + SaveOrPreview(c, hostsfile) + MaybePrintln(c, fmt.Sprintf("Deleted %s", hostname)) + } + } else { + MaybePrintln(c, fmt.Sprintf("%s not found in %s", hostname, hostess.GetHostsPath())) + } +} + +// Has command indicates whether a hostname is present in the hosts file +func Has(options *Options, hostname string) error { + if len(c.Args()) != 1 { + MaybeError(c, "expected ") + } + domain := c.Args()[0] + hostsfile := LoadHostfile(c) + + found := hostsfile.Hosts.ContainsDomain(domain) + if found { + MaybePrintln(c, fmt.Sprintf("Found %s in %s", domain, hostess.GetHostsPath())) + } else { + MaybeError(c, fmt.Sprintf("%s not found in %s", domain, hostess.GetHostsPath())) + } +} + +// OnOff enables (uncomments) or disables (comments) the specified hostname in +// the hosts file. Exits code 1 if the hostname is missing. +func OnOff(options *Options, hostname string) error { + hostsfile := LoadHostfile(c) + + // Switch on / off commands + success := false + if c.Command.Name == "on" { + success = hostsfile.Hosts.Enable(domain) + } else { + success = hostsfile.Hosts.Disable(domain) + } + + if success { + SaveOrPreview(c, hostsfile) + if c.Command.Name == "on" { + MaybePrintln(c, fmt.Sprintf("Enabled %s", domain)) + } else { + MaybePrintln(c, fmt.Sprintf("Disabled %s", domain)) + } + } else { + MaybeError(c, fmt.Sprintf("%s not found in %s", domain, hostess.GetHostsPath())) + } +} + +func Enable(options *Options, hostname string) error { + +} + +func Disable(options *Options, hostname string) error { + +} + +// List command shows a list of hostnames in the hosts file +func List(options *Options) error { + hostsfile := AlwaysLoadHostFile(c) + widestHostname := 0 + widestIP := 0 + for _, hostname := range hostsfile.Hosts { + dlen := len(hostname.Domain) + if dlen > widestHostname { + widestHostname = dlen + } + ilen := len(hostname.IP) + if ilen > widestIP { + widestIP = ilen + } + } + + for _, hostname := range hostsfile.Hosts { + fmt.Printf("%s -> %s %s\n", + StrPadRight(hostname.Domain, widestHostname), + StrPadRight(hostname.IP.String(), widestIP), + hostname.FormatEnabled()) + } +} + +// Format command removes duplicates and conflicts from the hosts file +func Format(options *Options) error { + hostsfile := AlwaysLoadHostFile(c) + if bytes.Equal(hostsfile.GetData(), hostsfile.Format()) { + MaybePrintln(c, fmt.Sprintf("%s is already formatted and contains no dupes or conflicts; nothing to do", hostess.GetHostsPath())) + os.Exit(0) + } + SaveOrPreview(c, hostsfile) +} + +// Dump command outputs hosts file contents as JSON +func Dump(options *Options) error { + hostsfile := AlwaysLoadHostFile(c) + jsonbytes, err := hostsfile.Hosts.Dump() + if err != nil { + MaybeError(c, err.Error()) + } + fmt.Println(fmt.Sprintf("%s", jsonbytes)) +} + +// Apply command adds hostnames to the hosts file from JSON +func Apply(options *Options, filename string) error { + jsonbytes, err := ioutil.ReadFile(filename) + if err != nil { + MaybeError(c, fmt.Sprintf("Unable to read %s: %s", filename, err)) + } + + hostfile := AlwaysLoadHostFile(c) + err = hostfile.Hosts.Apply(jsonbytes) + if err != nil { + MaybeError(c, fmt.Sprintf("Error applying changes to hosts file: %s", err)) + } + + SaveOrPreview(c, hostfile) + MaybePrintln(c, fmt.Sprintf("%s applied", filename)) +} diff --git a/commands/commands_test.go b/commands/commands_test.go new file mode 100644 index 0000000..fd2e468 --- /dev/null +++ b/commands/commands_test.go @@ -0,0 +1,26 @@ +package commands + +import ( + "testing" + ) + +func TestStrPadRight(t *testing.T) { + + type testCase struct { + Expected string + Output string + Name string + } + + cases := []testCase{ + {"", StrPadRight("", 0), "Zero-length no padding"}, + {" ", StrPadRight("", 10), "Zero-length 10 padding"}, + {"string", StrPadRight("string", 0), "6-length 0 padding"}, + } + + for _, test := range cases { + if test.Output != test.Expected { + t.Errorf("Failed case: %s\nExpected %q Found %q", test.Name, test.Expected, test.Output) + } + } +} diff --git a/commands_test.go b/commands_test.go deleted file mode 100644 index ff0e13e..0000000 --- a/commands_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package hostess - -import ( - "flag" - "os" - "testing" - - "github.com/codegangsta/cli" - "github.com/stretchr/testify/assert" -) - -func TestStrPadRight(t *testing.T) { - assert.Equal(t, "", StrPadRight("", 0), "Zero-length no padding") - assert.Equal(t, " ", StrPadRight("", 10), "Zero-length 10 padding") - assert.Equal(t, "string", StrPadRight("string", 0), "6-length 0 padding") -} - -func TestLs(t *testing.T) { - os.Setenv("HOSTESS_PATH", "test-fixtures/hostfile1") - defer os.Setenv("HOSTESS_PATH", "") - c := cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil) - Ls(c) -} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4c95e5d --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/cbednarski/hostess + +go 1.14 diff --git a/hostfile.go b/hostfile.go deleted file mode 100644 index 04ca657..0000000 --- a/hostfile.go +++ /dev/null @@ -1,204 +0,0 @@ -package hostess - -import ( - "fmt" - "io/ioutil" - "os" - "runtime" - "strings" -) - -const defaultOSX = ` -## -# Host Database -# -# localhost is used to configure the loopback interface -# when the system is booting. Do not change this entry. -## - -127.0.0.1 localhost -255.255.255.255 broadcasthost -::1 localhost -fe80::1%lo0 localhost -` - -const defaultLinux = ` -127.0.0.1 localhost -127.0.1.1 HOSTNAME - -# The following lines are desirable for IPv6 capable hosts -::1 localhost ip6-localhost ip6-loopback -fe00::0 ip6-localnet -ff00::0 ip6-mcastprefix -ff02::1 ip6-allnodes -ff02::2 ip6-allrouters -ff02::3 ip6-allhosts -` - -// Hostfile represents /etc/hosts (or a similar file, depending on OS), and -// includes a list of Hostnames. Hostfile includes -type Hostfile struct { - Path string - Hosts Hostlist - data []byte -} - -// NewHostfile creates a new Hostfile object from the specified file. -func NewHostfile() *Hostfile { - return &Hostfile{GetHostsPath(), Hostlist{}, []byte{}} -} - -// GetHostsPath returns the location of the hostfile; either env HOSTESS_PATH -// or /etc/hosts if HOSTESS_PATH is not set. -func GetHostsPath() string { - path := os.Getenv("HOSTESS_PATH") - if path == "" { - if runtime.GOOS == "windows" { - path = "C:\\Windows\\System32\\drivers\\etc\\hosts" - } else { - path = "/etc/hosts" - } - } - return path -} - -// TrimWS (Trim Whitespace) removes space, newline, and tabs from a string -// using strings.Trim() -func TrimWS(s string) string { - return strings.TrimSpace(s) -} - -// ParseLine parses an individual line in a hostfile, which may contain one -// (un)commented ip and one or more hostnames. For example -// -// 127.0.0.1 localhost mysite1 mysite2 -func ParseLine(line string) (Hostlist, error) { - var hostnames Hostlist - - if len(line) == 0 { - return hostnames, fmt.Errorf("line is blank") - } - - // Parse leading # for disabled lines - enabled := true - if line[0:1] == "#" { - enabled = false - line = TrimWS(line[1:]) - } - - // Parse other #s for actual comments - line = strings.Split(line, "#")[0] - - // Replace tabs and multispaces with single spaces throughout - line = strings.Replace(line, "\t", " ", -1) - for strings.Contains(line, " ") { - line = strings.Replace(line, " ", " ", -1) - } - line = TrimWS(line) - - // Break line into words - words := strings.Split(line, " ") - for idx, word := range words { - words[idx] = TrimWS(word) - } - - // Separate the first bit (the ip) from the other bits (the domains) - ip := words[0] - domains := words[1:] - - // if LooksLikeIPv4(ip) || LooksLikeIPv6(ip) { - for _, v := range domains { - hostname, err := NewHostname(v, ip, enabled) - if err != nil { - return nil, err - } - hostnames = append(hostnames, hostname) - } - // } - - return hostnames, nil -} - -// MustParseLine is like ParseLine but panics instead of errors. -func MustParseLine(line string) Hostlist { - hostlist, err := ParseLine(line) - if err != nil { - panic(err) - } - return hostlist -} - -// Parse reads -func (h *Hostfile) Parse() []error { - var errs []error - var line = 1 - for _, v := range strings.Split(string(h.data), "\n") { - hostnames, _ := ParseLine(v) - // if err != nil { - // log.Printf("Error parsing line %d: %s\n", line, err) - // } - for _, hostname := range hostnames { - err := h.Hosts.Add(hostname) - if err != nil { - errs = append(errs, err) - } - } - line++ - } - return errs -} - -// Read the contents of the hostfile from disk -func (h *Hostfile) Read() error { - data, err := ioutil.ReadFile(h.Path) - if err == nil { - h.data = data - } - return err -} - -// LoadHostfile creates a new Hostfile struct and tries to populate it from -// disk. Read and/or parse errors are returned as a slice. -func LoadHostfile() (hostfile *Hostfile, errs []error) { - hostfile = NewHostfile() - readErr := hostfile.Read() - if readErr != nil { - errs = []error{readErr} - return - } - errs = hostfile.Parse() - hostfile.Hosts.Sort() - return -} - -// GetData returns the internal snapshot of the hostfile we read when we loaded -// this hostfile from disk (if we ever did that). This is implemented for -// testing and you probably won't need to use it. -func (h *Hostfile) GetData() []byte { - return h.data -} - -// Format takes the current list of Hostnames in this Hostfile and turns it -// into a string suitable for use as an /etc/hosts file. -// Sorting uses the following logic: -// 1. List is sorted by IP address -// 2. Commented items are left in place -// 3. 127.* appears at the top of the list (so boot resolvers don't break) -// 4. When present, localhost will always appear first in the domain list -func (h *Hostfile) Format() []byte { - return h.Hosts.Format() -} - -// Save writes the Hostfile to disk to /etc/hosts or to the location specified -// by the HOSTESS_PATH environment variable (if set). -func (h *Hostfile) Save() error { - file, err := os.OpenFile(h.Path, os.O_RDWR|os.O_APPEND|os.O_TRUNC, 0644) - if err != nil { - return err - } - - defer file.Close() - _, err = file.Write(h.Format()) - - return err -} diff --git a/hostfile_test.go b/hostfile_test.go deleted file mode 100644 index e144334..0000000 --- a/hostfile_test.go +++ /dev/null @@ -1,202 +0,0 @@ -package hostess_test - -import ( - "fmt" - "runtime" - "strings" - "testing" - - "github.com/cbednarski/hostess" -) - -const ipv4Pass = ` -127.0.0.1 -127.0.1.1 -10.200.30.50 -99.99.99.99 -999.999.999.999 -0.1.1.0 -` - -const ipv4Fail = ` -1234.1.1.1 -123.5.6 -12.12 -76.76.67.67.45 -` - -const ipv6 = `` -const domain = "localhost" -const ip = "127.0.0.1" -const enabled = true - -func Diff(expected, actual string) string { - return fmt.Sprintf(` ----- Expected ---- -%s ------ Actual ----- -%s -`, expected, actual) -} - -func TestGetHostsPath(t *testing.T) { - path := hostess.GetHostsPath() - var expected string - if runtime.GOOS == "windows" { - expected = "C:\\Windows\\System32\\drivers\\etc\\hosts" - } else { - expected = "/etc/hosts" - } - if path != expected { - t.Error("Hosts path should be " + expected) - } -} - -func TestFormatHostfile(t *testing.T) { - // The sort order here is a bit weird. - // 1. We want localhost entries at the top - // 2. The rest are sorted by IP as STRINGS, not numeric values, so 10 - // precedes 8 - const expected = `127.0.0.1 localhost devsite -127.0.1.1 ip-10-37-12-18 -# 8.8.8.8 google.com -10.37.12.18 devsite.com m.devsite.com -` - - hostfile := hostess.NewHostfile() - hostfile.Path = "./hosts" - hostfile.Hosts.Add(hostess.MustHostname("localhost", "127.0.0.1", true)) - hostfile.Hosts.Add(hostess.MustHostname("ip-10-37-12-18", "127.0.1.1", true)) - hostfile.Hosts.Add(hostess.MustHostname("devsite", "127.0.0.1", true)) - hostfile.Hosts.Add(hostess.MustHostname("google.com", "8.8.8.8", false)) - hostfile.Hosts.Add(hostess.MustHostname("devsite.com", "10.37.12.18", true)) - hostfile.Hosts.Add(hostess.MustHostname("m.devsite.com", "10.37.12.18", true)) - f := string(hostfile.Format()) - if f != expected { - t.Errorf("Hostfile output is not formatted correctly: %s", Diff(expected, f)) - } -} - -func TestTrimWS(t *testing.T) { - const expected = ` candy - - ` - actual := hostess.TrimWS(expected) - if actual != "candy" { - t.Errorf("Output was not trimmed correctly: %s", Diff(expected, actual)) - } -} - -func TestParseLineBlank(t *testing.T) { - // Blank line - hosts, err := hostess.ParseLine("") - expected := "line is blank" - if err.Error() != expected { - t.Errorf("Expected error %q; found %q", expected, err.Error()) - } - if len(hosts) > 0 { - t.Error("Expected to find zero hostnames") - } -} - -func TestParseLineComment(t *testing.T) { - // Comment - hosts, err := hostess.ParseLine("# The following lines are desirable for IPv6 capable hosts") - if err == nil { - t.Error(err) - } - if len(hosts) > 0 { - t.Error("Expected to find zero hostnames") - } -} - -func TestParseLineOneWordComment(t *testing.T) { - // Single word comment - hosts, err := hostess.ParseLine("#blah") - if err != nil { - t.Error(err) - } - if len(hosts) > 0 { - t.Error("Expected to find zero hostnames") - } -} - -func TestParseLineBasicHostnameComment(t *testing.T) { - hosts, err := hostess.ParseLine("#66.33.99.11 test.domain.com") - if err != nil { - t.Error(err) - } - if !hosts.Contains(hostess.MustHostname("test.domain.com", "66.33.99.11", false)) || - len(hosts) != 1 { - t.Error("Expected to find test.domain.com (disabled)") - } -} - -func TestParseLineMultiHostnameComment(t *testing.T) { - hosts, err := hostess.ParseLine("# 66.33.99.11 test.domain.com domain.com") - if err != nil { - t.Error(err) - } - if !hosts.Contains(hostess.MustHostname("test.domain.com", "66.33.99.11", false)) || - !hosts.Contains(hostess.MustHostname("domain.com", "66.33.99.11", false)) || - len(hosts) != 2 { - t.Error("Expected to find domain.com and test.domain.com (disabled)") - t.Errorf("Found %+v", hosts) - } -} - -func TestParseLineMultiHostname(t *testing.T) { - // Not Commented stuff - hosts, err := hostess.ParseLine("255.255.255.255 broadcasthost test.domain.com domain.com") - if err != nil { - t.Error(err) - } - if !hosts.Contains(hostess.MustHostname("broadcasthost", "255.255.255.255", true)) || - !hosts.Contains(hostess.MustHostname("test.domain.com", "255.255.255.255", true)) || - !hosts.Contains(hostess.MustHostname("domain.com", "255.255.255.255", true)) || - len(hosts) != 3 { - t.Error("Expected to find broadcasthost, domain.com, and test.domain.com (enabled)") - } -} - -func TestParseLineIPv6A(t *testing.T) { - // Ipv6 stuff - hosts, err := hostess.ParseLine("::1 localhost") - if err != nil { - t.Error(err) - } - if !hosts.Contains(hostess.MustHostname("localhost", "::1", true)) || - len(hosts) != 1 { - t.Error("Expected to find localhost ipv6 (enabled)") - } -} - -func TestParseLineIPv6B(t *testing.T) { - hosts, err := hostess.ParseLine("ff02::1 ip6-allnodes") - if err != nil { - t.Error(err) - } - if !hosts.Contains(hostess.MustHostname("ip6-allnodes", "ff02::1", true)) || - len(hosts) != 1 { - t.Error("Expected to find ip6-allnodes ipv6 (enabled)") - } -} - -func TestLoadHostfile(t *testing.T) { - hostfile := hostess.NewHostfile() - hostfile.Read() - if !strings.Contains(string(hostfile.GetData()), domain) { - t.Errorf("Expected to find %s", domain) - } - - hostfile.Parse() - on := enabled - if runtime.GOOS == "windows" { - on = false - } - hostname := hostess.MustHostname(domain, ip, on) - found := hostfile.Hosts.Contains(hostname) - if !found { - t.Errorf("Expected to find %#v", hostname) - } -} diff --git a/hostlist.go b/hostlist.go deleted file mode 100644 index 2b0c145..0000000 --- a/hostlist.go +++ /dev/null @@ -1,437 +0,0 @@ -package hostess - -import ( - "encoding/json" - "errors" - "fmt" - "net" - "sort" - "strings" -) - -// ErrInvalidVersionArg is raised when a function expects IPv 4 or 6 but is -// passed a value not 4 or 6. -var ErrInvalidVersionArg = errors.New("Version argument must be 4 or 6") - -// Hostlist is a sortable set of Hostnames. When in a Hostlist, Hostnames must -// follow some rules: -// -// - Hostlist may contain IPv4 AND IPv6 ("IP version" or "IPv") Hostnames. -// - Names are only allowed to overlap if IP version is different. -// - Adding a Hostname for an existing name will replace the old one. -// -// The Hostlist uses a deterministic Sort order designed to make a hostfile -// output look a particular way. Generally you don't need to worry about this -// as Sort will be called automatically before Format. However, the Hostlist -// may or may not be sorted at any particular time during runtime. -// -// See the docs and implementation in Sort and Add for more details. -type Hostlist []*Hostname - -// NewHostlist initializes a new Hostlist -func NewHostlist() *Hostlist { - return &Hostlist{} -} - -// Len returns the number of Hostnames in the list, part of sort.Interface -func (h Hostlist) Len() int { - return len(h) -} - -// MakeSurrogateIP takes an IP like 127.0.0.1 and munges it to 0.0.0.1 so we can -// sort it more easily. Note that we don't actually want to change the value, -// so we use value copies here (not pointers). -func MakeSurrogateIP(IP net.IP) net.IP { - if len(IP.String()) > 3 && IP.String()[0:3] == "127" { - return net.ParseIP("0" + IP.String()[3:]) - } - return IP -} - -// Less determines the sort order of two Hostnames, part of sort.Interface -func (h Hostlist) Less(A, B int) bool { - // Sort IPv4 before IPv6 - // A is IPv4 and B is IPv6. A wins! - if !h[A].IPv6 && h[B].IPv6 { - return true - } - // A is IPv6 but B is IPv4. A loses! - if h[A].IPv6 && !h[B].IPv6 { - return false - } - - // Sort "localhost" at the top - if h[A].Domain == "localhost" { - return true - } - if h[B].Domain == "localhost" { - return false - } - - // Compare the the IP addresses (byte array) - // We want to push 127. to the top so we're going to mark it zero. - surrogateA := MakeSurrogateIP(h[A].IP) - surrogateB := MakeSurrogateIP(h[B].IP) - if !surrogateA.Equal(surrogateB) { - for charIndex := range surrogateA { - // A and B's IPs differ at this index, and A is less. A wins! - if surrogateA[charIndex] < surrogateB[charIndex] { - return true - } - // A and B's IPs differ at this index, and B is less. A loses! - if surrogateA[charIndex] > surrogateB[charIndex] { - return false - } - } - // If we got here then the IPs are the same and we want to continue on - // to the domain sorting section. - } - - // Prep for sorting by domain name - aLength := len(h[A].Domain) - bLength := len(h[B].Domain) - max := aLength - if bLength > max { - max = bLength - } - - // Sort domains alphabetically - // TODO: This works best if domains are lowercased. However, we do not - // enforce lowercase because of UTF-8 domain names, which may be broken by - // case folding. There is a way to do this correctly but it's complicated - // so I'm not going to do it right now. - for charIndex := 0; charIndex < max; charIndex++ { - // This index is longer than A, so A is shorter. A wins! - if charIndex >= aLength { - return true - } - // This index is longer than B, so B is shorter. A loses! - if charIndex >= bLength { - return false - } - // A and B differ at this index and A is less. A wins! - if h[A].Domain[charIndex] < h[B].Domain[charIndex] { - return true - } - // A and B differ at this index and B is less. A loses! - if h[A].Domain[charIndex] > h[B].Domain[charIndex] { - return false - } - } - - // If we got here then A and B are the same -- by definition A is not Less - // than B so we return false. Technically we shouldn't get here since Add - // should not allow duplicates, but we'll guard anyway. - return false -} - -// Swap changes the position of two Hostnames, part of sort.Interface -func (h Hostlist) Swap(i, j int) { - h[i], h[j] = h[j], h[i] -} - -// Sort this list of Hostnames, according to Hostlist sorting rules: -// -// 1. localhost comes before other domains -// 2. IPv4 comes before IPv6 -// 3. IPs are sorted in numerical order -// 4. domains are sorted in alphabetical -func (h *Hostlist) Sort() { - sort.Sort(*h) -} - -// Contains returns true if this Hostlist has the specified Hostname -func (h *Hostlist) Contains(b *Hostname) bool { - for _, a := range *h { - if a.Equal(b) { - return true - } - } - return false -} - -// ContainsDomain returns true if a Hostname in this Hostlist matches domain -func (h *Hostlist) ContainsDomain(domain string) bool { - for _, hostname := range *h { - if hostname.Domain == domain { - return true - } - } - return false -} - -// ContainsIP returns true if a Hostname in this Hostlist matches IP -func (h *Hostlist) ContainsIP(IP net.IP) bool { - for _, hostname := range *h { - if hostname.EqualIP(IP) { - return true - } - } - return false -} - -// Add a new Hostname to this hostlist. Add uses some merging logic in the -// event it finds duplicated hostnames. In the case of a conflict (incompatible -// entries) the last write wins. In the case of duplicates, duplicates will be -// removed and the remaining entry will be enabled if any of the duplicates was -// enabled. -// -// Both duplicate and conflicts return errors so you are aware of them, but you -// don't necessarily need to do anything about the error. -func (h *Hostlist) Add(hostnamev *Hostname) error { - hostname, err := NewHostname(hostnamev.Domain, hostnamev.IP.String(), hostnamev.Enabled) - if err != nil { - return err - } - for index, found := range *h { - if found.Equal(hostname) { - // If either hostname is enabled we will set the existing one to - // enabled state. That way if we add a hostname from the end of a - // hosts file it will take over, and if we later add a disabled one - // the original one will stick. We still error in this case so the - // user can see that there is a duplicate. - (*h)[index].Enabled = found.Enabled || hostname.Enabled - return fmt.Errorf("Duplicate hostname entry for %s -> %s", - hostname.Domain, hostname.IP) - } else if found.Domain == hostname.Domain && found.IPv6 == hostname.IPv6 { - (*h)[index] = hostname - return fmt.Errorf("Conflicting hostname entries for %s -> %s and -> %s", - hostname.Domain, hostname.IP, found.IP) - } - } - *h = append(*h, hostname) - return nil -} - -// IndexOf will indicate the index of a Hostname in Hostlist, or -1 if it is -// not found. -func (h *Hostlist) IndexOf(host *Hostname) int { - for index, found := range *h { - if found.Equal(host) { - return index - } - } - return -1 -} - -// IndexOfDomainV will indicate the index of a Hostname in Hostlist that has -// the same domain and IP version, or -1 if it is not found. -// -// This function will panic if IP version is not 4 or 6. -func (h *Hostlist) IndexOfDomainV(domain string, version int) int { - if version != 4 && version != 6 { - panic(ErrInvalidVersionArg) - } - for index, hostname := range *h { - if hostname.Domain == domain && hostname.IPv6 == (version == 6) { - return index - } - } - return -1 -} - -// Remove will delete the Hostname at the specified index. If index is out of -// bounds (i.e. -1), Remove silently no-ops. Remove returns the number of items -// removed (0 or 1). -func (h *Hostlist) Remove(index int) int { - if index > -1 && index < len(*h) { - *h = append((*h)[:index], (*h)[index+1:]...) - return 1 - } - return 0 -} - -// RemoveDomain removes both IPv4 and IPv6 Hostname entries matching domain. -// Returns the number of entries removed. -func (h *Hostlist) RemoveDomain(domain string) int { - return h.RemoveDomainV(domain, 4) + h.RemoveDomainV(domain, 6) -} - -// RemoveDomainV removes a Hostname entry matching the domain and IP version. -func (h *Hostlist) RemoveDomainV(domain string, version int) int { - return h.Remove(h.IndexOfDomainV(domain, version)) -} - -// Enable will change any Hostnames matching domain to be enabled. -func (h *Hostlist) Enable(domain string) bool { - found := false - for _, hostname := range *h { - if hostname.Domain == domain { - hostname.Enabled = true - found = true - } - } - return found -} - -// EnableV will change a Hostname matching domain and IP version to be enabled. -// -// This function will panic if IP version is not 4 or 6. -func (h *Hostlist) EnableV(domain string, version int) bool { - found := false - if version != 4 && version != 6 { - panic(ErrInvalidVersionArg) - } - for _, hostname := range *h { - if hostname.Domain == domain && hostname.IPv6 == (version == 6) { - hostname.Enabled = true - found = true - } - } - return found -} - -// Disable will change any Hostnames matching domain to be disabled. -func (h *Hostlist) Disable(domain string) bool { - found := false - for _, hostname := range *h { - if hostname.Domain == domain { - hostname.Enabled = false - found = true - } - } - return found -} - -// DisableV will change any Hostnames matching domain and IP version to be disabled. -// -// This function will panic if IP version is not 4 or 6. -func (h *Hostlist) DisableV(domain string, version int) bool { - found := false - if version != 4 && version != 6 { - panic(ErrInvalidVersionArg) - } - for _, hostname := range *h { - if hostname.Domain == domain && hostname.IPv6 == (version == 6) { - hostname.Enabled = false - found = true - } - } - return found -} - -// FilterByIP filters the list of hostnames by IP address. -func (h *Hostlist) FilterByIP(IP net.IP) (hostnames []*Hostname) { - for _, hostname := range *h { - if hostname.IP.Equal(IP) { - hostnames = append(hostnames, hostname) - } - } - return -} - -// FilterByDomain filters the list of hostnames by Domain. -func (h *Hostlist) FilterByDomain(domain string) (hostnames []*Hostname) { - for _, hostname := range *h { - if hostname.Domain == domain { - hostnames = append(hostnames, hostname) - } - } - return -} - -// FilterByDomainV filters the list of hostnames by domain and IPv4 or IPv6. -// This should never contain more than one item, but returns a list for -// consistency with other filter functions. -// -// This function will panic if IP version is not 4 or 6. -func (h *Hostlist) FilterByDomainV(domain string, version int) (hostnames []*Hostname) { - if version != 4 && version != 6 { - panic(ErrInvalidVersionArg) - } - for _, hostname := range *h { - if hostname.Domain == domain && hostname.IPv6 == (version == 6) { - hostnames = append(hostnames, hostname) - } - } - return -} - -// GetUniqueIPs extracts an ordered list of unique IPs from the Hostlist. -// This calls Sort() internally. -func (h *Hostlist) GetUniqueIPs() []net.IP { - h.Sort() - // A map doesn't preserve order so we're going to use the map to check - // whether we've seen something and use the list to keep track of the - // order. - seen := make(map[string]bool) - inOrder := []net.IP{} - - for _, hostname := range *h { - key := (*hostname).IP.String() - if !seen[key] { - seen[key] = true - inOrder = append(inOrder, (*hostname).IP) - } - } - return inOrder -} - -// Format takes the current list of Hostnames in this Hostfile and turns it -// into a string suitable for use as an /etc/hosts file. -// Sorting uses the following logic: -// -// 1. List is sorted by IP address -// 2. Commented items are sorted displayed -// 3. 127.* appears at the top of the list (so boot resolvers don't break) -// 4. When present, "localhost" will always appear first in the domain list -func (h *Hostlist) Format() []byte { - h.Sort() - out := []byte{} - - // We want to output one line of hostnames per IP, so first we get that - // list of IPs and iterate. - for _, IP := range h.GetUniqueIPs() { - // Technically if an IP has some disabled hostnames we'll show two - // lines, one starting with a comment (#). - enabledIPs := []string{} - disabledIPs := []string{} - - // For this IP, get all hostnames that match and iterate over them. - for _, hostname := range h.FilterByIP(IP) { - // If it's enabled, put it in the enabled bucket (likewise for - // disabled hostnames) - if hostname.Enabled { - enabledIPs = append(enabledIPs, hostname.Domain) - } else { - disabledIPs = append(disabledIPs, hostname.Domain) - } - } - - // Finally, if the bucket contains anything, concatenate it all - // together and append it to the output. Also add a newline. - if len(enabledIPs) > 0 { - concat := fmt.Sprintf("%s %s", IP.String(), strings.Join(enabledIPs, " ")) - out = append(out, []byte(concat)...) - out = append(out, []byte("\n")...) - } - - if len(disabledIPs) > 0 { - concat := fmt.Sprintf("# %s %s", IP.String(), strings.Join(disabledIPs, " ")) - out = append(out, []byte(concat)...) - out = append(out, []byte("\n")...) - } - } - - return out -} - -// Dump exports all entries in the Hostlist as JSON -func (h *Hostlist) Dump() ([]byte, error) { - return json.MarshalIndent(h, "", " ") -} - -// Apply imports all entries from the JSON input to this Hostlist -func (h *Hostlist) Apply(jsonbytes []byte) error { - var hostnames Hostlist - err := json.Unmarshal(jsonbytes, &hostnames) - if err != nil { - return err - } - - for _, hostname := range hostnames { - h.Add(hostname) - } - - return nil -} diff --git a/hostlist_test.go b/hostlist_test.go deleted file mode 100644 index 311464a..0000000 --- a/hostlist_test.go +++ /dev/null @@ -1,243 +0,0 @@ -package hostess_test - -import ( - "bytes" - "fmt" - "net" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/cbednarski/hostess" -) - -func TestAddDuplicate(t *testing.T) { - list := hostess.NewHostlist() - - hostname := hostess.MustHostname("mysite", "1.2.3.4", false) - err := list.Add(hostname) - assert.Nil(t, err, "Expected no errors when adding a hostname for the first time") - - hostname.Enabled = true - err = list.Add(hostname) - assert.NotNil(t, err, "Expected error when adding a duplicate") - assert.True(t, (*list)[0].Enabled, "Expected hostname to be in enabled state") -} - -func TestAddConflict(t *testing.T) { - hostnameA := hostess.MustHostname("mysite", "1.2.3.4", true) - hostnameB := hostess.MustHostname("mysite", "5.2.3.4", false) - - list := hostess.NewHostlist() - list.Add(hostnameA) - err := list.Add(hostnameB) - assert.NotNil(t, err, "Expected conflict error") - - if !(*list)[0].Equal(hostnameB) { - t.Error("Expected second hostname to overwrite") - } - if (*list)[0].Enabled { - t.Error("Expected second hostname to be disabled") - } -} - -func TestMakeSurrogateIP(t *testing.T) { - original := net.ParseIP("127.0.0.1") - expected1 := net.ParseIP("0.0.0.1") - IP1 := hostess.MakeSurrogateIP(original) - if !IP1.Equal(expected1) { - t.Errorf("Expected %s to convert to %s; got %s", original, expected1, IP1) - } - - expected2 := net.ParseIP("10.20.30.40") - IP2 := hostess.MakeSurrogateIP(expected2) - if !IP2.Equal(expected2) { - t.Errorf("Expected %s to remain unchanged; got %s", expected2, IP2) - } -} - -func TestContainsDomainIp(t *testing.T) { - hosts := hostess.NewHostlist() - hosts.Add(hostess.MustHostname(domain, ip, false)) - hosts.Add(hostess.MustHostname("google.com", "8.8.8.8", true)) - - if !hosts.ContainsDomain(domain) { - t.Errorf("Expected to find %s", domain) - } - - const extraneousDomain = "yahoo.com" - if hosts.ContainsDomain(extraneousDomain) { - t.Errorf("Did not expect to find %s", extraneousDomain) - } - - var expectedIP = net.ParseIP(ip) - if !hosts.ContainsIP(expectedIP) { - t.Errorf("Expected to find %s", ip) - } - - var extraneousIP = net.ParseIP("1.2.3.4") - if hosts.ContainsIP(extraneousIP) { - t.Errorf("Did not expect to find %s", extraneousIP) - } - - expectedHostname := hostess.MustHostname(domain, ip, true) - if !hosts.Contains(expectedHostname) { - t.Errorf("Expected to find %+v", expectedHostname) - } - - extraneousHostname := hostess.MustHostname("yahoo.com", "4.3.2.1", false) - if hosts.Contains(extraneousHostname) { - t.Errorf("Did not expect to find %+v", extraneousHostname) - } -} - -func TestFormat(t *testing.T) { - hosts := hostess.NewHostlist() - hosts.Add(hostess.MustHostname(domain, ip, false)) - hosts.Add(hostess.MustHostname("google.com", "8.8.8.8", true)) - - expected := `# 127.0.0.1 localhost -8.8.8.8 google.com -` - if string(hosts.Format()) != expected { - t.Error("Formatted hosts list is not formatted correctly") - } -} - -func TestRemove(t *testing.T) { - hosts := hostess.NewHostlist() - hosts.Add(hostess.MustHostname(domain, ip, false)) - hosts.Add(hostess.MustHostname("google.com", "8.8.8.8", true)) - - removed := hosts.Remove(1) - if removed != 1 { - t.Error("Expected to remove 1 item") - } - if len(*hosts) > 1 { - t.Errorf("Expected hostlist to have 1 item, found %d", len(*hosts)) - } - if hosts.ContainsDomain("google.com") { - t.Errorf("Expected not to find google.com") - } - - hosts.Add(hostess.MustHostname(domain, "::1", enabled)) - removed = hosts.RemoveDomain(domain) - if removed != 2 { - t.Error("Expected to remove 2 items") - } -} - -func TestRemoveDomain(t *testing.T) { - hosts := hostess.NewHostlist() - h1 := hostess.MustHostname("google.com", "127.0.0.1", false) - h2 := hostess.MustHostname("google.com", "::1", true) - hosts.Add(h1) - hosts.Add(h2) - - hosts.RemoveDomainV("google.com", 4) - if hosts.Contains(h1) { - t.Error("Should not contain ipv4 hostname") - } - if !hosts.Contains(h2) { - t.Error("Should still contain ipv6 hostname") - } - - hosts.RemoveDomainV("google.com", 6) - if len(*hosts) != 0 { - t.Error("Should no longer contain any hostnames") - } -} - -func CheckIndexDomain(t *testing.T, index int, domain string, hosts *hostess.Hostlist) { - if (*hosts)[index].Domain != domain { - t.Errorf("Expected %s to be in position %d. Found: %s", domain, index, (*hosts)[index].FormatHuman()) - } -} - -func TestSort(t *testing.T) { - // Getting 100% coverage on this is kinda tricky. It's pretty close and - // this is already too long. - - hosts := hostess.NewHostlist() - hosts.Add(hostess.MustHostname("google.com", "8.8.8.8", true)) - hosts.Add(hostess.MustHostname("google3.com", "::1", true)) - hosts.Add(hostess.MustHostname(domain, ip, false)) - hosts.Add(hostess.MustHostname("google2.com", "8.8.4.4", true)) - hosts.Add(hostess.MustHostname("blah2", "10.20.1.1", true)) - hosts.Add(hostess.MustHostname("blah3", "10.20.1.1", true)) - hosts.Add(hostess.MustHostname("blah33", "10.20.1.1", true)) - hosts.Add(hostess.MustHostname("blah", "10.20.1.1", true)) - hosts.Add(hostess.MustHostname("hostname", "127.0.1.1", true)) - hosts.Add(hostess.MustHostname("devsite", "127.0.0.1", true)) - - hosts.Sort() - - CheckIndexDomain(t, 0, "localhost", hosts) - CheckIndexDomain(t, 1, "devsite", hosts) - CheckIndexDomain(t, 2, "hostname", hosts) - CheckIndexDomain(t, 3, "google2.com", hosts) - CheckIndexDomain(t, 4, "google.com", hosts) - CheckIndexDomain(t, 5, "blah", hosts) - CheckIndexDomain(t, 6, "blah2", hosts) - CheckIndexDomain(t, 7, "blah3", hosts) - CheckIndexDomain(t, 8, "blah33", hosts) - CheckIndexDomain(t, 9, "google3.com", hosts) -} - -func ExampleHostlist() { - hosts := hostess.NewHostlist() - hosts.Add(hostess.MustHostname("google.com", "127.0.0.1", false)) - hosts.Add(hostess.MustHostname("google.com", "::1", true)) - - fmt.Printf("%s\n", hosts.Format()) - // Output: - // # 127.0.0.1 google.com - // ::1 google.com -} - -const hostsjson = `[ - { - "domain": "google.com", - "ip": "127.0.0.1", - "enabled": false - }, - { - "domain": "google.com", - "ip": "::1", - "enabled": true - } -]` - -func TestDump(t *testing.T) { - hosts := hostess.NewHostlist() - hosts.Add(hostess.MustHostname("google.com", "127.0.0.1", false)) - hosts.Add(hostess.MustHostname("google.com", "::1", true)) - - expected := []byte(hostsjson) - actual, _ := hosts.Dump() - - if !bytes.Equal(actual, expected) { - t.Errorf("JSON output did not match expected output: %s", Diff(string(expected), string(actual))) - } - -} - -func TestApply(t *testing.T) { - hosts := hostess.NewHostlist() - hosts.Apply([]byte(hostsjson)) - - hostnameA := hostess.MustHostname("google.com", "127.0.0.1", false) - if !hosts.Contains(hostnameA) { - t.Errorf("Expected to find %s", hostnameA.Format()) - } - - hostnameB := hostess.MustHostname("google.com", "::1", true) - if !hosts.Contains(hostnameB) { - t.Errorf("Expected to find %s", hostnameB.Format()) - } - - hosts.Apply([]byte(hostsjson)) - if hosts.Len() != 2 { - t.Error("Hostslist contains the wrong number of items, expected 2") - } -} diff --git a/hostname.go b/hostname.go deleted file mode 100644 index 687f3b2..0000000 --- a/hostname.go +++ /dev/null @@ -1,101 +0,0 @@ -package hostess - -import ( - "fmt" - "net" - "regexp" - "strings" -) - -var ipv4Pattern = regexp.MustCompile(`^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$`) -var ipv6Pattern = regexp.MustCompile(`^[(a-fA-F0-9){1-4}:]+$`) - -// LooksLikeIPv4 returns true if the IP looks like it's IPv4. This does not -// validate whether the string is a valid IP address. -func LooksLikeIPv4(ip string) bool { - return ipv4Pattern.MatchString(ip) -} - -// LooksLikeIPv6 returns true if the IP looks like it's IPv6. This does not -// validate whether the string is a valid IP address. -func LooksLikeIPv6(ip string) bool { - if !strings.Contains(ip, ":") { - return false - } - return ipv6Pattern.MatchString(ip) -} - -// Hostname represents a hosts file entry, including a Domain, IP, whether the -// Hostname is enabled (uncommented in the hosts file), and whether the IP is -// in the IPv6 format. You should always create these with NewHostname(). Note: -// when using Hostnames in the context of a Hostlist, you should not change the -// Hostname fields except through the Hostlist's aggregate methods. Doing so -// can cause unexpected behavior. Instead, use Hostlist's Add, Remove, Enable, -// and Disable methods. -type Hostname struct { - Domain string `json:"domain"` - IP net.IP `json:"ip"` - Enabled bool `json:"enabled"` - IPv6 bool `json:"-"` -} - -// NewHostname creates a new Hostname struct and automatically sets the IPv6 -// field based on the IP you pass in. -func NewHostname(domain, ip string, enabled bool) (*Hostname, error) { - if !LooksLikeIPv4(ip) && !LooksLikeIPv6(ip) { - return nil, fmt.Errorf("Unable to parse IP address %q", ip) - } - IP := net.ParseIP(ip) - return &Hostname{domain, IP, enabled, LooksLikeIPv6(ip)}, nil -} - -// MustHostname calls NewHostname but panics if there is an error parsing it. -func MustHostname(domain, ip string, enabled bool) *Hostname { - hostname, err := NewHostname(domain, ip, enabled) - if err != nil { - panic(err) - } - return hostname -} - -// Equal compares two Hostnames. Note that only the Domain and IP fields are -// compared because Enabled is transient state, and IPv6 should be set -// automatically based on IP. -func (h *Hostname) Equal(n *Hostname) bool { - return h.Domain == n.Domain && h.IP.Equal(n.IP) -} - -// EqualIP compares an IP against this Hostname. -func (h *Hostname) EqualIP(ip net.IP) bool { - return h.IP.Equal(ip) -} - -// IsValid does a spot-check on the domain and IP to make sure they aren't blank -func (h *Hostname) IsValid() bool { - return h.Domain != "" && h.IP != nil -} - -// Format outputs the Hostname as you'd see it in a hosts file, with a comment -// if it is disabled. E.g. -// # 127.0.0.1 blah.example.com -func (h *Hostname) Format() string { - r := fmt.Sprintf("%s %s", h.IP.String(), h.Domain) - if !h.Enabled { - r = "# " + r - } - return r -} - -// FormatEnabled displays Hostname.Enabled as (On) or (Off) -func (h *Hostname) FormatEnabled() string { - if h.Enabled { - return "(On)" - } - return "(Off)" -} - -// FormatHuman outputs the Hostname in a more human-readable format: -// blah.example.com -> 127.0.0.1 (Off) -func (h *Hostname) FormatHuman() string { - return fmt.Sprintf("%s -> %s %s", h.Domain, h.IP, h.FormatEnabled()) -} diff --git a/hostname_test.go b/hostname_test.go deleted file mode 100644 index f64dffa..0000000 --- a/hostname_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package hostess_test - -import ( - "net" - "testing" - - "github.com/cbednarski/hostess" -) - -func TestHostname(t *testing.T) { - h := hostess.MustHostname(domain, ip, enabled) - - if h.Domain != domain { - t.Errorf("Domain should be %s", domain) - } - if !h.IP.Equal(net.ParseIP(ip)) { - t.Errorf("IP should be %s", ip) - } - if h.Enabled != enabled { - t.Errorf("Enabled should be %t", enabled) - } -} - -func TestEqual(t *testing.T) { - a := hostess.MustHostname("localhost", "127.0.0.1", true) - b := hostess.MustHostname("localhost", "127.0.0.1", false) - c := hostess.MustHostname("localhost", "127.0.1.1", false) - - if !a.Equal(b) { - t.Errorf("%+v and %+v should be equal", a, b) - } - if a.Equal(c) { - t.Errorf("%+v and %+v should not be equal", a, c) - } -} - -func TestEqualIP(t *testing.T) { - a := hostess.MustHostname("localhost", "127.0.0.1", true) - c := hostess.MustHostname("localhost", "127.0.1.1", false) - ip := net.ParseIP("127.0.0.1") - - if !a.EqualIP(ip) { - t.Errorf("%s and %s should be equal", a.IP, ip) - } - if a.EqualIP(c.IP) { - t.Errorf("%s and %s should not be equal", a.IP, c.IP) - } -} - -func TestIsValid(t *testing.T) { - hostname := &hostess.Hostname{ - Domain: "localhost", - IP: net.ParseIP("127.0.0.1"), - Enabled: true, - IPv6: true, - } - if !hostname.IsValid() { - t.Fatalf("%+v should be a valid hostname", hostname) - } -} - -func TestIsValidBlank(t *testing.T) { - hostname := &hostess.Hostname{ - Domain: "", - IP: net.ParseIP("127.0.0.1"), - Enabled: true, - IPv6: true, - } - if hostname.IsValid() { - t.Errorf("%+v should be invalid because the name is blank", hostname) - } -} -func TestIsValidBadIP(t *testing.T) { - hostname := &hostess.Hostname{ - Domain: "localhost", - IP: net.ParseIP("localhost"), - Enabled: true, - IPv6: true, - } - if hostname.IsValid() { - t.Errorf("%+v should be invalid because the ip is malformed", hostname) - } -} - -func TestFormatHostname(t *testing.T) { - hostname := hostess.MustHostname(domain, ip, enabled) - - const exp_enabled = "127.0.0.1 localhost" - if hostname.Format() != exp_enabled { - t.Errorf("Hostname format doesn't match desired output: %s", Diff(hostname.Format(), exp_enabled)) - } - - hostname.Enabled = false - const exp_disabled = "# 127.0.0.1 localhost" - if hostname.Format() != exp_disabled { - t.Errorf("Hostname format doesn't match desired output: %s", Diff(hostname.Format(), exp_disabled)) - } -} - -func TestFormatEnabled(t *testing.T) { - hostname := hostess.MustHostname(domain, ip, enabled) - const expectedOn = "(On)" - if hostname.FormatEnabled() != expectedOn { - t.Errorf("Expected hostname to be turned %s", expectedOn) - } - const expectedHumanOn = "localhost -> 127.0.0.1 (On)" - if hostname.FormatHuman() != expectedHumanOn { - t.Errorf("Unexpected output%s", Diff(expectedHumanOn, hostname.FormatHuman())) - } - - hostname.Enabled = false - if hostname.FormatEnabled() != "(Off)" { - t.Error("Expected hostname to be turned (Off)") - } -} diff --git a/main.go b/main.go new file mode 100644 index 0000000..c8a30de --- /dev/null +++ b/main.go @@ -0,0 +1,162 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "os" + + "github.com/cbednarski/hostess/commands" + "github.com/cbednarski/hostess/hostess" +) + +const help = `An idempotent tool for managing %s + +Commands + + fmt Reformat the hosts file + + add Add or overwrite a hosts entry + rm Remote a hosts entry + on Enable a hosts entry + off Disable a hosts entry + + ls List hosts entries + has Exit 0 if entry present in hosts file, 1 if not + + dump Export hosts entries as JSON + apply Import hosts entries from JSON + + All commands that change the hosts file will implicitly reformat it. + +Flags + + -n will preview changes but not rewrite your hosts file + -f force changes even if there are errors parsing the hosts file + -4 limit changes to IPv4 entries + -6 limit changes to IPv6 entries + +Configuration + + HOSTESS_FMT may be set to unix or windows to force that platform's syntax + HOSTESS_PATH may be set to point to a file other than the platform default + +About + + Copyright 2015-2020 Chris Bednarski ; MIT Licensed + Portions Copyright the Go authors, licensed under BSD-style license + Bugs and updates via https://github.com/cbednarski/hostess +` + +var ( + Version = "dev" + ErrInvalidCommand = errors.New("invalid command") +) + +func ExitWithError(err error) { + if err != nil { + os.Stderr.WriteString(err.Error()) + os.Stderr.WriteString("\n") + os.Exit(1) + } +} + +func CommandUsage(command string) error { + return fmt.Errorf("Usage: %s %s ", os.Args[0], command) +} + +func wrappedMain() error { + cli := flag.NewFlagSet(os.Args[0], flag.ExitOnError) + ipv4 := cli.Bool("4",false, "IPv4") + ipv6 := cli.Bool("6",false, "IPv6") + preview := cli.Bool("n",false, "preview") + force := cli.Bool("f",false, "force") + cli.Usage = func() { + fmt.Printf(help, hostess.GetHostsPath()) + } + + if err := cli.Parse(os.Args[2:]); err != nil { + return err + } + + options := &commands.Options{ + IPVersion: 0, + Preview: *preview, + Force: *force, + } + if *ipv4 { + options.IPVersion = options.IPVersion|commands.IPv4 + } + if *ipv6 { + options.IPVersion = options.IPVersion|commands.IPv6 + } + + if len(os.Args) == 1 { + cli.Usage() + return nil + } + + command := os.Args[1] + switch command { + + case "-v", "--version", "version": + fmt.Println(Version) + return nil + + case "-h", "--help", "help": + cli.Usage() + return nil + + case "add": + if len(cli.Args()) != 2 { + return fmt.Errorf("Usage: %s add ", cli.Name()) + } + return commands.Add(options, cli.Arg(0), cli.Arg(1)) + + case "rm": + if cli.Arg(0) == "" { + return CommandUsage(command) + } + return commands.Remove(options, cli.Arg(0)) + + case "has": + if cli.Arg(0) == "" { + return CommandUsage(command) + } + return commands.Has(options, cli.Arg(0)) + + case "on": + if cli.Arg(0) == "" { + return CommandUsage(command) + } + return commands.Enable(options, cli.Arg(0)) + + case "off": + if cli.Arg(0) == "" { + return CommandUsage(command) + } + return commands.Disable(options, cli.Arg(0)) + + case "fmt": + return commands.Format(options) + + case "ls": + return commands.List(options) + + case "dump": + return commands.Dump(options) + + case "apply": + if cli.Arg(0) == "" { + return fmt.Errorf("Usage: %s apply ", os.Args[0]) + } + return commands.Apply(options, cli.Arg(0)) + + default: + return ErrInvalidCommand + } +} + +func main() { + ExitWithError(wrappedMain()) +} diff --git a/test-fixtures/hostfile1 b/test-fixtures/hostfile1 deleted file mode 100644 index 1172aa2..0000000 --- a/test-fixtures/hostfile1 +++ /dev/null @@ -1,13 +0,0 @@ -#192.168.0.1 pie.dev.example.com -192.168.0.2 cookie.example.com -::1 hostname.pie hostname.candy cake.example.com -fe:23b3:890e:342e::ef strawberry.pie.example.com -# fe:23b3:890e:342e::ef dev.strawberry.pie.example.com -192.168.1.3 pie.example.com -127.0.1.1 robobrain -# fe:23b3:890e:342e::ef chocolate.cake.example.com chocolate.ru.example.com chocolate.tr.example.com chocolate.cookie.example.com -fe:23b3:890e:342e::ef chocolate.pie.example.com -::1 localhost -127.0.0.1 localhost -192.168.1.1 pie.example.com -192.168.1.1 strawberry.pie.example.com \ No newline at end of file diff --git a/test-fixtures/hostfile2 b/test-fixtures/hostfile2 deleted file mode 100644 index eb20dbf..0000000 --- a/test-fixtures/hostfile2 +++ /dev/null @@ -1,2 +0,0 @@ -# entries: 2361 -0.0.0.0 101com.com diff --git a/vendor/github.com/codegangsta/cli/LICENSE b/vendor/github.com/codegangsta/cli/LICENSE deleted file mode 100644 index 5515ccf..0000000 --- a/vendor/github.com/codegangsta/cli/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -Copyright (C) 2013 Jeremy Saenz -All Rights Reserved. - -MIT LICENSE - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/codegangsta/cli/app.go b/vendor/github.com/codegangsta/cli/app.go deleted file mode 100644 index f627420..0000000 --- a/vendor/github.com/codegangsta/cli/app.go +++ /dev/null @@ -1,306 +0,0 @@ -package cli - -import ( - "fmt" - "io" - "io/ioutil" - "os" - "time" -) - -// App is the main structure of a cli application. It is recomended that -// an app be created with the cli.NewApp() function -type App struct { - // The name of the program. Defaults to os.Args[0] - Name string - // Description of the program. - Usage string - // Version of the program - Version string - // List of commands to execute - Commands []Command - // List of flags to parse - Flags []Flag - // Boolean to enable bash completion commands - EnableBashCompletion bool - // Boolean to hide built-in help command - HideHelp bool - // Boolean to hide built-in version flag - HideVersion bool - // An action to execute when the bash-completion flag is set - BashComplete func(context *Context) - // An action to execute before any subcommands are run, but after the context is ready - // If a non-nil error is returned, no subcommands are run - Before func(context *Context) error - // An action to execute after any subcommands are run, but after the subcommand has finished - // It is run even if Action() panics - After func(context *Context) error - // The action to execute when no subcommands are specified - Action func(context *Context) - // Execute this function if the proper command cannot be found - CommandNotFound func(context *Context, command string) - // Compilation date - Compiled time.Time - // List of all authors who contributed - Authors []Author - // Name of Author (Note: Use App.Authors, this is deprecated) - Author string - // Email of Author (Note: Use App.Authors, this is deprecated) - Email string - // Writer writer to write output to - Writer io.Writer -} - -// Tries to find out when this binary was compiled. -// Returns the current time if it fails to find it. -func compileTime() time.Time { - info, err := os.Stat(os.Args[0]) - if err != nil { - return time.Now() - } - return info.ModTime() -} - -// Creates a new cli Application with some reasonable defaults for Name, Usage, Version and Action. -func NewApp() *App { - return &App{ - Name: os.Args[0], - Usage: "A new cli application", - Version: "0.0.0", - BashComplete: DefaultAppComplete, - Action: helpCommand.Action, - Compiled: compileTime(), - Writer: os.Stdout, - } -} - -// Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination -func (a *App) Run(arguments []string) (err error) { - if a.Author != "" || a.Email != "" { - a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email}) - } - - // append help to commands - if a.Command(helpCommand.Name) == nil && !a.HideHelp { - a.Commands = append(a.Commands, helpCommand) - if (HelpFlag != BoolFlag{}) { - a.appendFlag(HelpFlag) - } - } - - //append version/help flags - if a.EnableBashCompletion { - a.appendFlag(BashCompletionFlag) - } - - if !a.HideVersion { - a.appendFlag(VersionFlag) - } - - // parse flags - set := flagSet(a.Name, a.Flags) - set.SetOutput(ioutil.Discard) - err = set.Parse(arguments[1:]) - nerr := normalizeFlags(a.Flags, set) - if nerr != nil { - fmt.Fprintln(a.Writer, nerr) - context := NewContext(a, set, nil) - ShowAppHelp(context) - fmt.Fprintln(a.Writer) - return nerr - } - context := NewContext(a, set, nil) - - if err != nil { - fmt.Fprintf(a.Writer, "Incorrect Usage.\n\n") - ShowAppHelp(context) - fmt.Fprintln(a.Writer) - return err - } - - if checkCompletions(context) { - return nil - } - - if checkHelp(context) { - return nil - } - - if checkVersion(context) { - return nil - } - - if a.After != nil { - defer func() { - afterErr := a.After(context) - if afterErr != nil { - if err != nil { - err = NewMultiError(err, afterErr) - } else { - err = afterErr - } - } - }() - } - - if a.Before != nil { - err := a.Before(context) - if err != nil { - return err - } - } - - args := context.Args() - if args.Present() { - name := args.First() - c := a.Command(name) - if c != nil { - return c.Run(context) - } - } - - // Run default Action - a.Action(context) - return nil -} - -// Another entry point to the cli app, takes care of passing arguments and error handling -func (a *App) RunAndExitOnError() { - if err := a.Run(os.Args); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -// Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags -func (a *App) RunAsSubcommand(ctx *Context) (err error) { - // append help to commands - if len(a.Commands) > 0 { - if a.Command(helpCommand.Name) == nil && !a.HideHelp { - a.Commands = append(a.Commands, helpCommand) - if (HelpFlag != BoolFlag{}) { - a.appendFlag(HelpFlag) - } - } - } - - // append flags - if a.EnableBashCompletion { - a.appendFlag(BashCompletionFlag) - } - - // parse flags - set := flagSet(a.Name, a.Flags) - set.SetOutput(ioutil.Discard) - err = set.Parse(ctx.Args().Tail()) - nerr := normalizeFlags(a.Flags, set) - context := NewContext(a, set, ctx) - - if nerr != nil { - fmt.Fprintln(a.Writer, nerr) - if len(a.Commands) > 0 { - ShowSubcommandHelp(context) - } else { - ShowCommandHelp(ctx, context.Args().First()) - } - fmt.Fprintln(a.Writer) - return nerr - } - - if err != nil { - fmt.Fprintf(a.Writer, "Incorrect Usage.\n\n") - ShowSubcommandHelp(context) - return err - } - - if checkCompletions(context) { - return nil - } - - if len(a.Commands) > 0 { - if checkSubcommandHelp(context) { - return nil - } - } else { - if checkCommandHelp(ctx, context.Args().First()) { - return nil - } - } - - if a.After != nil { - defer func() { - afterErr := a.After(context) - if afterErr != nil { - if err != nil { - err = NewMultiError(err, afterErr) - } else { - err = afterErr - } - } - }() - } - - if a.Before != nil { - err := a.Before(context) - if err != nil { - return err - } - } - - args := context.Args() - if args.Present() { - name := args.First() - c := a.Command(name) - if c != nil { - return c.Run(context) - } - } - - // Run default Action - a.Action(context) - - return nil -} - -// Returns the named command on App. Returns nil if the command does not exist -func (a *App) Command(name string) *Command { - for _, c := range a.Commands { - if c.HasName(name) { - return &c - } - } - - return nil -} - -func (a *App) hasFlag(flag Flag) bool { - for _, f := range a.Flags { - if flag == f { - return true - } - } - - return false -} - -func (a *App) appendFlag(flag Flag) { - if !a.hasFlag(flag) { - a.Flags = append(a.Flags, flag) - } -} - -// Author represents someone who has contributed to a cli project. -type Author struct { - Name string // The Authors name - Email string // The Authors email -} - -// String makes Author comply to the Stringer interface, to allow an easy print in the templating process -func (a Author) String() string { - e := "" - if a.Email != "" { - e = "<" + a.Email + "> " - } - - return fmt.Sprintf("%v %v", a.Name, e) -} diff --git a/vendor/github.com/codegangsta/cli/cli.go b/vendor/github.com/codegangsta/cli/cli.go deleted file mode 100644 index 31dc912..0000000 --- a/vendor/github.com/codegangsta/cli/cli.go +++ /dev/null @@ -1,40 +0,0 @@ -// Package cli provides a minimal framework for creating and organizing command line -// Go applications. cli is designed to be easy to understand and write, the most simple -// cli application can be written as follows: -// func main() { -// cli.NewApp().Run(os.Args) -// } -// -// Of course this application does not do much, so let's make this an actual application: -// func main() { -// app := cli.NewApp() -// app.Name = "greet" -// app.Usage = "say a greeting" -// app.Action = func(c *cli.Context) { -// println("Greetings") -// } -// -// app.Run(os.Args) -// } -package cli - -import ( - "strings" -) - -type MultiError struct { - Errors []error -} - -func NewMultiError(err ...error) MultiError { - return MultiError{Errors: err} -} - -func (m MultiError) Error() string { - errs := make([]string, len(m.Errors)) - for i, err := range m.Errors { - errs[i] = err.Error() - } - - return strings.Join(errs, "\n") -} diff --git a/vendor/github.com/codegangsta/cli/command.go b/vendor/github.com/codegangsta/cli/command.go deleted file mode 100644 index b721c0a..0000000 --- a/vendor/github.com/codegangsta/cli/command.go +++ /dev/null @@ -1,184 +0,0 @@ -package cli - -import ( - "fmt" - "io/ioutil" - "strings" -) - -// Command is a subcommand for a cli.App. -type Command struct { - // The name of the command - Name string - // short name of the command. Typically one character (deprecated, use `Aliases`) - ShortName string - // A list of aliases for the command - Aliases []string - // A short description of the usage of this command - Usage string - // A longer explanation of how the command works - Description string - // The function to call when checking for bash command completions - BashComplete func(context *Context) - // An action to execute before any sub-subcommands are run, but after the context is ready - // If a non-nil error is returned, no sub-subcommands are run - Before func(context *Context) error - // An action to execute after any subcommands are run, but after the subcommand has finished - // It is run even if Action() panics - After func(context *Context) error - // The function to call when this command is invoked - Action func(context *Context) - // List of child commands - Subcommands []Command - // List of flags to parse - Flags []Flag - // Treat all flags as normal arguments if true - SkipFlagParsing bool - // Boolean to hide built-in help command - HideHelp bool -} - -// Invokes the command given the context, parses ctx.Args() to generate command-specific flags -func (c Command) Run(ctx *Context) error { - - if len(c.Subcommands) > 0 || c.Before != nil || c.After != nil { - return c.startApp(ctx) - } - - if !c.HideHelp && (HelpFlag != BoolFlag{}) { - // append help to flags - c.Flags = append( - c.Flags, - HelpFlag, - ) - } - - if ctx.App.EnableBashCompletion { - c.Flags = append(c.Flags, BashCompletionFlag) - } - - set := flagSet(c.Name, c.Flags) - set.SetOutput(ioutil.Discard) - - firstFlagIndex := -1 - terminatorIndex := -1 - for index, arg := range ctx.Args() { - if arg == "--" { - terminatorIndex = index - break - } else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 { - firstFlagIndex = index - } - } - - var err error - if firstFlagIndex > -1 && !c.SkipFlagParsing { - args := ctx.Args() - regularArgs := make([]string, len(args[1:firstFlagIndex])) - copy(regularArgs, args[1:firstFlagIndex]) - - var flagArgs []string - if terminatorIndex > -1 { - flagArgs = args[firstFlagIndex:terminatorIndex] - regularArgs = append(regularArgs, args[terminatorIndex:]...) - } else { - flagArgs = args[firstFlagIndex:] - } - - err = set.Parse(append(flagArgs, regularArgs...)) - } else { - err = set.Parse(ctx.Args().Tail()) - } - - if err != nil { - fmt.Fprint(ctx.App.Writer, "Incorrect Usage.\n\n") - ShowCommandHelp(ctx, c.Name) - fmt.Fprintln(ctx.App.Writer) - return err - } - - nerr := normalizeFlags(c.Flags, set) - if nerr != nil { - fmt.Fprintln(ctx.App.Writer, nerr) - fmt.Fprintln(ctx.App.Writer) - ShowCommandHelp(ctx, c.Name) - fmt.Fprintln(ctx.App.Writer) - return nerr - } - context := NewContext(ctx.App, set, ctx) - - if checkCommandCompletions(context, c.Name) { - return nil - } - - if checkCommandHelp(context, c.Name) { - return nil - } - context.Command = c - c.Action(context) - return nil -} - -func (c Command) Names() []string { - names := []string{c.Name} - - if c.ShortName != "" { - names = append(names, c.ShortName) - } - - return append(names, c.Aliases...) -} - -// Returns true if Command.Name or Command.ShortName matches given name -func (c Command) HasName(name string) bool { - for _, n := range c.Names() { - if n == name { - return true - } - } - return false -} - -func (c Command) startApp(ctx *Context) error { - app := NewApp() - - // set the name and usage - app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) - if c.Description != "" { - app.Usage = c.Description - } else { - app.Usage = c.Usage - } - - // set CommandNotFound - app.CommandNotFound = ctx.App.CommandNotFound - - // set the flags and commands - app.Commands = c.Subcommands - app.Flags = c.Flags - app.HideHelp = c.HideHelp - - app.Version = ctx.App.Version - app.HideVersion = ctx.App.HideVersion - app.Compiled = ctx.App.Compiled - app.Author = ctx.App.Author - app.Email = ctx.App.Email - app.Writer = ctx.App.Writer - - // bash completion - app.EnableBashCompletion = ctx.App.EnableBashCompletion - if c.BashComplete != nil { - app.BashComplete = c.BashComplete - } - - // set the actions - app.Before = c.Before - app.After = c.After - if c.Action != nil { - app.Action = c.Action - } else { - app.Action = helpSubcommand.Action - } - - return app.RunAsSubcommand(ctx) -} diff --git a/vendor/github.com/codegangsta/cli/context.go b/vendor/github.com/codegangsta/cli/context.go deleted file mode 100644 index 5b67129..0000000 --- a/vendor/github.com/codegangsta/cli/context.go +++ /dev/null @@ -1,376 +0,0 @@ -package cli - -import ( - "errors" - "flag" - "strconv" - "strings" - "time" -) - -// Context is a type that is passed through to -// each Handler action in a cli application. Context -// can be used to retrieve context-specific Args and -// parsed command-line options. -type Context struct { - App *App - Command Command - flagSet *flag.FlagSet - setFlags map[string]bool - globalSetFlags map[string]bool - parentContext *Context -} - -// Creates a new context. For use in when invoking an App or Command action. -func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { - return &Context{App: app, flagSet: set, parentContext: parentCtx} -} - -// Looks up the value of a local int flag, returns 0 if no int flag exists -func (c *Context) Int(name string) int { - return lookupInt(name, c.flagSet) -} - -// Looks up the value of a local time.Duration flag, returns 0 if no time.Duration flag exists -func (c *Context) Duration(name string) time.Duration { - return lookupDuration(name, c.flagSet) -} - -// Looks up the value of a local float64 flag, returns 0 if no float64 flag exists -func (c *Context) Float64(name string) float64 { - return lookupFloat64(name, c.flagSet) -} - -// Looks up the value of a local bool flag, returns false if no bool flag exists -func (c *Context) Bool(name string) bool { - return lookupBool(name, c.flagSet) -} - -// Looks up the value of a local boolT flag, returns false if no bool flag exists -func (c *Context) BoolT(name string) bool { - return lookupBoolT(name, c.flagSet) -} - -// Looks up the value of a local string flag, returns "" if no string flag exists -func (c *Context) String(name string) string { - return lookupString(name, c.flagSet) -} - -// Looks up the value of a local string slice flag, returns nil if no string slice flag exists -func (c *Context) StringSlice(name string) []string { - return lookupStringSlice(name, c.flagSet) -} - -// Looks up the value of a local int slice flag, returns nil if no int slice flag exists -func (c *Context) IntSlice(name string) []int { - return lookupIntSlice(name, c.flagSet) -} - -// Looks up the value of a local generic flag, returns nil if no generic flag exists -func (c *Context) Generic(name string) interface{} { - return lookupGeneric(name, c.flagSet) -} - -// Looks up the value of a global int flag, returns 0 if no int flag exists -func (c *Context) GlobalInt(name string) int { - if fs := lookupParentFlagSet(name, c); fs != nil { - return lookupInt(name, fs) - } - return 0 -} - -// Looks up the value of a global time.Duration flag, returns 0 if no time.Duration flag exists -func (c *Context) GlobalDuration(name string) time.Duration { - if fs := lookupParentFlagSet(name, c); fs != nil { - return lookupDuration(name, fs) - } - return 0 -} - -// Looks up the value of a global bool flag, returns false if no bool flag exists -func (c *Context) GlobalBool(name string) bool { - if fs := lookupParentFlagSet(name, c); fs != nil { - return lookupBool(name, fs) - } - return false -} - -// Looks up the value of a global string flag, returns "" if no string flag exists -func (c *Context) GlobalString(name string) string { - if fs := lookupParentFlagSet(name, c); fs != nil { - return lookupString(name, fs) - } - return "" -} - -// Looks up the value of a global string slice flag, returns nil if no string slice flag exists -func (c *Context) GlobalStringSlice(name string) []string { - if fs := lookupParentFlagSet(name, c); fs != nil { - return lookupStringSlice(name, fs) - } - return nil -} - -// Looks up the value of a global int slice flag, returns nil if no int slice flag exists -func (c *Context) GlobalIntSlice(name string) []int { - if fs := lookupParentFlagSet(name, c); fs != nil { - return lookupIntSlice(name, fs) - } - return nil -} - -// Looks up the value of a global generic flag, returns nil if no generic flag exists -func (c *Context) GlobalGeneric(name string) interface{} { - if fs := lookupParentFlagSet(name, c); fs != nil { - return lookupGeneric(name, fs) - } - return nil -} - -// Returns the number of flags set -func (c *Context) NumFlags() int { - return c.flagSet.NFlag() -} - -// Determines if the flag was actually set -func (c *Context) IsSet(name string) bool { - if c.setFlags == nil { - c.setFlags = make(map[string]bool) - c.flagSet.Visit(func(f *flag.Flag) { - c.setFlags[f.Name] = true - }) - } - return c.setFlags[name] == true -} - -// Determines if the global flag was actually set -func (c *Context) GlobalIsSet(name string) bool { - if c.globalSetFlags == nil { - c.globalSetFlags = make(map[string]bool) - for ctx := c.parentContext; ctx != nil && c.globalSetFlags[name] == false; ctx = ctx.parentContext { - ctx.flagSet.Visit(func(f *flag.Flag) { - c.globalSetFlags[f.Name] = true - }) - } - } - return c.globalSetFlags[name] -} - -// Returns a slice of flag names used in this context. -func (c *Context) FlagNames() (names []string) { - for _, flag := range c.Command.Flags { - name := strings.Split(flag.getName(), ",")[0] - if name == "help" { - continue - } - names = append(names, name) - } - return -} - -// Returns a slice of global flag names used by the app. -func (c *Context) GlobalFlagNames() (names []string) { - for _, flag := range c.App.Flags { - name := strings.Split(flag.getName(), ",")[0] - if name == "help" || name == "version" { - continue - } - names = append(names, name) - } - return -} - -type Args []string - -// Returns the command line arguments associated with the context. -func (c *Context) Args() Args { - args := Args(c.flagSet.Args()) - return args -} - -// Returns the nth argument, or else a blank string -func (a Args) Get(n int) string { - if len(a) > n { - return a[n] - } - return "" -} - -// Returns the first argument, or else a blank string -func (a Args) First() string { - return a.Get(0) -} - -// Return the rest of the arguments (not the first one) -// or else an empty string slice -func (a Args) Tail() []string { - if len(a) >= 2 { - return []string(a)[1:] - } - return []string{} -} - -// Checks if there are any arguments present -func (a Args) Present() bool { - return len(a) != 0 -} - -// Swaps arguments at the given indexes -func (a Args) Swap(from, to int) error { - if from >= len(a) || to >= len(a) { - return errors.New("index out of range") - } - a[from], a[to] = a[to], a[from] - return nil -} - -func lookupParentFlagSet(name string, ctx *Context) *flag.FlagSet { - for ctx := ctx.parentContext; ctx != nil; ctx = ctx.parentContext { - if f := ctx.flagSet.Lookup(name); f != nil { - return ctx.flagSet - } - } - return nil -} - -func lookupInt(name string, set *flag.FlagSet) int { - f := set.Lookup(name) - if f != nil { - val, err := strconv.Atoi(f.Value.String()) - if err != nil { - return 0 - } - return val - } - - return 0 -} - -func lookupDuration(name string, set *flag.FlagSet) time.Duration { - f := set.Lookup(name) - if f != nil { - val, err := time.ParseDuration(f.Value.String()) - if err == nil { - return val - } - } - - return 0 -} - -func lookupFloat64(name string, set *flag.FlagSet) float64 { - f := set.Lookup(name) - if f != nil { - val, err := strconv.ParseFloat(f.Value.String(), 64) - if err != nil { - return 0 - } - return val - } - - return 0 -} - -func lookupString(name string, set *flag.FlagSet) string { - f := set.Lookup(name) - if f != nil { - return f.Value.String() - } - - return "" -} - -func lookupStringSlice(name string, set *flag.FlagSet) []string { - f := set.Lookup(name) - if f != nil { - return (f.Value.(*StringSlice)).Value() - - } - - return nil -} - -func lookupIntSlice(name string, set *flag.FlagSet) []int { - f := set.Lookup(name) - if f != nil { - return (f.Value.(*IntSlice)).Value() - - } - - return nil -} - -func lookupGeneric(name string, set *flag.FlagSet) interface{} { - f := set.Lookup(name) - if f != nil { - return f.Value - } - return nil -} - -func lookupBool(name string, set *flag.FlagSet) bool { - f := set.Lookup(name) - if f != nil { - val, err := strconv.ParseBool(f.Value.String()) - if err != nil { - return false - } - return val - } - - return false -} - -func lookupBoolT(name string, set *flag.FlagSet) bool { - f := set.Lookup(name) - if f != nil { - val, err := strconv.ParseBool(f.Value.String()) - if err != nil { - return true - } - return val - } - - return false -} - -func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) { - switch ff.Value.(type) { - case *StringSlice: - default: - set.Set(name, ff.Value.String()) - } -} - -func normalizeFlags(flags []Flag, set *flag.FlagSet) error { - visited := make(map[string]bool) - set.Visit(func(f *flag.Flag) { - visited[f.Name] = true - }) - for _, f := range flags { - parts := strings.Split(f.getName(), ",") - if len(parts) == 1 { - continue - } - var ff *flag.Flag - for _, name := range parts { - name = strings.Trim(name, " ") - if visited[name] { - if ff != nil { - return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name) - } - ff = set.Lookup(name) - } - } - if ff == nil { - continue - } - for _, name := range parts { - name = strings.Trim(name, " ") - if !visited[name] { - copyFlag(name, ff, set) - } - } - } - return nil -} diff --git a/vendor/github.com/codegangsta/cli/flag.go b/vendor/github.com/codegangsta/cli/flag.go deleted file mode 100644 index 531b091..0000000 --- a/vendor/github.com/codegangsta/cli/flag.go +++ /dev/null @@ -1,497 +0,0 @@ -package cli - -import ( - "flag" - "fmt" - "os" - "strconv" - "strings" - "time" -) - -// This flag enables bash-completion for all commands and subcommands -var BashCompletionFlag = BoolFlag{ - Name: "generate-bash-completion", -} - -// This flag prints the version for the application -var VersionFlag = BoolFlag{ - Name: "version, v", - Usage: "print the version", -} - -// This flag prints the help for all commands and subcommands -// Set to the zero value (BoolFlag{}) to disable flag -- keeps subcommand -// unless HideHelp is set to true) -var HelpFlag = BoolFlag{ - Name: "help, h", - Usage: "show help", -} - -// Flag is a common interface related to parsing flags in cli. -// For more advanced flag parsing techniques, it is recomended that -// this interface be implemented. -type Flag interface { - fmt.Stringer - // Apply Flag settings to the given flag set - Apply(*flag.FlagSet) - getName() string -} - -func flagSet(name string, flags []Flag) *flag.FlagSet { - set := flag.NewFlagSet(name, flag.ContinueOnError) - - for _, f := range flags { - f.Apply(set) - } - return set -} - -func eachName(longName string, fn func(string)) { - parts := strings.Split(longName, ",") - for _, name := range parts { - name = strings.Trim(name, " ") - fn(name) - } -} - -// Generic is a generic parseable type identified by a specific flag -type Generic interface { - Set(value string) error - String() string -} - -// GenericFlag is the flag type for types implementing Generic -type GenericFlag struct { - Name string - Value Generic - Usage string - EnvVar string -} - -// String returns the string representation of the generic flag to display the -// help text to the user (uses the String() method of the generic flag to show -// the value) -func (f GenericFlag) String() string { - return withEnvHint(f.EnvVar, fmt.Sprintf("%s%s \"%v\"\t%v", prefixFor(f.Name), f.Name, f.Value, f.Usage)) -} - -// Apply takes the flagset and calls Set on the generic flag with the value -// provided by the user for parsing by the flag -func (f GenericFlag) Apply(set *flag.FlagSet) { - val := f.Value - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - val.Set(envVal) - break - } - } - } - - eachName(f.Name, func(name string) { - set.Var(f.Value, name, f.Usage) - }) -} - -func (f GenericFlag) getName() string { - return f.Name -} - -// StringSlice is an opaque type for []string to satisfy flag.Value -type StringSlice []string - -// Set appends the string value to the list of values -func (f *StringSlice) Set(value string) error { - *f = append(*f, value) - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (f *StringSlice) String() string { - return fmt.Sprintf("%s", *f) -} - -// Value returns the slice of strings set by this flag -func (f *StringSlice) Value() []string { - return *f -} - -// StringSlice is a string flag that can be specified multiple times on the -// command-line -type StringSliceFlag struct { - Name string - Value *StringSlice - Usage string - EnvVar string -} - -// String returns the usage -func (f StringSliceFlag) String() string { - firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") - pref := prefixFor(firstName) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s [%v]\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f StringSliceFlag) Apply(set *flag.FlagSet) { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - newVal := &StringSlice{} - for _, s := range strings.Split(envVal, ",") { - s = strings.TrimSpace(s) - newVal.Set(s) - } - f.Value = newVal - break - } - } - } - - eachName(f.Name, func(name string) { - if f.Value == nil { - f.Value = &StringSlice{} - } - set.Var(f.Value, name, f.Usage) - }) -} - -func (f StringSliceFlag) getName() string { - return f.Name -} - -// StringSlice is an opaque type for []int to satisfy flag.Value -type IntSlice []int - -// Set parses the value into an integer and appends it to the list of values -func (f *IntSlice) Set(value string) error { - tmp, err := strconv.Atoi(value) - if err != nil { - return err - } else { - *f = append(*f, tmp) - } - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (f *IntSlice) String() string { - return fmt.Sprintf("%d", *f) -} - -// Value returns the slice of ints set by this flag -func (f *IntSlice) Value() []int { - return *f -} - -// IntSliceFlag is an int flag that can be specified multiple times on the -// command-line -type IntSliceFlag struct { - Name string - Value *IntSlice - Usage string - EnvVar string -} - -// String returns the usage -func (f IntSliceFlag) String() string { - firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") - pref := prefixFor(firstName) - return withEnvHint(f.EnvVar, fmt.Sprintf("%s [%v]\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f IntSliceFlag) Apply(set *flag.FlagSet) { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - newVal := &IntSlice{} - for _, s := range strings.Split(envVal, ",") { - s = strings.TrimSpace(s) - err := newVal.Set(s) - if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) - } - } - f.Value = newVal - break - } - } - } - - eachName(f.Name, func(name string) { - if f.Value == nil { - f.Value = &IntSlice{} - } - set.Var(f.Value, name, f.Usage) - }) -} - -func (f IntSliceFlag) getName() string { - return f.Name -} - -// BoolFlag is a switch that defaults to false -type BoolFlag struct { - Name string - Usage string - EnvVar string -} - -// String returns a readable representation of this value (for usage defaults) -func (f BoolFlag) String() string { - return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f BoolFlag) Apply(set *flag.FlagSet) { - val := false - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValBool, err := strconv.ParseBool(envVal) - if err == nil { - val = envValBool - } - break - } - } - } - - eachName(f.Name, func(name string) { - set.Bool(name, val, f.Usage) - }) -} - -func (f BoolFlag) getName() string { - return f.Name -} - -// BoolTFlag this represents a boolean flag that is true by default, but can -// still be set to false by --some-flag=false -type BoolTFlag struct { - Name string - Usage string - EnvVar string -} - -// String returns a readable representation of this value (for usage defaults) -func (f BoolTFlag) String() string { - return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f BoolTFlag) Apply(set *flag.FlagSet) { - val := true - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValBool, err := strconv.ParseBool(envVal) - if err == nil { - val = envValBool - break - } - } - } - } - - eachName(f.Name, func(name string) { - set.Bool(name, val, f.Usage) - }) -} - -func (f BoolTFlag) getName() string { - return f.Name -} - -// StringFlag represents a flag that takes as string value -type StringFlag struct { - Name string - Value string - Usage string - EnvVar string -} - -// String returns the usage -func (f StringFlag) String() string { - var fmtString string - fmtString = "%s %v\t%v" - - if len(f.Value) > 0 { - fmtString = "%s \"%v\"\t%v" - } else { - fmtString = "%s %v\t%v" - } - - return withEnvHint(f.EnvVar, fmt.Sprintf(fmtString, prefixedNames(f.Name), f.Value, f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f StringFlag) Apply(set *flag.FlagSet) { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - f.Value = envVal - break - } - } - } - - eachName(f.Name, func(name string) { - set.String(name, f.Value, f.Usage) - }) -} - -func (f StringFlag) getName() string { - return f.Name -} - -// IntFlag is a flag that takes an integer -// Errors if the value provided cannot be parsed -type IntFlag struct { - Name string - Value int - Usage string - EnvVar string -} - -// String returns the usage -func (f IntFlag) String() string { - return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name), f.Value, f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f IntFlag) Apply(set *flag.FlagSet) { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValInt, err := strconv.ParseInt(envVal, 0, 64) - if err == nil { - f.Value = int(envValInt) - break - } - } - } - } - - eachName(f.Name, func(name string) { - set.Int(name, f.Value, f.Usage) - }) -} - -func (f IntFlag) getName() string { - return f.Name -} - -// DurationFlag is a flag that takes a duration specified in Go's duration -// format: https://golang.org/pkg/time/#ParseDuration -type DurationFlag struct { - Name string - Value time.Duration - Usage string - EnvVar string -} - -// String returns a readable representation of this value (for usage defaults) -func (f DurationFlag) String() string { - return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name), f.Value, f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f DurationFlag) Apply(set *flag.FlagSet) { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValDuration, err := time.ParseDuration(envVal) - if err == nil { - f.Value = envValDuration - break - } - } - } - } - - eachName(f.Name, func(name string) { - set.Duration(name, f.Value, f.Usage) - }) -} - -func (f DurationFlag) getName() string { - return f.Name -} - -// Float64Flag is a flag that takes an float value -// Errors if the value provided cannot be parsed -type Float64Flag struct { - Name string - Value float64 - Usage string - EnvVar string -} - -// String returns the usage -func (f Float64Flag) String() string { - return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name), f.Value, f.Usage)) -} - -// Apply populates the flag given the flag set and environment -func (f Float64Flag) Apply(set *flag.FlagSet) { - if f.EnvVar != "" { - for _, envVar := range strings.Split(f.EnvVar, ",") { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValFloat, err := strconv.ParseFloat(envVal, 10) - if err == nil { - f.Value = float64(envValFloat) - } - } - } - } - - eachName(f.Name, func(name string) { - set.Float64(name, f.Value, f.Usage) - }) -} - -func (f Float64Flag) getName() string { - return f.Name -} - -func prefixFor(name string) (prefix string) { - if len(name) == 1 { - prefix = "-" - } else { - prefix = "--" - } - - return -} - -func prefixedNames(fullName string) (prefixed string) { - parts := strings.Split(fullName, ",") - for i, name := range parts { - name = strings.Trim(name, " ") - prefixed += prefixFor(name) + name - if i < len(parts)-1 { - prefixed += ", " - } - } - return -} - -func withEnvHint(envVar, str string) string { - envText := "" - if envVar != "" { - envText = fmt.Sprintf(" [$%s]", strings.Join(strings.Split(envVar, ","), ", $")) - } - return str + envText -} diff --git a/vendor/github.com/codegangsta/cli/help.go b/vendor/github.com/codegangsta/cli/help.go deleted file mode 100644 index 9b7b9b9..0000000 --- a/vendor/github.com/codegangsta/cli/help.go +++ /dev/null @@ -1,235 +0,0 @@ -package cli - -import ( - "fmt" - "io" - "strings" - "text/tabwriter" - "text/template" -) - -// The text template for the Default help topic. -// cli.go uses text/template to render templates. You can -// render custom help text by setting this variable. -var AppHelpTemplate = `NAME: - {{.Name}} - {{.Usage}} - -USAGE: - {{.Name}} {{if .Flags}}[global options] {{end}}command{{if .Flags}} [command options]{{end}} [arguments...] - -VERSION: - {{.Version}}{{if len .Authors}} - -AUTHOR(S): - {{range .Authors}}{{ . }}{{end}}{{end}} - -COMMANDS: - {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} - {{end}}{{if .Flags}} -GLOBAL OPTIONS: - {{range .Flags}}{{.}} - {{end}}{{end}} -` - -// The text template for the command help topic. -// cli.go uses text/template to render templates. You can -// render custom help text by setting this variable. -var CommandHelpTemplate = `NAME: - {{.Name}} - {{.Usage}} - -USAGE: - command {{.Name}}{{if .Flags}} [command options]{{end}} [arguments...]{{if .Description}} - -DESCRIPTION: - {{.Description}}{{end}}{{if .Flags}} - -OPTIONS: - {{range .Flags}}{{.}} - {{end}}{{ end }} -` - -// The text template for the subcommand help topic. -// cli.go uses text/template to render templates. You can -// render custom help text by setting this variable. -var SubcommandHelpTemplate = `NAME: - {{.Name}} - {{.Usage}} - -USAGE: - {{.Name}} command{{if .Flags}} [command options]{{end}} [arguments...] - -COMMANDS: - {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} - {{end}}{{if .Flags}} -OPTIONS: - {{range .Flags}}{{.}} - {{end}}{{end}} -` - -var helpCommand = Command{ - Name: "help", - Aliases: []string{"h"}, - Usage: "Shows a list of commands or help for one command", - Action: func(c *Context) { - args := c.Args() - if args.Present() { - ShowCommandHelp(c, args.First()) - } else { - ShowAppHelp(c) - } - }, -} - -var helpSubcommand = Command{ - Name: "help", - Aliases: []string{"h"}, - Usage: "Shows a list of commands or help for one command", - Action: func(c *Context) { - args := c.Args() - if args.Present() { - ShowCommandHelp(c, args.First()) - } else { - ShowSubcommandHelp(c) - } - }, -} - -// Prints help for the App or Command -type helpPrinter func(w io.Writer, templ string, data interface{}) - -var HelpPrinter helpPrinter = printHelp - -// Prints version for the App -var VersionPrinter = printVersion - -func ShowAppHelp(c *Context) { - HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) -} - -// Prints the list of subcommands as the default app completion method -func DefaultAppComplete(c *Context) { - for _, command := range c.App.Commands { - for _, name := range command.Names() { - fmt.Fprintln(c.App.Writer, name) - } - } -} - -// Prints help for the given command -func ShowCommandHelp(ctx *Context, command string) { - // show the subcommand help for a command with subcommands - if command == "" { - HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App) - return - } - - for _, c := range ctx.App.Commands { - if c.HasName(command) { - HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) - return - } - } - - if ctx.App.CommandNotFound != nil { - ctx.App.CommandNotFound(ctx, command) - } else { - fmt.Fprintf(ctx.App.Writer, "No help topic for '%v'\n", command) - } -} - -// Prints help for the given subcommand -func ShowSubcommandHelp(c *Context) { - ShowCommandHelp(c, c.Command.Name) -} - -// Prints the version number of the App -func ShowVersion(c *Context) { - VersionPrinter(c) -} - -func printVersion(c *Context) { - fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version) -} - -// Prints the lists of commands within a given context -func ShowCompletions(c *Context) { - a := c.App - if a != nil && a.BashComplete != nil { - a.BashComplete(c) - } -} - -// Prints the custom completions for a given command -func ShowCommandCompletions(ctx *Context, command string) { - c := ctx.App.Command(command) - if c != nil && c.BashComplete != nil { - c.BashComplete(ctx) - } -} - -func printHelp(out io.Writer, templ string, data interface{}) { - funcMap := template.FuncMap{ - "join": strings.Join, - } - - w := tabwriter.NewWriter(out, 0, 8, 1, '\t', 0) - t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) - err := t.Execute(w, data) - if err != nil { - panic(err) - } - w.Flush() -} - -func checkVersion(c *Context) bool { - if c.GlobalBool("version") || c.GlobalBool("v") || c.Bool("version") || c.Bool("v") { - ShowVersion(c) - return true - } - - return false -} - -func checkHelp(c *Context) bool { - if c.GlobalBool("h") || c.GlobalBool("help") || c.Bool("h") || c.Bool("help") { - ShowAppHelp(c) - return true - } - - return false -} - -func checkCommandHelp(c *Context, name string) bool { - if c.Bool("h") || c.Bool("help") { - ShowCommandHelp(c, name) - return true - } - - return false -} - -func checkSubcommandHelp(c *Context) bool { - if c.GlobalBool("h") || c.GlobalBool("help") { - ShowSubcommandHelp(c) - return true - } - - return false -} - -func checkCompletions(c *Context) bool { - if (c.GlobalBool(BashCompletionFlag.Name) || c.Bool(BashCompletionFlag.Name)) && c.App.EnableBashCompletion { - ShowCompletions(c) - return true - } - - return false -} - -func checkCommandCompletions(c *Context, name string) bool { - if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion { - ShowCommandCompletions(c, name) - return true - } - - return false -}