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