mirror of https://github.com/cbednarski/hostess
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 flagspull/38/head
parent
4c91c57875
commit
66670942dd
@ -1,3 +0,0 @@
|
|||||||
FROM scratch
|
|
||||||
ADD hostess /hostess
|
|
||||||
ENTRYPOINT ["/hostess"]
|
|
@ -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)
|
|
||||||
}
|
|
@ -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…
Reference in New Issue