Compare commits

...

42 Commits

Author SHA1 Message Date
Chris Bednarski 187298f216 Change format switch from 'linux' to 'unix' to match the readme 4 years ago
Chris Bednarski 4f4dd46d6f Merge branch 'master' of https://github.com/cbednarski/hostess 4 years ago
Chris Bednarski dfe047857d Fix panic when hostess is called with no arguments 4 years ago
Chris Bednarski f0fc63817f Added changes to the changelog 4 years ago
Chris Bednarski 71f910ca52 Added changes to the changelog 4 years ago
Chris Bednarski b5e6f45648 Added windows-specific expected test output for ExitCodeFmt test 4 years ago
Chris Bednarski fdfc2f9001 Update references to parsed CLI args to use zero-index because command is no longer in that slice 4 years ago
Chris Bednarski 168ef0ce47 Update to fmt -n (preview) behavior
- Fixing -n not being parsed by CLI
- fmt -n will error on any duplicates or conflicts
- fmt (without -n) will NOT error on duplicates; it will simply replace them
- fmt (without -n) WILL error on conflicts, since it does not know how to fix them
- fmt will exit 1 when an error state is reached
4 years ago
Chris Bednarski e56fc7aec0 Updated changelog 4 years ago
Chris Bednarski e899791fce Don't fail when a duplicate is encountered (fixes #39) 4 years ago
Chris Bednarski 0e15ef497a Added test for issue #39 4 years ago
Chris Bednarski ebbdf23b34 Ignore .DS_Store files 4 years ago
Chris Bednarski 4fc0ac9077
Merge pull request #38 from chenrui333/update-travis
Minor update on travis config
4 years ago
Chris Bednarski d60ff45bf8 Updated changelog for windows hosts format 4 years ago
Chris Bednarski a343a8e07f Fix missing windows test case, fmt 4 years ago
Chris Bednarski aecfa8f689 Added windows-specific hostfile format, and tests 4 years ago
Chris Bednarski c4e4f6d820 Removed -4 and -6 from CLI (these are still usable in the lib) 4 years ago
Rui Chen 7fbd6215e8
Minor update on travis config 4 years ago
Chris Bednarski 138ef9dcd4 Added changelog for 0.4.1 4 years ago
Chris Bednarski 1e2456bc6a Added a note about sudo and Windows Admin prompt 4 years ago
Chris Bednarski df2d795268 Fix hostfile.Save() on Windows 4 years ago
Chris Bednarski be9a630ae8 0.4.0 release notes 4 years ago
Chris Bednarski d3e97d1758 Fix argument parsing for CLI
Add tests for main and commands
Fix documentation for installation so the program works with sudo
4 years ago
Chris Bednarski 127256655c Not doing 0.5.0 yet. 4 years ago
Chris Bednarski ca1856f5fe Added changelog 4 years ago
Chris Bednarski 9c247955f6 go fmt 4 years ago
Chris Bednarski ba102742d4 Added contributor section 4 years ago
Chris Bednarski 960732bbb4 Be nice to Travis and just use a pre-baked version 4 years ago
Chris Bednarski dd746035f7 Add appveyor config, changelog, README TLC, and update TravisCI Go to 1.14 4 years ago
Chris Bednarski 18546969ce Added package comment 4 years ago
Chris Bednarski c53f06fdc8 Moved commands from commands package to root 4 years ago
Chris Bednarski 000eb28419 Clean up makefile 4 years ago
Chris Bednarski 2b25df69d5 Remove trailing space 4 years ago
Chris Bednarski b9940cf499 Cleaned up gitignore because we're not using all that stuff anymore 4 years ago
Chris Bednarski 26a3d25daf Add the library back. oops. 4 years ago
Chris Bednarski ddad9598d6 Added release target to Makefile 4 years ago
Chris Bednarski aeb56f8a79 Ignore bin folder 4 years ago
Chris Bednarski 7ca1771c71 More work on refactoring
- Reimplement the rest of the frontend code
- Remove -f option since this is just covered by fmt now
- Still some bugs left to work out
4 years ago
Chris Bednarski 66670942dd Started major refactor for 0.5.0
- Rewrite CLI to use flag package instead of codegangsta/cli
- Move lib to hostess/ and move main.go to project root
- Change some of the hostess API to return errors instead of panicking
- Remove aff, del, and list commands
- Remove -s and -q flags
4 years ago
Chris Bednarski 4c91c57875 Update to canonical golint import path, closes #35 Thanks Mateusz Kubuszok! 5 years ago
Chris Bednarski 017b8f33a4
Merge pull request #32 from alswl/master
fix: spelling
6 years ago
alswl 6426b1e7c6
fix: spelling 6 years ago

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

10
.gitignore vendored

@ -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,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015 Chris Bednarski
Copyright (c) 2015-2020 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,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,6 +1,6 @@
# 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)
# 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)
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,67 +10,63 @@ Because sometimes DNS doesn't work in production. And because editing
`/etc/hosts` by hand is a pain. Put hostess in your `Makefile` or deploy scripts
and call it a day.
## Using Hostess
\* And `C:\Windows\System32\drivers\etc\hosts` on Windows.
### Download and Install
**Note: 0.4.0 has backwards incompatible changes in the API and CLI.** See
`CHANGELOG.md` for details.
## Installation
Download a [precompiled release](https://github.com/cbednarski/hostess/releases)
from GitHub.
from GitHub, or build from source (with a [recent version of Go](https://golang.org/dl)):
Builds are supported for OSX, Linux, Windows, and RaspberryPi.
git clone https://github.com/cbednarski/hostess
cd hostess
make install
### Usage
## Usage
hostess add domain ip # Add or replace a hosts entry for this domain pointing to this IP
hostess aff domain ip # Add or replace a hosts entry in an off state
hostess del domain # (alias rm) Remove a domain from your hosts file
hostess has domain # exit code 0 if the domain is in your hostfile, 1 otherwise
hostess off domain # Disable a domain (but don't remove it completely), exit 1 if entry is missing
hostess on domain # Re-enable a domain that was disabled, exit 1 if entry is missing
hostess list # (alias ls) List domains, target ips, and on/off status
hostess fix # Rewrite your hosts file; use -n to dry run
hostess dump # Dump your hostfile as json
hostess apply # Add entries from a json file
Run `hostess` or `hostess -h` to see a full list of commands.
Flags
**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_).
-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
## Format
hostess will mangle your hosts file. Domains pointing at the same IP will be
grouped together and disabled domains commented out.
On unixes, hostess follows the format specified by `man hosts`, with one line
per IP address:
127.0.0.1 localhost hostname2 hostname3
127.0.1.1 machine.name
# 10.10.20.30 some.host
### IPv4 and IPv6
On Windows, hostess writes each hostname on its own line.
Your hosts file *can* contain overlapping entries where the same hostname points
to both an IPv4 and IPv6 IP. In this case, hostess commands will apply to both
entries. Typically you won't have this kind of overlap and the default behavior
is OK. However, if you need to be more granular you can use `-4` or `-6` to
limit operations to entries associated with that type of IP.
127.0.0.1 localhost
127.0.0.1 hostname2
127.0.0.1 hostname3
## Developing Hostess
## Configuration
### Configuration
hostess may be configured via environment variables.
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_FMT` may be set to `windows` or `unix` to override platform detection
for the hosts file format. See Behavior, above, for details
### Building from Source
- `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.
To build from source you'll need to have go 1.4+
## IPv4 and IPv6
#### Install with go get
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.
go get github.com/cbednarski/hostess/cmd/hostess
## Contributing
#### Install from source
git clone https://github.com/cbednarski/hostess
cd hostess
make
make install
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. 👻

@ -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,3 @@
module github.com/cbednarski/hostess
go 1.14

@ -1,3 +1,5 @@
// Package hostess provides an API to interact with /etc/hosts and its Windows
// equivalent.
package hostess
import (
@ -8,6 +10,8 @@ import (
"strings"
)
const EnvHostessPath = `HOSTESS_PATH`
const defaultOSX = `
##
# Host Database
@ -51,7 +55,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("HOSTESS_PATH")
path := os.Getenv(EnvHostessPath)
if path == "" {
if runtime.GOOS == "windows" {
path = "C:\\Windows\\System32\\drivers\\etc\\hosts"
@ -192,7 +196,26 @@ 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 {
file, err := os.OpenFile(h.Path, os.O_RDWR|os.O_APPEND|os.O_TRUNC, 0644)
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)
}
if err != nil {
return err
}

@ -2,11 +2,15 @@ package hostess_test
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/cbednarski/hostess"
"github.com/cbednarski/hostess/hostess"
)
const ipv4Pass = `
@ -57,12 +61,22 @@ 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
const expected = `127.0.0.1 localhost devsite
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))
@ -200,3 +214,30 @@ 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,17 +1,23 @@
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 ErrInvalidVersionArg = errors.New("version argument must be 4 or 6")
var ErrHostnameNotFound = errors.New("hostname not found")
// Hostlist is a sortable set of Hostnames. When in a Hostlist, Hostnames must
// follow some rules:
@ -132,10 +138,10 @@ func (h Hostlist) Swap(i, j int) {
// Sort this list of Hostnames, according to Hostlist sorting rules:
//
// 1. localhost comes before other domains
// 1. localhost comes before other hostnames
// 2. IPv4 comes before IPv6
// 3. IPs are sorted in numerical order
// 4. domains are sorted in alphabetical
// 4. The remaining hostnames are sorted in lexicographical order
func (h *Hostlist) Sort() {
sort.Sort(*h)
}
@ -178,28 +184,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(hostnamev *Hostname) error {
hostname, err := NewHostname(hostnamev.Domain, hostnamev.IP.String(), hostnamev.Enabled)
func (h *Hostlist) Add(input *Hostname) error {
newHostname, err := NewHostname(input.Domain, input.IP.String(), input.Enabled)
if err != nil {
return err
}
for index, found := range *h {
if found.Equal(hostname) {
if found.Equal(newHostname) {
// If either hostname is enabled we will set the existing one to
// enabled state. That way if we add a hostname from the end of a
// hosts file it will take over, and if we later add a disabled one
// the original one will stick. We still error in this case so the
// user can see that there is a duplicate.
(*h)[index].Enabled = found.Enabled || hostname.Enabled
return fmt.Errorf("Duplicate hostname entry for %s -> %s",
hostname.Domain, hostname.IP)
} else if found.Domain == hostname.Domain && found.IPv6 == hostname.IPv6 {
(*h)[index] = hostname
return fmt.Errorf("Conflicting hostname entries for %s -> %s and -> %s",
hostname.Domain, hostname.IP, found.IP)
(*h)[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 = append(*h, hostname)
*h = append(*h, newHostname)
return nil
}
@ -252,62 +258,58 @@ func (h *Hostlist) RemoveDomainV(domain string, version int) int {
return h.Remove(h.IndexOfDomainV(domain, version))
}
// Enable will change any Hostnames matching domain to be enabled.
func (h *Hostlist) Enable(domain string) bool {
found := false
// Enable will change any Hostnames matching name to be enabled.
func (h *Hostlist) Enable(name string) error {
for _, hostname := range *h {
if hostname.Domain == domain {
if hostname.Domain == name {
hostname.Enabled = true
found = true
return nil
}
}
return found
return ErrHostnameNotFound
}
// EnableV will change a Hostname matching domain and IP version to be enabled.
//
// This function will panic if IP version is not 4 or 6.
func (h *Hostlist) EnableV(domain string, version int) bool {
found := false
func (h *Hostlist) EnableV(domain string, version int) error {
if version != 4 && version != 6 {
panic(ErrInvalidVersionArg)
return ErrInvalidVersionArg
}
for _, hostname := range *h {
if hostname.Domain == domain && hostname.IPv6 == (version == 6) {
hostname.Enabled = true
found = true
return nil
}
}
return found
return ErrHostnameNotFound
}
// Disable will change any Hostnames matching domain to be disabled.
func (h *Hostlist) Disable(domain string) bool {
found := false
// Disable will change any Hostnames matching name to be disabled.
func (h *Hostlist) Disable(name string) error {
for _, hostname := range *h {
if hostname.Domain == domain {
if hostname.Domain == name {
hostname.Enabled = false
found = true
return nil
}
}
return found
return ErrHostnameNotFound
}
// DisableV will change any Hostnames matching domain and IP version to be disabled.
//
// This function will panic if IP version is not 4 or 6.
func (h *Hostlist) DisableV(domain string, version int) bool {
found := false
func (h *Hostlist) DisableV(domain string, version int) error {
if version != 4 && version != 6 {
panic(ErrInvalidVersionArg)
return ErrInvalidVersionArg
}
for _, hostname := range *h {
if hostname.Domain == domain && hostname.IPv6 == (version == 6) {
hostname.Enabled = false
found = true
return nil
}
}
return found
return ErrHostnameNotFound
}
// FilterByIP filters the list of hostnames by IP address.
@ -375,9 +377,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) Format() []byte {
func (h *Hostlist) FormatLinux() []byte {
h.Sort()
out := []byte{}
out := bytes.Buffer{}
// We want to output one line of hostnames per IP, so first we get that
// list of IPs and iterate.
@ -401,19 +403,49 @@ func (h *Hostlist) Format() []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 {
concat := fmt.Sprintf("%s %s", IP.String(), strings.Join(enabledIPs, " "))
out = append(out, []byte(concat)...)
out = append(out, []byte("\n")...)
out.WriteString(fmt.Sprintf("%s %s\n", IP.String(), strings.Join(enabledIPs, " ")))
}
if len(disabledIPs) > 0 {
concat := fmt.Sprintf("# %s %s", IP.String(), strings.Join(disabledIPs, " "))
out = append(out, []byte(concat)...)
out = append(out, []byte("\n")...)
out.WriteString(fmt.Sprintf("# %s %s\n", IP.String(), strings.Join(disabledIPs, " ")))
}
}
return out
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()
}
}
// Dump exports all entries in the Hostlist as JSON

@ -3,25 +3,30 @@ package hostess_test
import (
"bytes"
"fmt"
"io/ioutil"
"net"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/cbednarski/hostess"
"github.com/cbednarski/hostess/hostess"
)
func TestAddDuplicate(t *testing.T) {
list := hostess.NewHostlist()
hostname := hostess.MustHostname("mysite", "1.2.3.4", false)
err := list.Add(hostname)
assert.Nil(t, err, "Expected no errors when adding a hostname for the first time")
if err := list.Add(hostname); err != nil {
t.Error(err)
}
hostname.Enabled = true
err = list.Add(hostname)
assert.NotNil(t, err, "Expected error when adding a duplicate")
assert.True(t, (*list)[0].Enabled, "Expected hostname to be in enabled state")
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")
}
}
func TestAddConflict(t *testing.T) {
@ -30,8 +35,9 @@ func TestAddConflict(t *testing.T) {
list := hostess.NewHostlist()
list.Add(hostnameA)
err := list.Add(hostnameB)
assert.NotNil(t, err, "Expected conflict error")
if err := list.Add(hostnameB); err == nil {
t.Errorf("Expected conflict error")
}
if !(*list)[0].Equal(hostnameB) {
t.Error("Expected second hostname to overwrite")
@ -91,6 +97,50 @@ 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"
"github.com/cbednarski/hostess"
hostess2 "github.com/cbednarski/hostess/hostess"
)
func TestHostname(t *testing.T) {
h := hostess.MustHostname(domain, ip, enabled)
h := hostess2.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 := 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)
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)
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 := hostess.MustHostname("localhost", "127.0.0.1", true)
c := hostess.MustHostname("localhost", "127.0.1.1", false)
a := hostess2.MustHostname("localhost", "127.0.0.1", true)
c := hostess2.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 := &hostess.Hostname{
hostname := &hostess2.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 := &hostess.Hostname{
hostname := &hostess2.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 := &hostess.Hostname{
hostname := &hostess2.Hostname{
Domain: "localhost",
IP: net.ParseIP("localhost"),
Enabled: true,
@ -83,7 +83,7 @@ func TestIsValidBadIP(t *testing.T) {
}
func TestFormatHostname(t *testing.T) {
hostname := hostess.MustHostname(domain, ip, enabled)
hostname := hostess2.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 := hostess.MustHostname(domain, ip, enabled)
hostname := hostess2.MustHostname(domain, ip, enabled)
const expectedOn = "(On)"
if hostname.FormatEnabled() != expectedOn {
t.Errorf("Expected hostname to be turned %s", expectedOn)

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

6
testdata/issue39 vendored

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