Started major refactor for 0.5.0

- Rewrite CLI to use flag package instead of codegangsta/cli
- Move lib to hostess/ and move main.go to project root
- Change some of the hostess API to return errors instead of panicking
- Remove aff, del, and list commands
- Remove -s and -q flags
pull/38/head
Chris Bednarski 4 years ago
parent 4c91c57875
commit 66670942dd

@ -1,7 +1,7 @@
language: go
go:
- 1.9.2
- 1.13.8
- master
notifications:

@ -1,3 +0,0 @@
FROM scratch
ADD hostess /hostess
ENTRYPOINT ["/hostess"]

@ -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

@ -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.

@ -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)
}

@ -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 <hostname> <ip> 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 <hostname> <ip>")
}
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 <domain> from the hosts file
func Del(c *cli.Context) {
if len(c.Args()) != 1 {
MaybeError(c, "expected <hostname>")
}
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 <hostname>")
}
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 <hostname>")
}
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))
}

@ -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 <hostname> <ip> 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 <domain> 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 <hostname>")
}
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))
}

@ -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)
}
}
}

@ -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)
}

@ -0,0 +1,3 @@
module github.com/cbednarski/hostess
go 1.14

@ -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
}

@ -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)
}
}

@ -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
}

@ -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")
}
}

@ -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())
}

@ -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)")
}
}

@ -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 <hostname> <ip> Add or overwrite a hosts entry
rm <hostname> Remote a hosts entry
on <hostname> Enable a hosts entry
off <hostname> 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 <chris@cbednarski.com>; 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 <hostname>", 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 <hostname> <ip>", 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 <filename>", os.Args[0])
}
return commands.Apply(options, cli.Arg(0))
default:
return ErrInvalidCommand
}
}
func main() {
ExitWithError(wrappedMain())
}

@ -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

@ -1,2 +0,0 @@
# entries: 2361
0.0.0.0 101com.com

@ -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.

@ -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)
}

@ -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")
}

@ -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)
}

@ -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
}

@ -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
}

@ -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
}
Loading…
Cancel
Save