diff --git a/cmd/add.go b/cmd/add.go index f6d40ae..d983087 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -68,7 +68,7 @@ func add(c *cli.Context) error { } logrus.Debugln("flushing hosts file to disk") - if err := hf.Flush(); err != nil { + if err := flushHostsfile(c, hf); err != nil { return cli.Exit(err.Error(), 2) } diff --git a/cmd/app.go b/cmd/app.go index e7e330b..598ded5 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -60,6 +60,15 @@ var ( Aliases: []string{"q"}, Usage: "Turn on off all logging", }, + &cli.BoolFlag{ + Name: "backup", + Aliases: []string{"b"}, + Usage: "Create backup before modifying hosts file", + }, + &cli.BoolFlag{ + Name: "safe", + Usage: "Check for concurrent modifications before writing", + }, }, } ) @@ -110,6 +119,28 @@ func loadHostsfile(c *cli.Context, readOnly bool) (*hostsfile.Hosts, error) { return hf, nil } +func flushHostsfile(c *cli.Context, hf *hostsfile.Hosts) error { + if c.Bool("safe") { + modified, err := hf.HasBeenModified() + if err != nil { + return err + } + if modified { + return fmt.Errorf("hosts file was modified by another process; reload and retry") + } + } + + if c.Bool("backup") { + if err := hf.Backup(); err != nil { + logrus.Warnf("Failed to create backup: %v", err) + } else { + logrus.Debugf("Backup created at: %s", hf.BackupPath()) + } + } + + return hf.Flush() +} + func outputHostsfile(hf *hostsfile.Hosts, all bool) { for _, line := range hf.Lines { if !all { diff --git a/cmd/check.go b/cmd/check.go index ab22a49..e14e720 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -12,34 +12,96 @@ func Check() *cli.Command { return &cli.Command{ Name: "check", Aliases: []string{"c"}, - Usage: "Check if ip or host exists", + Usage: "Check if ip or host exists. With IP and hosts: check if all hosts are mapped to IP", Action: check, - ArgsUsage: "[IP|HOST]", + ArgsUsage: "[IP|HOST] or [IP] [HOST] ([HOST]...)", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Suppress output, only return exit code", + }, + &cli.BoolFlag{ + Name: "any", + Usage: "With multiple hosts: succeed if ANY host matches (default: ALL must match)", + }, + }, } } + func check(c *cli.Context) error { if c.Args().Len() < 1 { logrus.Infof("No input, pass an ip address or hostname to check.") return nil } - hf, err := loadHostsfile(c, true) + hf, err := loadHostsfile(c, true) // readOnly=true, no sudo needed if err != nil { return err } - input := c.Args().First() - if net.ParseIP(input) != nil { - if hf.HasIP(input) { - logrus.Infof("%s exists in hosts file\n", input) + args := c.Args().Slice() + firstArg := args[0] + quiet := c.Bool("quiet") + + // Single argument: check if IP or hostname exists (backward compatible) + if c.Args().Len() == 1 { + if net.ParseIP(firstArg) != nil { + if hf.HasIP(firstArg) { + if !quiet { + logrus.Infof("%s exists in hosts file\n", firstArg) + } + return nil + } + } + + if hf.HasHostname(firstArg) { + if !quiet { + logrus.Infof("%s exists in hosts file\n", firstArg) + } + return nil + } + + return cli.Exit(fmt.Sprintf("%s does not match anything in the hosts file", firstArg), 1) + } + + // Multiple arguments: first must be IP, rest are hostnames + if net.ParseIP(firstArg) == nil { + return cli.Exit(fmt.Sprintf("%s is not a valid IP address. When checking multiple hosts, first argument must be an IP.", firstArg), 2) + } + + ip := firstArg + hosts := args[1:] + + if c.Bool("any") { + // Check if ANY host is mapped to IP + if hf.HasAny(ip, hosts...) { + if !quiet { + logrus.Infof("At least one host is mapped to %s\n", ip) + } return nil } + return cli.Exit(fmt.Sprintf("None of the specified hosts are mapped to %s", ip), 1) } - if hf.HasHostname(input) { - logrus.Infof("%s exists in hosts file\n", input) + // Default: Check if ALL hosts are mapped to IP + if hf.HasAll(ip, hosts...) { + if !quiet { + logrus.Infof("All specified hosts are mapped to %s\n", ip) + } return nil } - return cli.Exit(fmt.Sprintf("%s does not match anything in the hosts file", input), 1) + // Report which hosts are missing + if !quiet { + missing := []string{} + for _, host := range hosts { + if !hf.Has(ip, host) { + missing = append(missing, host) + } + } + return cli.Exit(fmt.Sprintf("Missing hosts for %s: %v", ip, missing), 1) + } + + return cli.Exit("", 1) } diff --git a/cmd/clean.go b/cmd/clean.go index d8b251e..52ac9ff 100644 --- a/cmd/clean.go +++ b/cmd/clean.go @@ -87,7 +87,7 @@ func clean(c *cli.Context) error { return debugFooter(c) } - if err := h.Flush(); err != nil { + if err := flushHostsfile(c, h); err != nil { return cli.Exit(err.Error(), 2) } return debugFooter(c) diff --git a/cmd/remove.go b/cmd/remove.go index d51ad40..147062c 100644 --- a/cmd/remove.go +++ b/cmd/remove.go @@ -85,7 +85,7 @@ func remove(c *cli.Context) error { } logrus.Debugln("flushing hosts file to disk") - if err := hf.Flush(); err != nil { + if err := flushHostsfile(c, hf); err != nil { return cli.Exit(err.Error(), 2) } diff --git a/go.mod b/go.mod index fbade7e..bffb159 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/goodhosts/cli go 1.21 require ( - github.com/goodhosts/hostsfile v0.1.6 + github.com/goodhosts/hostsfile v0.1.7 github.com/magefile/mage v1.15.0 github.com/olekukonko/tablewriter v0.0.5 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 github.com/urfave/cli/v2 v2.26.0 github.com/uwu-tools/magex v0.10.0 diff --git a/go.sum b/go.sum index 48af9e6..bd5b2ba 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= -github.com/goodhosts/hostsfile v0.1.6 h1:aK6DxpNV6pZ1NbdvNE2vYBMTnvIJF5O2J/8ZOlp2eMY= -github.com/goodhosts/hostsfile v0.1.6/go.mod h1:bkCocEIf3Ca0hcBustUZoWYhOgKUaIK+47m8fBjoBx8= +github.com/goodhosts/hostsfile v0.1.7 h1:g8G2EU8t22AA7gNkXgC3kzj3aKvQBAf7azUonff1lNU= +github.com/goodhosts/hostsfile v0.1.7/go.mod h1:JsGCIqafGGoOkWYRPKowm/fH400CocwW+vIzFYEmZ2A= github.com/icrowley/fake v0.0.0-20221112152111-d7b7e2276db2 h1:qU3v73XG4QAqCPHA4HOpfC1EfUvtLIDvQK4mNQ0LvgI= github.com/icrowley/fake v0.0.0-20221112152111-d7b7e2276db2/go.mod h1:dQ6TM/OGAe+cMws81eTe4Btv1dKxfPZ2CX+YaAFAPN4= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -31,14 +31,10 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk= github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA= github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=