Compare commits

...

19 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

1
.gitignore vendored

@ -1,2 +1,3 @@
/.idea/
/bin/
.DS_Store

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

@ -1,5 +1,32 @@
# 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

@ -60,11 +60,9 @@ hostess may be configured via environment variables.
## IPv4 and IPv6
Your hosts file _may_ 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.
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.
## Contributing

@ -11,14 +11,10 @@ import (
"github.com/cbednarski/hostess/hostess"
)
const (
IPv4 = 1 << iota
IPv6 = 1 << iota
)
var ErrParsingHostsFile = errors.New("Errors while parsing hostsfile. Please resolve any conflicts and try again.")
type Options struct {
IPVersion int
Preview bool
Preview bool
}
// PrintErrLn will print to stderr followed by a newline
@ -27,18 +23,30 @@ func PrintErrLn(err error) {
}
// LoadHostfile will try to load, parse, and return a Hostfile. If we
// encounter errors we will terminate, unless -f is passed.
func LoadHostfile() (*hostess.Hostfile, error) {
// encounter errors we will terminate.
func LoadHostfile(options *Options) (*hostess.Hostfile, error) {
hosts, errs := hostess.LoadHostfile()
var err error
if len(errs) > 0 {
for _, err := range errs {
PrintErrLn(err)
// 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 nil, errors.New("Errors while parsing hostsfile. Please fix any dupes or conflicts and try again.")
}
return hosts, nil
return hosts, err
}
// SaveOrPreview will display or write the Hostfile
@ -70,7 +78,7 @@ func StrPadRight(input string, length int) string {
// Add command parses <hostname> <ip> and adds or updates a hostname in the
// hosts file
func Add(options *Options, hostname, ip string) error {
hostsfile, err := LoadHostfile()
hostsfile, err := LoadHostfile(options)
newHostname, err := hostess.NewHostname(hostname, ip, true)
if err != nil {
@ -103,7 +111,7 @@ func Add(options *Options, hostname, ip string) error {
// Remove command removes any hostname(s) matching <domain> from the hosts file
func Remove(options *Options, hostname string) error {
hostsfile, err := LoadHostfile()
hostsfile, err := LoadHostfile(options)
if err != nil {
return err
}
@ -124,7 +132,7 @@ func Remove(options *Options, hostname string) error {
// Has command indicates whether a hostname is present in the hosts file
func Has(options *Options, hostname string) error {
hostsfile, err := LoadHostfile()
hostsfile, err := LoadHostfile(options)
if err != nil {
return err
}
@ -141,7 +149,7 @@ func Has(options *Options, hostname string) error {
}
func Enable(options *Options, hostname string) error {
hostsfile, err := LoadHostfile()
hostsfile, err := LoadHostfile(options)
if err != nil {
return err
}
@ -160,7 +168,7 @@ func Enable(options *Options, hostname string) error {
}
func Disable(options *Options, hostname string) error {
hostsfile, err := LoadHostfile()
hostsfile, err := LoadHostfile(options)
if err != nil {
return err
}
@ -187,7 +195,7 @@ func Disable(options *Options, hostname string) error {
// List command shows a list of hostnames in the hosts file
func List(options *Options) error {
hostsfile, err := LoadHostfile()
hostsfile, err := LoadHostfile(options)
if err != nil {
return err
}
@ -216,9 +224,9 @@ func List(options *Options) error {
return nil
}
// Format command removes duplicates and conflicts from the hosts file
// Format command removes duplicates from the hosts file
func Format(options *Options) error {
hostsfile, err := LoadHostfile()
hostsfile, err := LoadHostfile(options)
if err != nil {
return err
}
@ -233,7 +241,7 @@ func Format(options *Options) error {
// Dump command outputs hosts file contents as JSON
func Dump(options *Options) error {
hostsfile, err := LoadHostfile()
hostsfile, err := LoadHostfile(options)
if err != nil {
return err
}
@ -254,7 +262,7 @@ func Apply(options *Options, filename string) error {
return fmt.Errorf("Unable to read JSON from %s: %s", filename, err)
}
hostfile, err := LoadHostfile()
hostfile, err := LoadHostfile(options)
if err != nil {
return err
}

@ -1,6 +1,8 @@
package main
import (
"os"
"path/filepath"
"testing"
)
@ -24,3 +26,14 @@ func TestStrPadRight(t *testing.T) {
}
}
}
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)
}
}

@ -61,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))

@ -1,10 +1,13 @@
package hostess
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"runtime"
"sort"
"strings"
)
@ -374,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.
@ -400,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,7 +3,9 @@ package hostess_test
import (
"bytes"
"fmt"
"io/ioutil"
"net"
"path/filepath"
"testing"
"github.com/cbednarski/hostess/hostess"
@ -95,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))

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

@ -33,8 +33,6 @@ Commands
Flags
-n will preview changes but not rewrite your hosts file
-4 limit changes to IPv4 entries
-6 limit changes to IPv6 entries
Configuration
@ -61,35 +59,35 @@ func ExitWithError(err error) {
}
}
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)
ipv4 := cli.Bool("4", false, "IPv4")
ipv6 := cli.Bool("6", false, "IPv6")
preview := cli.Bool("n", false, "preview")
cli.Usage = func() {
fmt.Printf(help, hostess.GetHostsPath())
cli.Usage = Usage
command := ""
if len(args) > 1 {
command = args[1]
} else {
Usage()
}
if err := cli.Parse(args[1:]); err != nil {
if err := cli.Parse(args[2:]); err != nil {
return err
}
options := &Options{
IPVersion: 0,
Preview: *preview,
}
if *ipv4 {
options.IPVersion = options.IPVersion | IPv4
}
if *ipv6 {
options.IPVersion = options.IPVersion | IPv6
Preview: *preview,
}
command := cli.Arg(0)
switch command {
case "-v", "--version", "version":
@ -104,46 +102,46 @@ func wrappedMain(args []string) error {
return Format(options)
case "add":
if len(cli.Args()) != 3 {
if len(cli.Args()) != 2 {
return fmt.Errorf("Usage: %s add <hostname> <ip>", cli.Name())
}
return Add(options, cli.Arg(1), cli.Arg(2))
return Add(options, cli.Arg(0), cli.Arg(1))
case "rm":
if cli.Arg(1) == "" {
if cli.Arg(0) == "" {
return CommandUsage(command)
}
return Remove(options, cli.Arg(1))
return Remove(options, cli.Arg(0))
case "on":
if cli.Arg(1) == "" {
if cli.Arg(0) == "" {
return CommandUsage(command)
}
return Enable(options, cli.Arg(1))
return Enable(options, cli.Arg(0))
case "off":
if cli.Arg(1) == "" {
if cli.Arg(0) == "" {
return CommandUsage(command)
}
return Disable(options, cli.Arg(1))
return Disable(options, cli.Arg(0))
case "ls":
return List(options)
case "has":
if cli.Arg(1) == "" {
if cli.Arg(0) == "" {
return CommandUsage(command)
}
return Has(options, cli.Arg(1))
return Has(options, cli.Arg(0))
case "dump":
return Dump(options)
case "apply":
if cli.Arg(1) == "" {
if cli.Arg(0) == "" {
return fmt.Errorf("Usage: %s apply <filename>", args[0])
}
return Apply(options, cli.Arg(1))
return Apply(options, cli.Arg(0))
default:
return ErrInvalidCommand

@ -1,9 +1,12 @@
package main
import (
"bytes"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
@ -13,10 +16,20 @@ import (
// 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) (string, func()) {
func CopyHostsFile(t *testing.T, fixtureFiles ...string) (string, func()) {
t.Helper()
fixture, err := os.Open("testdata/ubuntu.hosts")
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)
}
@ -36,6 +49,7 @@ func CopyHostsFile(t *testing.T) (string, func()) {
cleanup := func() {
os.Remove(temp.Name())
os.Unsetenv(hostess.EnvHostessPath)
}
return temp.Name(), cleanup
@ -65,6 +79,20 @@ 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)
}
@ -102,6 +130,22 @@ 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)
}
@ -133,6 +177,18 @@ 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)
}
@ -166,7 +222,81 @@ 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
Loading…
Cancel
Save