Compare commits

..

No commits in common. 'master' and 'v0.3.0' have entirely different histories.

@ -1,9 +0,0 @@
install:
- go version
build: false
deploy: false
test_script:
- go test ./...
- go vet ./...

10
.gitignore vendored

@ -1,3 +1,7 @@
/.idea/
/bin/
.DS_Store
/hostess
/hostess.exe
/hostess_*
/coverage.out
/coverage.html
/.vscode
/.idea

@ -1,8 +1,8 @@
dist: bionic
language: go
go:
- 1.14.x
- 1.9.2
- master
notifications:
email: false

@ -1,89 +0,0 @@
# 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

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

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015-2020 Chris Bednarski
Copyright (c) 2015 Chris Bednarski
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

@ -1,20 +1,42 @@
RELEASE_VERSION=$(shell git describe --tags)
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
test:
go test ./...
go vet ./...
go test -coverprofile=coverage.out; go tool cover -html=coverage.out -o coverage.html
go vet $(PACKAGES)
golint $(PACKAGES)
install:
go build -o bin/hostess .
sudo mv bin/hostess /usr/local/bin/hostess
gox:
go get github.com/mitchellh/gox
gox -build-toolchain
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 .
build-all: test
which gox || make gox
gox -arch="386 amd64 arm" -os="darwin linux windows" github.com/cbednarski/hostess/cmd/hostess
clean:
rm -rf ./bin/
install: hostess
mkdir -p $(bindir)
cp hostess $(bindir)/hostess
.PHONY: install test release clean
clean:
rm -f ./hostess
rm -f ./hostess_*
rm -f ./coverage.*

@ -1,6 +1,6 @@
# hostess [![Linux Build Status](https://travis-ci.org/cbednarski/hostess.svg)](https://travis-ci.org/cbednarski/hostess) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/wtxqb880b7v9dfgn/branch/master?svg=true)](https://ci.appveyor.com/project/cbednarski/hostess/branch/master) [![GoDoc](https://godoc.org/github.com/cbednarski/hostess?status.svg)](http://godoc.org/github.com/cbednarski/hostess)
# hostess [![](https://travis-ci.org/cbednarski/hostess.svg)](https://travis-ci.org/cbednarski/hostess) [![Coverage Status](https://coveralls.io/repos/cbednarski/hostess/badge.svg)](https://coveralls.io/r/cbednarski/hostess) [![GoDoc](https://godoc.org/github.com/cbednarski/hostess?status.svg)](http://godoc.org/github.com/cbednarski/hostess)
An **idempotent** command-line utility for managing your `/etc/hosts`* file.
An **idempotent** command-line utility for managing your `/etc/hosts` file.
hostess add local.example.com 127.0.0.1
hostess add staging.example.com 10.0.2.16
@ -10,63 +10,67 @@ Because sometimes DNS doesn't work in production. And because editing
`/etc/hosts` by hand is a pain. Put hostess in your `Makefile` or deploy scripts
and call it a day.
\* And `C:\Windows\System32\drivers\etc\hosts` on Windows.
## Using Hostess
**Note: 0.4.0 has backwards incompatible changes in the API and CLI.** See
`CHANGELOG.md` for details.
## Installation
### Download and Install
Download a [precompiled release](https://github.com/cbednarski/hostess/releases)
from GitHub, or build from source (with a [recent version of Go](https://golang.org/dl)):
from GitHub.
git clone https://github.com/cbednarski/hostess
cd hostess
make install
Builds are supported for OSX, Linux, Windows, and RaspberryPi.
## Usage
### Usage
Run `hostess` or `hostess -h` to see a full list of commands.
hostess add domain ip # Add or replace a hosts entry for this domain pointing to this IP
hostess aff domain ip # Add or replace a hosts entry in an off state
hostess del domain # (alias rm) Remove a domain from your hosts file
hostess has domain # exit code 0 if the domain is in your hostfile, 1 otherwise
hostess off domain # Disable a domain (but don't remove it completely), exit 1 if entry is missing
hostess on domain # Re-enable a domain that was disabled, exit 1 if entry is missing
hostess list # (alias ls) List domains, target ips, and on/off status
hostess fix # Rewrite your hosts file; use -n to dry run
hostess dump # Dump your hostfile as json
hostess apply # Add entries from a json file
**Note** The hosts file is protected. On unixes you will need to use `sudo` or
run the `hostess` command as root. On Windows, you will need to run `hostess`
from an elevated prompt (right click and _Run as administrator_).
Flags
## Format
-n # Dry run. Show what will happen but don't do it; output to stdout
-4 # Limit operation to ipv4 entries
-6 # Limit operation to ipv6 entries
On unixes, hostess follows the format specified by `man hosts`, with one line
per IP address:
hostess will mangle your hosts file. Domains pointing at the same IP will be
grouped together and disabled domains commented out.
127.0.0.1 localhost hostname2 hostname3
127.0.1.1 machine.name
# 10.10.20.30 some.host
On Windows, hostess writes each hostname on its own line.
### IPv4 and IPv6
127.0.0.1 localhost
127.0.0.1 hostname2
127.0.0.1 hostname3
Your hosts file *can* contain overlapping entries where the same hostname points
to both an IPv4 and IPv6 IP. In this case, hostess commands will apply to both
entries. Typically you won't have this kind of overlap and the default behavior
is OK. However, if you need to be more granular you can use `-4` or `-6` to
limit operations to entries associated with that type of IP.
## Configuration
## Developing Hostess
hostess may be configured via environment variables.
### Configuration
- `HOSTESS_FMT` may be set to `windows` or `unix` to override platform detection
for the hosts file format. See Behavior, above, for details
By default, hostess will read / write to `/etc/hosts`. You can use the
`HOSTESS_PATH` environment variable to provide an alternate path (for testing).
- `HOSTESS_PATH` may be set to override platform detection for the location of
the hosts file. By default this is `C:\Windows\System32\drivers\etc\hosts` on
Windows and `/etc/hosts` everywhere else.
### Building from Source
## IPv4 and IPv6
To build from source you'll need to have go 1.4+
It's possible for your hosts file to include overlapping entries for IPv4 and
IPv6. This is an uncommon case so the CLI ignores this distinction. The hostess
library includes logic that differentiates between these cases.
#### Install with go get
## Contributing
go get github.com/cbednarski/hostess/cmd/hostess
I hope my software is useful, readable, fun to use, and helps you learn
something new. I maintain this software in my spare time. I rarely merge PRs
because I am both lazy and a snob. Bug reports, fixes, questions, and comments
are welcome but expect a delayed response. No refunds. 👻
#### Install from source
git clone https://github.com/cbednarski/hostess
cd hostess
make
make install

@ -0,0 +1,125 @@
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,280 +1,289 @@
package main
package hostess
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/cbednarski/hostess/hostess"
"github.com/codegangsta/cli"
)
var ErrParsingHostsFile = errors.New("Errors while parsing hostsfile. Please resolve any conflicts and try again.")
type Options struct {
Preview bool
// 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)
}
// PrintErrLn will print to stderr followed by a newline
func PrintErrLn(err error) {
os.Stderr.WriteString(fmt.Sprintf("%s\n", err))
// ErrCantWriteHostFile indicates that we are unable to write to the hosts file
var ErrCantWriteHostFile = fmt.Errorf(
"Unable to write to %s. Maybe you need to sudo?", GetHostsPath())
// MaybeErrorln will print an error message unless -s is passed
func MaybeErrorln(c *cli.Context, message string) {
if !AnyBool(c, "s") {
os.Stderr.WriteString(fmt.Sprintf("%s\n", message))
}
}
// 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()
// 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)
}
var err error
// 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)
}
}
if len(errs) > 0 {
// If -n is passed, we'll always exit with an error code on duplicate.
// See issue 39
if options.Preview {
err = ErrParsingHostsFile
// 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
}
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
}
// AlwaysLoadHostFile will load, parse, and return a Hostfile. If we encouter
// errors they will be printed to the terminal, but we'll try to continue.
func AlwaysLoadHostFile(c *cli.Context) *Hostfile {
hostsfile, errs := LoadHostfile()
if len(errs) > 0 {
for _, err := range errs {
MaybeErrorln(c, err.Error())
}
}
return hosts, err
return hostsfile
}
// SaveOrPreview will display or write the Hostfile
func SaveOrPreview(options *Options, hostfile *hostess.Hostfile) error {
// MaybeSaveHostFile will output or write the Hostfile, or exit 1 and error.
func MaybeSaveHostFile(c *cli.Context, hostfile *Hostfile) {
// If -n is passed, no-op and output the resultant hosts file to stdout.
// Otherwise it's for real and we're going to write it.
if options.Preview {
if AnyBool(c, "n") {
fmt.Printf("%s", hostfile.Format())
return nil
}
if err := hostfile.Save(); err != nil {
return fmt.Errorf("Unable to write to %s. (error: %s)", hostess.GetHostsPath(), err)
} else {
err := hostfile.Save()
if err != nil {
MaybeError(c, ErrCantWriteHostFile.Error())
}
}
return nil
}
// StrPadRight adds spaces to the right of a string until it reaches length.
// StrPadRight adds spaces to the right of a string until it reaches l length.
// If the input string is already that long, do nothing.
func StrPadRight(input string, length int) string {
minimum := len(input)
if length <= minimum {
return input
func StrPadRight(s string, l int) string {
r := l - len(s)
if r < 0 {
r = 0
}
return input + strings.Repeat(" ", length-minimum)
return s + strings.Repeat(" ", r)
}
// Add command parses <hostname> <ip> and adds or updates a hostname in the
// hosts file
func Add(options *Options, hostname, ip string) error {
hostsfile, err := LoadHostfile(options)
// 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)
newHostname, err := hostess.NewHostname(hostname, ip, true)
hostname, err := NewHostname(c.Args()[0], c.Args()[1], true)
if err != nil {
return err
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
}
replaced := hostsfile.Hosts.ContainsDomain(newHostname.Domain)
replace := hostsfile.Hosts.ContainsDomain(hostname.Domain)
// Note that Add() may return an error, but they are informational only. We
// don't actually care what the error is -- we just want to add the
// hostname and save the file. This way the behavior is idempotent.
hostsfile.Hosts.Add(newHostname)
hostsfile.Hosts.Add(hostname)
// If the user passes -n then we'll Add and show the new hosts file, but
// not save it.
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())
if c.Bool("n") || AnyBool(c, "n") {
fmt.Printf("%s", hostsfile.Format())
} else {
fmt.Printf("Added %s\n", newHostname.FormatHuman())
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()))
}
}
return nil
}
// 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
// Del command removes any hostname(s) matching <domain> from the hosts file
func Del(c *cli.Context) {
if len(c.Args()) != 1 {
MaybeError(c, "expected <hostname>")
}
domain := c.Args()[0]
hostsfile := MaybeLoadHostFile(c)
found := hostsfile.Hosts.ContainsDomain(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
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()))
}
fmt.Printf("Deleted %s\n", hostname)
return nil
}
// Has command indicates whether a hostname is present in the hosts file
func Has(options *Options, hostname string) error {
hostsfile, err := LoadHostfile(options)
if err != nil {
return err
func Has(c *cli.Context) {
if len(c.Args()) != 1 {
MaybeError(c, "expected <hostname>")
}
domain := c.Args()[0]
hostsfile := MaybeLoadHostFile(c)
found := hostsfile.Hosts.ContainsDomain(hostname)
found := hostsfile.Hosts.ContainsDomain(domain)
if found {
fmt.Printf("Found %s in %s\n", hostname, hostess.GetHostsPath())
MaybePrintln(c, fmt.Sprintf("Found %s in %s", domain, GetHostsPath()))
} else {
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)
MaybeError(c, fmt.Sprintf("%s not found in %s", domain, GetHostsPath()))
}
return nil
}
func Enable(options *Options, hostname string) error {
hostsfile, err := LoadHostfile(options)
if err != nil {
return err
}
if err := hostsfile.Hosts.Enable(hostname); err != nil {
return err
}
if err := SaveOrPreview(options, hostsfile); err != nil {
return err
}
fmt.Printf("Enabled %s\n", hostname)
return nil
}
func Disable(options *Options, hostname string) error {
hostsfile, err := LoadHostfile(options)
if err != nil {
return err
// OnOff enables (uncomments) or disables (comments) the specified hostname in
// the hosts file. Exits code 1 if the hostname is missing.
func OnOff(c *cli.Context) {
if len(c.Args()) != 1 {
MaybeError(c, "expected <hostname>")
}
domain := c.Args()[0]
hostsfile := MaybeLoadHostFile(c)
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
}
return err
// Switch on / off commands
success := false
if c.Command.Name == "on" {
success = hostsfile.Hosts.Enable(domain)
} else {
success = hostsfile.Hosts.Disable(domain)
}
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))
}
} else {
MaybeError(c, fmt.Sprintf("%s not found in %s", domain, GetHostsPath()))
}
fmt.Printf("Disabled %s\n", hostname)
return nil
}
// 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
// Ls command shows a list of hostnames in the hosts file
func Ls(c *cli.Context) {
hostsfile := AlwaysLoadHostFile(c)
maxdomain := 0
maxip := 0
for _, hostname := range hostsfile.Hosts {
dlen := len(hostname.Domain)
if dlen > widestHostname {
widestHostname = dlen
if dlen > maxdomain {
maxdomain = dlen
}
ilen := len(hostname.IP)
if ilen > widestIP {
widestIP = ilen
if ilen > maxip {
maxip = ilen
}
}
for _, hostname := range hostsfile.Hosts {
fmt.Printf("%s -> %s %s\n",
StrPadRight(hostname.Domain, widestHostname),
StrPadRight(hostname.IP.String(), widestIP),
StrPadRight(hostname.Domain, maxdomain),
StrPadRight(hostname.IP.String(), maxip),
hostname.FormatEnabled())
}
return nil
}
// Format command removes duplicates from the hosts file
func Format(options *Options) error {
hostsfile, err := LoadHostfile(options)
if err != nil {
return err
}
const fixHelp = `Programmatically rewrite your hostsfile.
Domains pointing to the same IP will be consolidated onto single lines and
sorted. Duplicates and conflicts will be removed. Extra whitespace and comments
will be removed.
hostess fix Rewrite the hostsfile
hostess fix -n Show the new hostsfile. Don't write it to disk.
`
// Fix command removes duplicates and conflicts from the hosts file
func Fix(c *cli.Context) {
hostsfile := AlwaysLoadHostFile(c)
if bytes.Equal(hostsfile.GetData(), hostsfile.Format()) {
fmt.Printf("%s is already formatted and contains no dupes or conflicts; nothing to do\n", hostess.GetHostsPath())
return nil
MaybePrintln(c, fmt.Sprintf("%s is already formatted and contains no dupes or conflicts; nothing to do", GetHostsPath()))
os.Exit(0)
}
return SaveOrPreview(options, hostsfile)
MaybeSaveHostFile(c, hostsfile)
}
// Dump command outputs hosts file contents as JSON
func Dump(options *Options) error {
hostsfile, err := LoadHostfile(options)
if err != nil {
return err
// Fixed command removes duplicates and conflicts from the hosts file
func Fixed(c *cli.Context) {
hostsfile := AlwaysLoadHostFile(c)
if bytes.Equal(hostsfile.GetData(), hostsfile.Format()) {
MaybePrintln(c, fmt.Sprintf("%s is already formatted and contains no dupes or conflicts", GetHostsPath()))
os.Exit(0)
} else {
MaybePrintln(c, fmt.Sprintf("%s is not formatted. Use hostess fix to format it", GetHostsPath()))
os.Exit(1)
}
}
// Dump command outputs hosts file contents as JSON
func Dump(c *cli.Context) {
hostsfile := AlwaysLoadHostFile(c)
jsonbytes, err := hostsfile.Hosts.Dump()
if err != nil {
return err
MaybeError(c, err.Error())
}
fmt.Println(fmt.Sprintf("%s", jsonbytes))
return nil
}
// Apply command adds hostnames to the hosts file from JSON
func Apply(options *Options, filename string) error {
jsonbytes, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("Unable to read JSON from %s: %s", filename, err)
func Apply(c *cli.Context) {
if len(c.Args()) != 1 {
MaybeError(c, "Usage should be apply [filename]")
}
filename := c.Args()[0]
hostfile, err := LoadHostfile(options)
jsonbytes, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
if err := hostfile.Hosts.Apply(jsonbytes); err != nil {
return fmt.Errorf("Error applying changes to hosts file: %s", err)
MaybeError(c, fmt.Sprintf("Unable to read %s: %s", filename, err))
}
if err := SaveOrPreview(options, hostfile); err != nil {
return err
hostfile := AlwaysLoadHostFile(c)
err = hostfile.Hosts.Apply(jsonbytes)
if err != nil {
MaybeError(c, fmt.Sprintf("Error applying changes to hosts file: %s", err))
}
fmt.Printf("%s applied\n", filename)
return nil
MaybeSaveHostFile(c, hostfile)
MaybePrintln(c, fmt.Sprintf("%s applied", filename))
}

@ -1,39 +1,23 @@
package main
package hostess
import (
"flag"
"os"
"path/filepath"
"testing"
"github.com/codegangsta/cli"
"github.com/stretchr/testify/assert"
)
func TestStrPadRight(t *testing.T) {
type testCase struct {
Expected string
Output string
Name string
}
cases := []testCase{
{"", StrPadRight("", 0), "Zero-length no padding"},
{" ", StrPadRight("", 10), "Zero-length 10 padding"},
{"string", StrPadRight("string", 0), "6-length 0 padding"},
}
for _, test := range cases {
if test.Output != test.Expected {
t.Errorf("Failed case: %s\nExpected %q Found %q", test.Name, test.Expected, test.Output)
}
}
assert.Equal(t, "", StrPadRight("", 0), "Zero-length no padding")
assert.Equal(t, " ", StrPadRight("", 10), "Zero-length 10 padding")
assert.Equal(t, "string", StrPadRight("string", 0), "6-length 0 padding")
}
func 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)
}
func TestLs(t *testing.T) {
os.Setenv("HOSTESS_PATH", "test-fixtures/hostfile1")
defer os.Setenv("HOSTESS_PATH", "")
c := cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil)
Ls(c)
}

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

@ -1,8 +0,0 @@
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

@ -1,17 +0,0 @@
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

@ -1,12 +0,0 @@
#192.168.0.1 pie.dev.example.com
192.168.0.2 cookie.example.com
::1 hostname.pie hostname.candy cake.example.com
fe:23b3:890e:342e::ef strawberry.pie.example.com
# fe:23b3:890e:342e::ef dev.strawberry.pie.example.com
127.0.1.1 robobrain
# fe:23b3:890e:342e::ef chocolate.cake.example.com chocolate.ru.example.com chocolate.tr.example.com chocolate.cookie.example.com
fe:23b3:890e:342e::ef chocolate.pie.example.com
::1 localhost
127.0.0.1 localhost
192.168.1.1 pie.example.com
192.168.1.1 strawberry.pie.example.com

@ -1,5 +1,3 @@
// Package hostess provides an API to interact with /etc/hosts and its Windows
// equivalent.
package hostess
import (
@ -10,8 +8,6 @@ import (
"strings"
)
const EnvHostessPath = `HOSTESS_PATH`
const defaultOSX = `
##
# Host Database
@ -55,7 +51,7 @@ func NewHostfile() *Hostfile {
// GetHostsPath returns the location of the hostfile; either env HOSTESS_PATH
// or /etc/hosts if HOSTESS_PATH is not set.
func GetHostsPath() string {
path := os.Getenv(EnvHostessPath)
path := os.Getenv("HOSTESS_PATH")
if path == "" {
if runtime.GOOS == "windows" {
path = "C:\\Windows\\System32\\drivers\\etc\\hosts"
@ -196,26 +192,7 @@ func (h *Hostfile) Format() []byte {
// Save writes the Hostfile to disk to /etc/hosts or to the location specified
// by the HOSTESS_PATH environment variable (if set).
func (h *Hostfile) Save() error {
var file *os.File
var err error
// TODO break platform-specific code into separate functions
// Windows wants the file to be truncated before it's opened. Then we re-
// write the entire file contents. Truncating up front is risky but I don't
// know of a better way to do it.
if runtime.GOOS == "windows" {
if err := os.Truncate(h.Path, 0); err != nil {
return err
}
file, err = os.OpenFile(h.Path, os.O_RDWR, 0644)
} else {
// TODO use atomic write-and-rename on Unix
// I think an earlier version of the program did this but it did not
// work on Windows so it was rolled back. We can probably get that code
// from history.
file, err = os.OpenFile(h.Path, os.O_RDWR|os.O_APPEND|os.O_TRUNC, 0644)
}
file, err := os.OpenFile(h.Path, os.O_RDWR|os.O_APPEND|os.O_TRUNC, 0644)
if err != nil {
return err
}

@ -2,15 +2,11 @@ package hostess_test
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/cbednarski/hostess/hostess"
"github.com/cbednarski/hostess"
)
const ipv4Pass = `
@ -61,22 +57,12 @@ func TestFormatHostfile(t *testing.T) {
// 1. We want localhost entries at the top
// 2. The rest are sorted by IP as STRINGS, not numeric values, so 10
// precedes 8
expected := `127.0.0.1 localhost devsite
const expected = `127.0.0.1 localhost devsite
127.0.1.1 ip-10-37-12-18
# 8.8.8.8 google.com
10.37.12.18 devsite.com m.devsite.com
`
if runtime.GOOS == "windows" {
expected = `127.0.0.1 localhost
127.0.0.1 devsite
127.0.1.1 ip-10-37-12-18
# 8.8.8.8 google.com
10.37.12.18 devsite.com
10.37.12.18 m.devsite.com
`
}
hostfile := hostess.NewHostfile()
hostfile.Path = "./hosts"
hostfile.Hosts.Add(hostess.MustHostname("localhost", "127.0.0.1", true))
@ -214,30 +200,3 @@ func TestLoadHostfile(t *testing.T) {
t.Errorf("Expected to find %#v", hostname)
}
}
func TestSaveHostfile(t *testing.T) {
fixture, err := os.Open(filepath.Join("testdata", "hostfile1"))
if err != nil {
t.Fatal(err)
}
tempfile, err := ioutil.TempFile("", "hostess-test-*")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tempfile.Name())
if _, err := io.Copy(tempfile, fixture); err != nil {
t.Fatal(err)
}
hostfile := hostess.NewHostfile()
hostfile.Path = tempfile.Name()
if err := hostfile.Read(); err != nil {
t.Fatal(err)
}
if err := hostfile.Save(); err != nil {
t.Fatal(err)
}
}

@ -1,23 +1,17 @@
package hostess
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"runtime"
"sort"
"strings"
)
const EnvHostessFmt = `HOSTESS_FMT`
// ErrInvalidVersionArg is raised when a function expects IPv 4 or 6 but is
// passed a value not 4 or 6.
var ErrInvalidVersionArg = errors.New("version argument must be 4 or 6")
var ErrHostnameNotFound = errors.New("hostname not found")
var ErrInvalidVersionArg = errors.New("Version argument must be 4 or 6")
// Hostlist is a sortable set of Hostnames. When in a Hostlist, Hostnames must
// follow some rules:
@ -138,10 +132,10 @@ func (h Hostlist) Swap(i, j int) {
// Sort this list of Hostnames, according to Hostlist sorting rules:
//
// 1. localhost comes before other hostnames
// 1. localhost comes before other domains
// 2. IPv4 comes before IPv6
// 3. IPs are sorted in numerical order
// 4. The remaining hostnames are sorted in lexicographical order
// 4. domains are sorted in alphabetical
func (h *Hostlist) Sort() {
sort.Sort(*h)
}
@ -184,28 +178,28 @@ func (h *Hostlist) ContainsIP(IP net.IP) bool {
//
// Both duplicate and conflicts return errors so you are aware of them, but you
// don't necessarily need to do anything about the error.
func (h *Hostlist) Add(input *Hostname) error {
newHostname, err := NewHostname(input.Domain, input.IP.String(), input.Enabled)
func (h *Hostlist) Add(hostnamev *Hostname) error {
hostname, err := NewHostname(hostnamev.Domain, hostnamev.IP.String(), hostnamev.Enabled)
if err != nil {
return err
}
for index, found := range *h {
if found.Equal(newHostname) {
if found.Equal(hostname) {
// If either hostname is enabled we will set the existing one to
// enabled state. That way if we add a hostname from the end of a
// hosts file it will take over, and if we later add a disabled one
// the original one will stick. We still error in this case so the
// user can see that there is a duplicate.
(*h)[index].Enabled = found.Enabled || newHostname.Enabled
return fmt.Errorf("duplicate hostname entry for %s -> %s",
newHostname.Domain, newHostname.IP)
} else if found.Domain == newHostname.Domain && found.IPv6 == newHostname.IPv6 {
(*h)[index] = newHostname
return fmt.Errorf("conflicting hostname entries for %s -> %s and -> %s",
newHostname.Domain, newHostname.IP, found.IP)
(*h)[index].Enabled = found.Enabled || hostname.Enabled
return fmt.Errorf("Duplicate hostname entry for %s -> %s",
hostname.Domain, hostname.IP)
} else if found.Domain == hostname.Domain && found.IPv6 == hostname.IPv6 {
(*h)[index] = hostname
return fmt.Errorf("Conflicting hostname entries for %s -> %s and -> %s",
hostname.Domain, hostname.IP, found.IP)
}
}
*h = append(*h, newHostname)
*h = append(*h, hostname)
return nil
}
@ -258,58 +252,62 @@ func (h *Hostlist) RemoveDomainV(domain string, version int) int {
return h.Remove(h.IndexOfDomainV(domain, version))
}
// Enable will change any Hostnames matching name to be enabled.
func (h *Hostlist) Enable(name string) error {
// Enable will change any Hostnames matching domain to be enabled.
func (h *Hostlist) Enable(domain string) bool {
found := false
for _, hostname := range *h {
if hostname.Domain == name {
if hostname.Domain == domain {
hostname.Enabled = true
return nil
found = true
}
}
return ErrHostnameNotFound
return found
}
// EnableV will change a Hostname matching domain and IP version to be enabled.
//
// This function will panic if IP version is not 4 or 6.
func (h *Hostlist) EnableV(domain string, version int) error {
func (h *Hostlist) EnableV(domain string, version int) bool {
found := false
if version != 4 && version != 6 {
return ErrInvalidVersionArg
panic(ErrInvalidVersionArg)
}
for _, hostname := range *h {
if hostname.Domain == domain && hostname.IPv6 == (version == 6) {
hostname.Enabled = true
return nil
found = true
}
}
return ErrHostnameNotFound
return found
}
// Disable will change any Hostnames matching name to be disabled.
func (h *Hostlist) Disable(name string) error {
// Disable will change any Hostnames matching domain to be disabled.
func (h *Hostlist) Disable(domain string) bool {
found := false
for _, hostname := range *h {
if hostname.Domain == name {
if hostname.Domain == domain {
hostname.Enabled = false
return nil
found = true
}
}
return ErrHostnameNotFound
return found
}
// DisableV will change any Hostnames matching domain and IP version to be disabled.
//
// This function will panic if IP version is not 4 or 6.
func (h *Hostlist) DisableV(domain string, version int) error {
func (h *Hostlist) DisableV(domain string, version int) bool {
found := false
if version != 4 && version != 6 {
return ErrInvalidVersionArg
panic(ErrInvalidVersionArg)
}
for _, hostname := range *h {
if hostname.Domain == domain && hostname.IPv6 == (version == 6) {
hostname.Enabled = false
return nil
found = true
}
}
return ErrHostnameNotFound
return found
}
// FilterByIP filters the list of hostnames by IP address.
@ -377,9 +375,9 @@ func (h *Hostlist) GetUniqueIPs() []net.IP {
// 2. Commented items are sorted displayed
// 3. 127.* appears at the top of the list (so boot resolvers don't break)
// 4. When present, "localhost" will always appear first in the domain list
func (h *Hostlist) FormatLinux() []byte {
func (h *Hostlist) Format() []byte {
h.Sort()
out := bytes.Buffer{}
out := []byte{}
// We want to output one line of hostnames per IP, so first we get that
// list of IPs and iterate.
@ -403,49 +401,19 @@ func (h *Hostlist) FormatLinux() []byte {
// Finally, if the bucket contains anything, concatenate it all
// together and append it to the output. Also add a newline.
if len(enabledIPs) > 0 {
out.WriteString(fmt.Sprintf("%s %s\n", IP.String(), strings.Join(enabledIPs, " ")))
concat := fmt.Sprintf("%s %s", IP.String(), strings.Join(enabledIPs, " "))
out = append(out, []byte(concat)...)
out = append(out, []byte("\n")...)
}
if len(disabledIPs) > 0 {
out.WriteString(fmt.Sprintf("# %s %s\n", IP.String(), strings.Join(disabledIPs, " ")))
concat := fmt.Sprintf("# %s %s", IP.String(), strings.Join(disabledIPs, " "))
out = append(out, []byte(concat)...)
out = append(out, []byte("\n")...)
}
}
return out.Bytes()
}
func (h Hostlist) FormatWindows() []byte {
h.Sort()
out := bytes.Buffer{}
for _, hostname := range h {
out.WriteString(hostname.Format())
out.WriteString("\n")
}
return out.Bytes()
}
func (h *Hostlist) Format() []byte {
format := os.Getenv(EnvHostessFmt)
if format == "" {
format = runtime.GOOS
}
switch format {
case "windows":
return h.FormatWindows()
case "unix":
return h.FormatLinux()
default:
// Theoretically the Windows format might be more compatible but there
// are a lot of different operating systems, and they're almost all
// unix-based OSes, so we'll just assume the linux format is OK. For
// example, FreeBSD, MacOS, and Linux all use the same format and while
// I haven't checked OpenBSD or NetBSD, I am going to assume they are
// OK with this format. If not we can add a case above.
return h.FormatLinux()
}
return out
}
// Dump exports all entries in the Hostlist as JSON

@ -3,30 +3,25 @@ package hostess_test
import (
"bytes"
"fmt"
"io/ioutil"
"net"
"path/filepath"
"testing"
"github.com/cbednarski/hostess/hostess"
"github.com/stretchr/testify/assert"
"github.com/cbednarski/hostess"
)
func TestAddDuplicate(t *testing.T) {
list := hostess.NewHostlist()
hostname := hostess.MustHostname("mysite", "1.2.3.4", false)
if err := list.Add(hostname); err != nil {
t.Error(err)
}
err := list.Add(hostname)
assert.Nil(t, err, "Expected no errors when adding a hostname for the first time")
hostname.Enabled = true
if err := list.Add(hostname); err == nil {
t.Error("Expected error because of duplicate entry")
}
if !(*list)[0].Enabled {
t.Error("Expected hostname to be in enabled state")
}
err = list.Add(hostname)
assert.NotNil(t, err, "Expected error when adding a duplicate")
assert.True(t, (*list)[0].Enabled, "Expected hostname to be in enabled state")
}
func TestAddConflict(t *testing.T) {
@ -35,9 +30,8 @@ func TestAddConflict(t *testing.T) {
list := hostess.NewHostlist()
list.Add(hostnameA)
if err := list.Add(hostnameB); err == nil {
t.Errorf("Expected conflict error")
}
err := list.Add(hostnameB)
assert.NotNil(t, err, "Expected conflict error")
if !(*list)[0].Equal(hostnameB) {
t.Error("Expected second hostname to overwrite")
@ -97,50 +91,6 @@ func TestContainsDomainIp(t *testing.T) {
}
}
func TestFormatLinux(t *testing.T) {
hostfile := hostess.NewHostfile()
hostfile.Path = filepath.Join("testdata", "hostfile3")
if err := hostfile.Read(); err != nil {
t.Fatal(err)
}
if errs := hostfile.Parse(); len(errs) != 0 {
t.Fatal(errs)
}
expected, err := ioutil.ReadFile(filepath.Join("testdata", "expected-linux"))
if err != nil {
t.Fatal(err)
}
output := hostfile.Hosts.FormatLinux()
if !bytes.Equal(output, expected) {
t.Error(Diff(string(expected), string(output)))
}
}
func TestFormatWindows(t *testing.T) {
hostfile := hostess.NewHostfile()
hostfile.Path = filepath.Join("testdata", "hostfile3")
if err := hostfile.Read(); err != nil {
t.Fatal(err)
}
if errs := hostfile.Parse(); len(errs) != 0 {
t.Fatal(errs)
}
expected, err := ioutil.ReadFile(filepath.Join("testdata", "expected-windows"))
if err != nil {
t.Fatal(err)
}
output := hostfile.Hosts.FormatWindows()
if !bytes.Equal(output, expected) {
t.Error(Diff(string(expected), string(output)))
}
}
func TestFormat(t *testing.T) {
hosts := hostess.NewHostlist()
hosts.Add(hostess.MustHostname(domain, ip, false))

@ -4,11 +4,11 @@ import (
"net"
"testing"
hostess2 "github.com/cbednarski/hostess/hostess"
"github.com/cbednarski/hostess"
)
func TestHostname(t *testing.T) {
h := hostess2.MustHostname(domain, ip, enabled)
h := hostess.MustHostname(domain, ip, enabled)
if h.Domain != domain {
t.Errorf("Domain should be %s", domain)
@ -22,9 +22,9 @@ func TestHostname(t *testing.T) {
}
func TestEqual(t *testing.T) {
a := hostess2.MustHostname("localhost", "127.0.0.1", true)
b := hostess2.MustHostname("localhost", "127.0.0.1", false)
c := hostess2.MustHostname("localhost", "127.0.1.1", false)
a := hostess.MustHostname("localhost", "127.0.0.1", true)
b := hostess.MustHostname("localhost", "127.0.0.1", false)
c := hostess.MustHostname("localhost", "127.0.1.1", false)
if !a.Equal(b) {
t.Errorf("%+v and %+v should be equal", a, b)
@ -35,8 +35,8 @@ func TestEqual(t *testing.T) {
}
func TestEqualIP(t *testing.T) {
a := hostess2.MustHostname("localhost", "127.0.0.1", true)
c := hostess2.MustHostname("localhost", "127.0.1.1", false)
a := hostess.MustHostname("localhost", "127.0.0.1", true)
c := hostess.MustHostname("localhost", "127.0.1.1", false)
ip := net.ParseIP("127.0.0.1")
if !a.EqualIP(ip) {
@ -48,7 +48,7 @@ func TestEqualIP(t *testing.T) {
}
func TestIsValid(t *testing.T) {
hostname := &hostess2.Hostname{
hostname := &hostess.Hostname{
Domain: "localhost",
IP: net.ParseIP("127.0.0.1"),
Enabled: true,
@ -60,7 +60,7 @@ func TestIsValid(t *testing.T) {
}
func TestIsValidBlank(t *testing.T) {
hostname := &hostess2.Hostname{
hostname := &hostess.Hostname{
Domain: "",
IP: net.ParseIP("127.0.0.1"),
Enabled: true,
@ -71,7 +71,7 @@ func TestIsValidBlank(t *testing.T) {
}
}
func TestIsValidBadIP(t *testing.T) {
hostname := &hostess2.Hostname{
hostname := &hostess.Hostname{
Domain: "localhost",
IP: net.ParseIP("localhost"),
Enabled: true,
@ -83,7 +83,7 @@ func TestIsValidBadIP(t *testing.T) {
}
func TestFormatHostname(t *testing.T) {
hostname := hostess2.MustHostname(domain, ip, enabled)
hostname := hostess.MustHostname(domain, ip, enabled)
const exp_enabled = "127.0.0.1 localhost"
if hostname.Format() != exp_enabled {
@ -98,7 +98,7 @@ func TestFormatHostname(t *testing.T) {
}
func TestFormatEnabled(t *testing.T) {
hostname := hostess2.MustHostname(domain, ip, enabled)
hostname := hostess.MustHostname(domain, ip, enabled)
const expectedOn = "(On)"
if hostname.FormatEnabled() != expectedOn {
t.Errorf("Expected hostname to be turned %s", expectedOn)

@ -1,153 +0,0 @@
// 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))
}

@ -1,302 +0,0 @@
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)
}
}

6
testdata/issue39 vendored

@ -1,6 +0,0 @@
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

@ -1,10 +0,0 @@
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

@ -0,0 +1,21 @@
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.

@ -0,0 +1,306 @@
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)
}

@ -0,0 +1,40 @@
// 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")
}

@ -0,0 +1,184 @@
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)
}

@ -0,0 +1,376 @@
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
}

@ -0,0 +1,497 @@
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
}

@ -0,0 +1,235 @@
package cli
import (
"fmt"
"io"
"strings"
"text/tabwriter"
"text/template"
)
// The text template for the Default help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var AppHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.Name}} {{if .Flags}}[global options] {{end}}command{{if .Flags}} [command options]{{end}} [arguments...]
VERSION:
{{.Version}}{{if len .Authors}}
AUTHOR(S):
{{range .Authors}}{{ . }}{{end}}{{end}}
COMMANDS:
{{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
{{end}}{{if .Flags}}
GLOBAL OPTIONS:
{{range .Flags}}{{.}}
{{end}}{{end}}
`
// The text template for the command help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var CommandHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
command {{.Name}}{{if .Flags}} [command options]{{end}} [arguments...]{{if .Description}}
DESCRIPTION:
{{.Description}}{{end}}{{if .Flags}}
OPTIONS:
{{range .Flags}}{{.}}
{{end}}{{ end }}
`
// The text template for the subcommand help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var SubcommandHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.Name}} command{{if .Flags}} [command options]{{end}} [arguments...]
COMMANDS:
{{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
{{end}}{{if .Flags}}
OPTIONS:
{{range .Flags}}{{.}}
{{end}}{{end}}
`
var helpCommand = Command{
Name: "help",
Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command",
Action: func(c *Context) {
args := c.Args()
if args.Present() {
ShowCommandHelp(c, args.First())
} else {
ShowAppHelp(c)
}
},
}
var helpSubcommand = Command{
Name: "help",
Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command",
Action: func(c *Context) {
args := c.Args()
if args.Present() {
ShowCommandHelp(c, args.First())
} else {
ShowSubcommandHelp(c)
}
},
}
// Prints help for the App or Command
type helpPrinter func(w io.Writer, templ string, data interface{})
var HelpPrinter helpPrinter = printHelp
// Prints version for the App
var VersionPrinter = printVersion
func ShowAppHelp(c *Context) {
HelpPrinter(c.App.Writer, AppHelpTemplate, c.App)
}
// Prints the list of subcommands as the default app completion method
func DefaultAppComplete(c *Context) {
for _, command := range c.App.Commands {
for _, name := range command.Names() {
fmt.Fprintln(c.App.Writer, name)
}
}
}
// Prints help for the given command
func ShowCommandHelp(ctx *Context, command string) {
// show the subcommand help for a command with subcommands
if command == "" {
HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App)
return
}
for _, c := range ctx.App.Commands {
if c.HasName(command) {
HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c)
return
}
}
if ctx.App.CommandNotFound != nil {
ctx.App.CommandNotFound(ctx, command)
} else {
fmt.Fprintf(ctx.App.Writer, "No help topic for '%v'\n", command)
}
}
// Prints help for the given subcommand
func ShowSubcommandHelp(c *Context) {
ShowCommandHelp(c, c.Command.Name)
}
// Prints the version number of the App
func ShowVersion(c *Context) {
VersionPrinter(c)
}
func printVersion(c *Context) {
fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version)
}
// Prints the lists of commands within a given context
func ShowCompletions(c *Context) {
a := c.App
if a != nil && a.BashComplete != nil {
a.BashComplete(c)
}
}
// Prints the custom completions for a given command
func ShowCommandCompletions(ctx *Context, command string) {
c := ctx.App.Command(command)
if c != nil && c.BashComplete != nil {
c.BashComplete(ctx)
}
}
func printHelp(out io.Writer, templ string, data interface{}) {
funcMap := template.FuncMap{
"join": strings.Join,
}
w := tabwriter.NewWriter(out, 0, 8, 1, '\t', 0)
t := template.Must(template.New("help").Funcs(funcMap).Parse(templ))
err := t.Execute(w, data)
if err != nil {
panic(err)
}
w.Flush()
}
func checkVersion(c *Context) bool {
if c.GlobalBool("version") || c.GlobalBool("v") || c.Bool("version") || c.Bool("v") {
ShowVersion(c)
return true
}
return false
}
func checkHelp(c *Context) bool {
if c.GlobalBool("h") || c.GlobalBool("help") || c.Bool("h") || c.Bool("help") {
ShowAppHelp(c)
return true
}
return false
}
func checkCommandHelp(c *Context, name string) bool {
if c.Bool("h") || c.Bool("help") {
ShowCommandHelp(c, name)
return true
}
return false
}
func checkSubcommandHelp(c *Context) bool {
if c.GlobalBool("h") || c.GlobalBool("help") {
ShowSubcommandHelp(c)
return true
}
return false
}
func checkCompletions(c *Context) bool {
if (c.GlobalBool(BashCompletionFlag.Name) || c.Bool(BashCompletionFlag.Name)) && c.App.EnableBashCompletion {
ShowCompletions(c)
return true
}
return false
}
func checkCommandCompletions(c *Context, name string) bool {
if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion {
ShowCommandCompletions(c, name)
return true
}
return false
}
Loading…
Cancel
Save