mirror of https://github.com/cbednarski/hostess
Compare commits
42 Commits
Author | SHA1 | Date |
---|---|---|
Chris Bednarski | 187298f216 | 4 years ago |
Chris Bednarski | 4f4dd46d6f | 4 years ago |
Chris Bednarski | dfe047857d | 4 years ago |
Chris Bednarski | f0fc63817f | 4 years ago |
Chris Bednarski | 71f910ca52 | 4 years ago |
Chris Bednarski | b5e6f45648 | 4 years ago |
Chris Bednarski | fdfc2f9001 | 4 years ago |
Chris Bednarski | 168ef0ce47 | 4 years ago |
Chris Bednarski | e56fc7aec0 | 4 years ago |
Chris Bednarski | e899791fce | 4 years ago |
Chris Bednarski | 0e15ef497a | 4 years ago |
Chris Bednarski | ebbdf23b34 | 4 years ago |
Chris Bednarski | 4fc0ac9077 | 4 years ago |
Chris Bednarski | d60ff45bf8 | 4 years ago |
Chris Bednarski | a343a8e07f | 4 years ago |
Chris Bednarski | aecfa8f689 | 4 years ago |
Chris Bednarski | c4e4f6d820 | 4 years ago |
Rui Chen | 7fbd6215e8 | 4 years ago |
Chris Bednarski | 138ef9dcd4 | 4 years ago |
Chris Bednarski | 1e2456bc6a | 4 years ago |
Chris Bednarski | df2d795268 | 4 years ago |
Chris Bednarski | be9a630ae8 | 4 years ago |
Chris Bednarski | d3e97d1758 | 4 years ago |
Chris Bednarski | 127256655c | 4 years ago |
Chris Bednarski | ca1856f5fe | 4 years ago |
Chris Bednarski | 9c247955f6 | 4 years ago |
Chris Bednarski | ba102742d4 | 4 years ago |
Chris Bednarski | 960732bbb4 | 4 years ago |
Chris Bednarski | dd746035f7 | 4 years ago |
Chris Bednarski | 18546969ce | 4 years ago |
Chris Bednarski | c53f06fdc8 | 4 years ago |
Chris Bednarski | 000eb28419 | 4 years ago |
Chris Bednarski | 2b25df69d5 | 4 years ago |
Chris Bednarski | b9940cf499 | 4 years ago |
Chris Bednarski | 26a3d25daf | 4 years ago |
Chris Bednarski | ddad9598d6 | 4 years ago |
Chris Bednarski | aeb56f8a79 | 4 years ago |
Chris Bednarski | 7ca1771c71 | 4 years ago |
Chris Bednarski | 66670942dd | 4 years ago |
Chris Bednarski | 4c91c57875 | 5 years ago |
Chris Bednarski | 017b8f33a4 | 6 years ago |
alswl | 6426b1e7c6 | 6 years ago |
@ -0,0 +1,9 @@
|
||||
install:
|
||||
- go version
|
||||
|
||||
build: false
|
||||
deploy: false
|
||||
|
||||
test_script:
|
||||
- go test ./...
|
||||
- go vet ./...
|
@ -1,7 +1,3 @@
|
||||
/hostess
|
||||
/hostess.exe
|
||||
/hostess_*
|
||||
/coverage.out
|
||||
/coverage.html
|
||||
/.vscode
|
||||
/.idea
|
||||
/.idea/
|
||||
/bin/
|
||||
.DS_Store
|
||||
|
@ -1,8 +1,8 @@
|
||||
dist: bionic
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.9.2
|
||||
- master
|
||||
- 1.14.x
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
|
@ -0,0 +1,89 @@
|
||||
# Change Log
|
||||
|
||||
## v0.5.2 (March 13, 2020)
|
||||
|
||||
Bug Fixes
|
||||
|
||||
- `hostess fmt -n` works properly again, and has more specific behavior:
|
||||
- `hostess fmt` will replace duplicates without asking for help
|
||||
- `hostess fmt -n` will *not* replace duplicates, and will exit with error if any are found (#41)
|
||||
- `hostess fmt` with and without `-n` will exit with error if conflicting hostnames are found because hostess cannot fix the conflicts
|
||||
|
||||
## v0.5.1 (March 10, 2020)
|
||||
|
||||
Bug Fixes
|
||||
|
||||
- Format will no longer exit with an error when encountering a duplicate entry (#39)
|
||||
|
||||
## v0.5.0 (March 7, 2020)
|
||||
|
||||
Breaking changes
|
||||
|
||||
- Windows now has a platform-specific hosts format with one IP and hostname per line
|
||||
|
||||
## v0.4.1 (February 28, 2020)
|
||||
|
||||
Bug Fixes
|
||||
|
||||
- Fix hostfiles not saving on Windows #27
|
||||
|
||||
## v0.4.0 (February 28, 2020)
|
||||
|
||||
0.4.0 is a major refactor of the frontend (CLI) focused on simplifying the UI
|
||||
and code, supporting newer Go tooling (i.e. go mod), and removing external
|
||||
dependencies.
|
||||
|
||||
Breaking Changes
|
||||
|
||||
- Moved CLI to `github.com/cbednarski/hostess`. `go get` should now do what you probably wanted the first time.
|
||||
- Moved library to `github.com/cbednarski/hostess/hostess`
|
||||
- Many command aliases and flags have been removed
|
||||
- `Hostlist.Enable` and `Hostlist.Disable` now return an `error` instead of `bool`. Check against `ErrHostnameNotFound`.
|
||||
- Several functions will now return `ErrInvalidVersionArg` instead of panicking in that case
|
||||
|
||||
Improvements
|
||||
|
||||
- Removed `codegangsta/cli`
|
||||
- Removed `aff` command
|
||||
- Removed `del` command (use `rm` instead)
|
||||
- Removed `list` command (use `ls` instead)
|
||||
- Removed `fixed` command (just run `fmt`)
|
||||
- Command `fix` renamed to `fmt`
|
||||
- Removed `-s` and `-q` flags. Errors are now shown always. Redirect stderr if you don't want to see them.
|
||||
- Removed `-f` from various commands. Use `fmt` instead.
|
||||
- Added Go mod support
|
||||
- Added AppVeyor for Windows builds
|
||||
- Overhauled the Makefile for easier builds
|
||||
|
||||
## v0.3.0 (February 18, 2018)
|
||||
|
||||
Improvements
|
||||
|
||||
- Added `fixed` subcommand which checks whether the hosts file is already formatted by hostess
|
||||
|
||||
Bug Fixes
|
||||
|
||||
- Show an error when there is a parsing failure instead of silently truncating the hosts file
|
||||
- Global flags between hostess and the subcommand are no longer ignored
|
||||
- Binary should now display the correct version of the software
|
||||
|
||||
## v0.2.1 (May 17, 2016)
|
||||
|
||||
Bug Fixes
|
||||
|
||||
- Fix vendor path for `codegangsta/cli`
|
||||
|
||||
## v0.2.0 (May 10, 2016)
|
||||
|
||||
Improvements
|
||||
|
||||
- Vendor `codegangsta/cli` for more reliable builds
|
||||
|
||||
Bug Fixes
|
||||
|
||||
- Fix panic in `hostess ls` #14
|
||||
- Fix incompatible API in CLI library #15
|
||||
|
||||
## v0.1.0 (June 6, 2015)
|
||||
|
||||
Initial release
|
@ -1,3 +0,0 @@
|
||||
FROM scratch
|
||||
ADD hostess /hostess
|
||||
ENTRYPOINT ["/hostess"]
|
@ -1,42 +1,20 @@
|
||||
PACKAGES=$(go list ./... | grep -v vendor)
|
||||
prefix=/usr/local
|
||||
exec_prefix=$(prefix)
|
||||
bindir=$(exec_prefix)/bin
|
||||
datarootdir=$(prefix)/share
|
||||
datadir=$(datarootdir)
|
||||
mandir=$(datarootdir)/man
|
||||
|
||||
.PHONY: all deps build test gox build-all install clean
|
||||
|
||||
all: build test
|
||||
|
||||
deps:
|
||||
go get github.com/golang/lint/golint
|
||||
go get github.com/stretchr/testify/assert
|
||||
go get golang.org/x/tools/cmd/cover
|
||||
go get
|
||||
|
||||
build: deps
|
||||
go build cmd/hostess/hostess.go
|
||||
RELEASE_VERSION=$(shell git describe --tags)
|
||||
|
||||
test:
|
||||
go test -coverprofile=coverage.out; go tool cover -html=coverage.out -o coverage.html
|
||||
go vet $(PACKAGES)
|
||||
golint $(PACKAGES)
|
||||
|
||||
gox:
|
||||
go get github.com/mitchellh/gox
|
||||
gox -build-toolchain
|
||||
go test ./...
|
||||
go vet ./...
|
||||
|
||||
build-all: test
|
||||
which gox || make gox
|
||||
gox -arch="386 amd64 arm" -os="darwin linux windows" github.com/cbednarski/hostess/cmd/hostess
|
||||
install:
|
||||
go build -o bin/hostess .
|
||||
sudo mv bin/hostess /usr/local/bin/hostess
|
||||
|
||||
install: hostess
|
||||
mkdir -p $(bindir)
|
||||
cp hostess $(bindir)/hostess
|
||||
release: test
|
||||
GOOS=windows GOARCH=amd64 go build -ldflags "-X main.Version=${RELEASE_VERSION}" -o bin/hostess_windows_amd64.exe .
|
||||
GOOS=darwin GOARCH=amd64 go build -ldflags "-X main.Version=${RELEASE_VERSION}" -o bin/hostess_macos_amd64 .
|
||||
GOOS=linux GOARCH=amd64 go build -ldflags "-X main.Version=${RELEASE_VERSION}" -o bin/hostess_linux_amd64 .
|
||||
GOOS=linux GOARCH=arm go build -ldflags "-X main.Version=${RELEASE_VERSION}" -o bin/hostess_linux_arm .
|
||||
|
||||
clean:
|
||||
rm -f ./hostess
|
||||
rm -f ./hostess_*
|
||||
rm -f ./coverage.*
|
||||
rm -rf ./bin/
|
||||
|
||||
.PHONY: install test release clean
|
||||
|
@ -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 if 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 +1,280 @@
|
||||
package hostess
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/cbednarski/hostess/hostess"
|
||||
)
|
||||
|
||||
// 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())
|
||||
var ErrParsingHostsFile = errors.New("Errors while parsing hostsfile. Please resolve any conflicts and try again.")
|
||||
|
||||
// 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))
|
||||
}
|
||||
type Options struct {
|
||||
Preview bool
|
||||
}
|
||||
|
||||
// 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)
|
||||
// PrintErrLn will print to stderr followed by a newline
|
||||
func PrintErrLn(err error) {
|
||||
os.Stderr.WriteString(fmt.Sprintf("%s\n", err))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
// LoadHostfile will try to load, parse, and return a Hostfile. If we
|
||||
// encounter errors we will terminate.
|
||||
func LoadHostfile(options *Options) (*hostess.Hostfile, error) {
|
||||
hosts, errs := hostess.LoadHostfile()
|
||||
|
||||
// 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
|
||||
}
|
||||
var err error
|
||||
|
||||
// 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())
|
||||
// If -n is passed, we'll always exit with an error code on duplicate.
|
||||
// See issue 39
|
||||
if options.Preview {
|
||||
err = ErrParsingHostsFile
|
||||
}
|
||||
|
||||
for _, currentErr := range errs {
|
||||
PrintErrLn(currentErr)
|
||||
// If we find a duplicate we'll notify the user and continue. For
|
||||
// other errors we'll bail out.
|
||||
if !strings.Contains(currentErr.Error(), "duplicate hostname entry") {
|
||||
err = ErrParsingHostsFile
|
||||
}
|
||||
}
|
||||
}
|
||||
return hostsfile
|
||||
|
||||
return hosts, err
|
||||
}
|
||||
|
||||
// MaybeSaveHostFile will output or write the Hostfile, or exit 1 and error.
|
||||
func MaybeSaveHostFile(c *cli.Context, hostfile *Hostfile) {
|
||||
// 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 AnyBool(c, "n") {
|
||||
if options.Preview {
|
||||
fmt.Printf("%s", hostfile.Format())
|
||||
} else {
|
||||
err := hostfile.Save()
|
||||
if err != nil {
|
||||
MaybeError(c, ErrCantWriteHostFile.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := hostfile.Save(); err != nil {
|
||||
return fmt.Errorf("Unable to write to %s. (error: %s)", hostess.GetHostsPath(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StrPadRight adds spaces to the right of a string until it reaches l length.
|
||||
// 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(s string, l int) string {
|
||||
r := l - len(s)
|
||||
if r < 0 {
|
||||
r = 0
|
||||
func StrPadRight(input string, length int) string {
|
||||
minimum := len(input)
|
||||
if length <= minimum {
|
||||
return input
|
||||
}
|
||||
return s + strings.Repeat(" ", r)
|
||||
return input + strings.Repeat(" ", length-minimum)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// hosts file
|
||||
func Add(options *Options, hostname, ip string) error {
|
||||
hostsfile, err := LoadHostfile(options)
|
||||
|
||||
hostname, err := NewHostname(c.Args()[0], c.Args()[1], true)
|
||||
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" {
|
||||
hostname.Enabled = false
|
||||
return err
|
||||
}
|
||||
|
||||
replace := hostsfile.Hosts.ContainsDomain(hostname.Domain)
|
||||
replaced := 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(hostname)
|
||||
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())
|
||||
if err := SaveOrPreview(options, hostsfile); err != nil {
|
||||
return err
|
||||
}
|
||||
// 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 replaced {
|
||||
fmt.Printf("Updated %s\n", newHostname.FormatHuman())
|
||||
} 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()))
|
||||
}
|
||||
fmt.Printf("Added %s\n", newHostname.FormatHuman())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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>")
|
||||
// Remove command removes any hostname(s) matching <domain> from the hosts file
|
||||
func Remove(options *Options, hostname string) error {
|
||||
hostsfile, err := LoadHostfile(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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()))
|
||||
found := hostsfile.Hosts.ContainsDomain(hostname)
|
||||
if !found {
|
||||
fmt.Printf("%s not found in %s", hostname, hostess.GetHostsPath())
|
||||
}
|
||||
|
||||
hostsfile.Hosts.RemoveDomain(hostname)
|
||||
if err := SaveOrPreview(options, hostsfile); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Deleted %s\n", hostname)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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>")
|
||||
func Has(options *Options, hostname string) error {
|
||||
hostsfile, err := LoadHostfile(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
domain := c.Args()[0]
|
||||
hostsfile := MaybeLoadHostFile(c)
|
||||
|
||||
found := hostsfile.Hosts.ContainsDomain(domain)
|
||||
found := hostsfile.Hosts.ContainsDomain(hostname)
|
||||
if found {
|
||||
MaybePrintln(c, fmt.Sprintf("Found %s in %s", domain, GetHostsPath()))
|
||||
fmt.Printf("Found %s in %s\n", hostname, hostess.GetHostsPath())
|
||||
} else {
|
||||
MaybeError(c, fmt.Sprintf("%s not found in %s", domain, GetHostsPath()))
|
||||
fmt.Printf("%s not found in %s\n", hostname, hostess.GetHostsPath())
|
||||
// Exit 1 for bash scripts to use this as a check
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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>")
|
||||
func Enable(options *Options, hostname string) error {
|
||||
hostsfile, err := LoadHostfile(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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 err := hostsfile.Hosts.Enable(hostname); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := SaveOrPreview(options, hostsfile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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))
|
||||
fmt.Printf("Enabled %s\n", hostname)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Disable(options *Options, hostname string) error {
|
||||
hostsfile, err := LoadHostfile(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := hostsfile.Hosts.Disable(hostname); err != nil {
|
||||
if err == hostess.ErrHostnameNotFound {
|
||||
// If the hostname does not exist then we have still achieved the
|
||||
// desired result, so we will not exit with an error here. We'll
|
||||
// handle the error by displaying it to the user.
|
||||
PrintErrLn(err)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
MaybeError(c, fmt.Sprintf("%s not found in %s", domain, GetHostsPath()))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := SaveOrPreview(options, hostsfile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Disabled %s\n", hostname)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ls command shows a list of hostnames in the hosts file
|
||||
func Ls(c *cli.Context) {
|
||||
hostsfile := AlwaysLoadHostFile(c)
|
||||
maxdomain := 0
|
||||
maxip := 0
|
||||
// List command shows a list of hostnames in the hosts file
|
||||
func List(options *Options) error {
|
||||
hostsfile, err := LoadHostfile(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
widestHostname := 0
|
||||
widestIP := 0
|
||||
|
||||
for _, hostname := range hostsfile.Hosts {
|
||||
dlen := len(hostname.Domain)
|
||||
if dlen > maxdomain {
|
||||
maxdomain = dlen
|
||||
if dlen > widestHostname {
|
||||
widestHostname = dlen
|
||||
}
|
||||
ilen := len(hostname.IP)
|
||||
if ilen > maxip {
|
||||
maxip = ilen
|
||||
if ilen > widestIP {
|
||||
widestIP = ilen
|
||||
}
|
||||
}
|
||||
|
||||
for _, hostname := range hostsfile.Hosts {
|
||||
fmt.Printf("%s -> %s %s\n",
|
||||
StrPadRight(hostname.Domain, maxdomain),
|
||||
StrPadRight(hostname.IP.String(), maxip),
|
||||
StrPadRight(hostname.Domain, widestHostname),
|
||||
StrPadRight(hostname.IP.String(), widestIP),
|
||||
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.
|
||||
`
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
// Format command removes duplicates from the hosts file
|
||||
func Format(options *Options) error {
|
||||
hostsfile, err := LoadHostfile(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
fmt.Printf("%s is already formatted and contains no dupes or conflicts; nothing to do\n", hostess.GetHostsPath())
|
||||
return nil
|
||||
}
|
||||
|
||||
return SaveOrPreview(options, hostsfile)
|
||||
}
|
||||
|
||||
// Dump command outputs hosts file contents as JSON
|
||||
func Dump(c *cli.Context) {
|
||||
hostsfile := AlwaysLoadHostFile(c)
|
||||
func Dump(options *Options) error {
|
||||
hostsfile, err := LoadHostfile(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jsonbytes, err := hostsfile.Hosts.Dump()
|
||||
if err != nil {
|
||||
MaybeError(c, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(fmt.Sprintf("%s", jsonbytes))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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]
|
||||
|
||||
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))
|
||||
return fmt.Errorf("Unable to read JSON from %s: %s", filename, err)
|
||||
}
|
||||
|
||||
hostfile := AlwaysLoadHostFile(c)
|
||||
err = hostfile.Hosts.Apply(jsonbytes)
|
||||
hostfile, err := LoadHostfile(options)
|
||||
if err != nil {
|
||||
MaybeError(c, fmt.Sprintf("Error applying changes to hosts file: %s", err))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := hostfile.Hosts.Apply(jsonbytes); err != nil {
|
||||
return fmt.Errorf("Error applying changes to hosts file: %s", err)
|
||||
}
|
||||
|
||||
if err := SaveOrPreview(options, hostfile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
MaybeSaveHostFile(c, hostfile)
|
||||
MaybePrintln(c, fmt.Sprintf("%s applied", filename))
|
||||
fmt.Printf("%s applied\n", filename)
|
||||
return nil
|
||||
}
|
||||
|
@ -1,23 +1,39 @@
|
||||
package hostess
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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")
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
func TestLoadHostfile(t *testing.T) {
|
||||
// Issue #39: This hosts file contains a duplicate. We should paper over it.
|
||||
os.Setenv("HOSTESS_PATH", filepath.Join("testdata", "issue39"))
|
||||
defer os.Unsetenv("HOSTESS_PATH")
|
||||
options := &Options{}
|
||||
|
||||
if _, err := LoadHostfile(options); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
127.0.0.1 localhost
|
||||
127.0.1.1 robobrain
|
||||
# 192.168.0.1 pie.dev.example.com
|
||||
192.168.0.2 cookie.example.com
|
||||
192.168.1.1 pie.example.com strawberry.pie.example.com
|
||||
::1 localhost cake.example.com hostname.candy hostname.pie
|
||||
fe:23b3:890e:342e::ef chocolate.pie.example.com strawberry.pie.example.com
|
||||
# fe:23b3:890e:342e::ef chocolate.cake.example.com chocolate.cookie.example.com chocolate.ru.example.com chocolate.tr.example.com dev.strawberry.pie.example.com
|
@ -0,0 +1,17 @@
|
||||
127.0.0.1 localhost
|
||||
127.0.1.1 robobrain
|
||||
# 192.168.0.1 pie.dev.example.com
|
||||
192.168.0.2 cookie.example.com
|
||||
192.168.1.1 pie.example.com
|
||||
192.168.1.1 strawberry.pie.example.com
|
||||
::1 localhost
|
||||
::1 cake.example.com
|
||||
::1 hostname.candy
|
||||
::1 hostname.pie
|
||||
# fe:23b3:890e:342e::ef chocolate.cake.example.com
|
||||
# fe:23b3:890e:342e::ef chocolate.cookie.example.com
|
||||
fe:23b3:890e:342e::ef chocolate.pie.example.com
|
||||
# fe:23b3:890e:342e::ef chocolate.ru.example.com
|
||||
# fe:23b3:890e:342e::ef chocolate.tr.example.com
|
||||
# fe:23b3:890e:342e::ef dev.strawberry.pie.example.com
|
||||
fe:23b3:890e:342e::ef strawberry.pie.example.com
|
@ -0,0 +1,12 @@
|
||||
#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
|
||||
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
|
@ -0,0 +1,153 @@
|
||||
// hostess is command-line utility for managing your /etc/hosts file. Works on
|
||||
// Unixes and Windows.
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"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
|
||||
|
||||
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 Usage() {
|
||||
fmt.Print(help, hostess.GetHostsPath())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func CommandUsage(command string) error {
|
||||
return fmt.Errorf("Usage: %s %s <hostname>", os.Args[0], command)
|
||||
}
|
||||
|
||||
func wrappedMain(args []string) error {
|
||||
cli := flag.NewFlagSet(args[0], flag.ExitOnError)
|
||||
preview := cli.Bool("n", false, "preview")
|
||||
cli.Usage = Usage
|
||||
|
||||
command := ""
|
||||
if len(args) > 1 {
|
||||
command = args[1]
|
||||
} else {
|
||||
Usage()
|
||||
}
|
||||
|
||||
if err := cli.Parse(args[2:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options := &Options{
|
||||
Preview: *preview,
|
||||
}
|
||||
|
||||
switch command {
|
||||
|
||||
case "-v", "--version", "version":
|
||||
fmt.Println(Version)
|
||||
return nil
|
||||
|
||||
case "", "-h", "--help", "help":
|
||||
cli.Usage()
|
||||
return nil
|
||||
|
||||
case "fmt":
|
||||
return Format(options)
|
||||
|
||||
case "add":
|
||||
if len(cli.Args()) != 2 {
|
||||
return fmt.Errorf("Usage: %s add <hostname> <ip>", cli.Name())
|
||||
}
|
||||
return Add(options, cli.Arg(0), cli.Arg(1))
|
||||
|
||||
case "rm":
|
||||
if cli.Arg(0) == "" {
|
||||
return CommandUsage(command)
|
||||
}
|
||||
return Remove(options, cli.Arg(0))
|
||||
|
||||
case "on":
|
||||
if cli.Arg(0) == "" {
|
||||
return CommandUsage(command)
|
||||
}
|
||||
return Enable(options, cli.Arg(0))
|
||||
|
||||
case "off":
|
||||
if cli.Arg(0) == "" {
|
||||
return CommandUsage(command)
|
||||
}
|
||||
return Disable(options, cli.Arg(0))
|
||||
|
||||
case "ls":
|
||||
return List(options)
|
||||
|
||||
case "has":
|
||||
if cli.Arg(0) == "" {
|
||||
return CommandUsage(command)
|
||||
}
|
||||
return Has(options, cli.Arg(0))
|
||||
|
||||
case "dump":
|
||||
return Dump(options)
|
||||
|
||||
case "apply":
|
||||
if cli.Arg(0) == "" {
|
||||
return fmt.Errorf("Usage: %s apply <filename>", args[0])
|
||||
}
|
||||
return Apply(options, cli.Arg(0))
|
||||
|
||||
default:
|
||||
return ErrInvalidCommand
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
ExitWithError(wrappedMain(os.Args))
|
||||
}
|
@ -0,0 +1,302 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cbednarski/hostess/hostess"
|
||||
)
|
||||
|
||||
// CopyHostsFile creates a temporary hosts file in the system temp directory,
|
||||
// sets the HOSTESS_PATH environment variable, and returns the file path and a
|
||||
// cleanup function
|
||||
func CopyHostsFile(t *testing.T, fixtureFiles ...string) (string, func()) {
|
||||
t.Helper()
|
||||
|
||||
fixtureFile := filepath.Join("testdata", "ubuntu.hosts")
|
||||
|
||||
// This is an optional argument so we'll default to the ubuntu.hosts above
|
||||
// and only accept arity 1 if the user passes in extra data
|
||||
if len(fixtureFiles) > 1 {
|
||||
t.Fatalf("%s supplied too many arguments to CopyHostsFile", t.Name())
|
||||
} else if len(fixtureFiles) == 1 {
|
||||
fixtureFile = fixtureFiles[0]
|
||||
}
|
||||
|
||||
fixture, err := os.Open(fixtureFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
temp, err := ioutil.TempFile("", "hostess-test-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(temp, fixture); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.Setenv(hostess.EnvHostessPath, temp.Name()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
os.Remove(temp.Name())
|
||||
os.Unsetenv(hostess.EnvHostessPath)
|
||||
}
|
||||
|
||||
return temp.Name(), cleanup
|
||||
}
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
temp, cleanup := CopyHostsFile(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := wrappedMain(strings.Split("hostess fmt", " ")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(temp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := string(data)
|
||||
|
||||
expected := `127.0.0.1 localhost myapp.local
|
||||
127.0.1.1 ubuntu
|
||||
192.168.0.30 raspberrypi
|
||||
::1 ip6-localhost ip6-loopback
|
||||
fe00:: ip6-localnet
|
||||
ff00:: ip6-mcastprefix
|
||||
ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
||||
`
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
expected = `127.0.0.1 localhost
|
||||
127.0.0.1 myapp.local
|
||||
127.0.1.1 ubuntu
|
||||
192.168.0.30 raspberrypi
|
||||
::1 ip6-localhost
|
||||
::1 ip6-loopback
|
||||
fe00:: ip6-localnet
|
||||
ff00:: ip6-mcastprefix
|
||||
ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
||||
`
|
||||
}
|
||||
|
||||
if output != expected {
|
||||
t.Errorf("--- Expected ---\n%s\n--- Found ---\n%s\n", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddHostname(t *testing.T) {
|
||||
temp, cleanup := CopyHostsFile(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := wrappedMain(strings.Split("hostess add my.new.website 127.0.0.1", " ")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := wrappedMain(strings.Split("hostess add mediaserver 192.168.0.82", " ")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := wrappedMain(strings.Split("hostess add myapp.local 10.20.0.23", " ")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(temp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := string(data)
|
||||
|
||||
expected := `127.0.0.1 localhost my.new.website
|
||||
127.0.1.1 ubuntu
|
||||
10.20.0.23 myapp.local
|
||||
192.168.0.30 raspberrypi
|
||||
192.168.0.82 mediaserver
|
||||
::1 ip6-localhost ip6-loopback
|
||||
fe00:: ip6-localnet
|
||||
ff00:: ip6-mcastprefix
|
||||
ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
||||
`
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
expected = `127.0.0.1 localhost
|
||||
127.0.0.1 my.new.website
|
||||
127.0.1.1 ubuntu
|
||||
10.20.0.23 myapp.local
|
||||
192.168.0.30 raspberrypi
|
||||
192.168.0.82 mediaserver
|
||||
::1 ip6-localhost
|
||||
::1 ip6-loopback
|
||||
fe00:: ip6-localnet
|
||||
ff00:: ip6-mcastprefix
|
||||
ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
||||
`
|
||||
}
|
||||
|
||||
if output != expected {
|
||||
t.Errorf("--- Expected ---\n%s\n--- Found ---\n%s\n", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveHostname(t *testing.T) {
|
||||
temp, cleanup := CopyHostsFile(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := wrappedMain(strings.Split("hostess rm myapp.local", " ")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := wrappedMain(strings.Split("hostess rm raspberrypi", " ")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(temp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := string(data)
|
||||
|
||||
expected := `127.0.0.1 localhost
|
||||
127.0.1.1 ubuntu
|
||||
::1 ip6-localhost ip6-loopback
|
||||
fe00:: ip6-localnet
|
||||
ff00:: ip6-mcastprefix
|
||||
ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
||||
`
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
expected = `127.0.0.1 localhost
|
||||
127.0.1.1 ubuntu
|
||||
::1 ip6-localhost
|
||||
::1 ip6-loopback
|
||||
fe00:: ip6-localnet
|
||||
ff00:: ip6-mcastprefix
|
||||
ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
||||
`
|
||||
}
|
||||
|
||||
if output != expected {
|
||||
t.Errorf("--- Expected ---\n%s\n--- Found ---\n%s\n", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostnameOff(t *testing.T) {
|
||||
temp, cleanup := CopyHostsFile(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := wrappedMain(strings.Split("hostess off myapp.local", " ")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := wrappedMain(strings.Split("hostess off raspberrypi", " ")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(temp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := string(data)
|
||||
|
||||
expected := `127.0.0.1 localhost
|
||||
# 127.0.0.1 myapp.local
|
||||
127.0.1.1 ubuntu
|
||||
# 192.168.0.30 raspberrypi
|
||||
::1 ip6-localhost ip6-loopback
|
||||
fe00:: ip6-localnet
|
||||
ff00:: ip6-mcastprefix
|
||||
ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
||||
`
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
expected = `127.0.0.1 localhost
|
||||
# 127.0.0.1 myapp.local
|
||||
127.0.1.1 ubuntu
|
||||
# 192.168.0.30 raspberrypi
|
||||
::1 ip6-localhost
|
||||
::1 ip6-loopback
|
||||
fe00:: ip6-localnet
|
||||
ff00:: ip6-mcastprefix
|
||||
ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
||||
`
|
||||
}
|
||||
|
||||
if output != expected {
|
||||
t.Errorf("--- Expected ---\n%s\n--- Found ---\n%s\n", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExitCodeFmt(t *testing.T) {
|
||||
temp, cleanup := CopyHostsFile(t, filepath.Join("testdata", "issue39"))
|
||||
defer cleanup()
|
||||
|
||||
state1, err := ioutil.ReadFile(temp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("%s", state1)
|
||||
|
||||
if err := wrappedMain([]string{"hostess", "fmt", "-n"}); err != ErrParsingHostsFile {
|
||||
t.Fatalf(`Expected %q, found %v`, ErrParsingHostsFile, err)
|
||||
}
|
||||
|
||||
state2, err := ioutil.ReadFile(temp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(state1, state2) {
|
||||
t.Error("Expected hosts contents before and after fix -n to be the same")
|
||||
}
|
||||
|
||||
if err := wrappedMain([]string{"hostess", "fmt"}); err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
finalExpected := `127.0.0.1 localhost kubernetes.docker.internal
|
||||
::1 localhost
|
||||
`
|
||||
if runtime.GOOS == "windows" {
|
||||
finalExpected = `127.0.0.1 localhost
|
||||
127.0.0.1 kubernetes.docker.internal
|
||||
::1 localhost
|
||||
`
|
||||
}
|
||||
|
||||
state3, err := ioutil.ReadFile(temp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(state3) != finalExpected {
|
||||
t.Fatalf("---- Expected ----\n%s\n---- Found ----\n%s\n", finalExpected, string(state3))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoCommand(t *testing.T) {
|
||||
if err := wrappedMain([]string{"main"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoArgs(t *testing.T) {
|
||||
if err := wrappedMain([]string{"main", ""}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
127.0.0.1 localhost kubernetes.docker.internal
|
||||
::1 localhost
|
||||
# Added by Docker Desktop
|
||||
# To allow the same kube context to work on the host and the container:
|
||||
127.0.0.1 kubernetes.docker.internal
|
||||
# End of section
|
@ -0,0 +1,10 @@
|
||||
127.0.0.1 localhost myapp.local
|
||||
127.0.1.1 ubuntu
|
||||
192.168.0.30 raspberrypi
|
||||
|
||||
# The following lines are desirable for IPv6 capable hosts
|
||||
::1 ip6-localhost ip6-loopback
|
||||
fe00::0 ip6-localnet
|
||||
ff00::0 ip6-mcastprefix
|
||||
ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
@ -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