Skip to content

Commit bbb1441

Browse files
committed
Add batch check command and safety features to CLI
Features added: - Extended check command to support batch checking: `check <IP> <host1> <host2>...` - Added --quiet flag for check command (only return exit codes) - Added --any flag for check command (match ANY instead of ALL hosts) - Added --backup global flag to create backup before modifying hosts - Added --safe global flag to detect concurrent modifications - Added flushHostsfile() helper that implements backup and safety checks Command updates: - add, remove, clean now use flushHostsfile() for safe writes - Backward compatible: single-arg check behavior preserved Dependencies: - Updated go.mod to use local hostsfile library with VVV improvements Exit codes: - 0: Success (host(s) found or operation completed) - 1: Check failed (host not found) - 2: Invalid arguments This enables VVV vagrant plugin to: - Check hosts without sudo (read-only operation) - Only request sudo when hosts are actually missing
1 parent 6a2a597 commit bbb1441

7 files changed

Lines changed: 112 additions & 22 deletions

File tree

cmd/add.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func add(c *cli.Context) error {
6868
}
6969

7070
logrus.Debugln("flushing hosts file to disk")
71-
if err := hf.Flush(); err != nil {
71+
if err := flushHostsfile(c, hf); err != nil {
7272
return cli.Exit(err.Error(), 2)
7373
}
7474

cmd/app.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ var (
6060
Aliases: []string{"q"},
6161
Usage: "Turn on off all logging",
6262
},
63+
&cli.BoolFlag{
64+
Name: "backup",
65+
Aliases: []string{"b"},
66+
Usage: "Create backup before modifying hosts file",
67+
},
68+
&cli.BoolFlag{
69+
Name: "safe",
70+
Usage: "Check for concurrent modifications before writing",
71+
},
6372
},
6473
}
6574
)
@@ -110,6 +119,28 @@ func loadHostsfile(c *cli.Context, readOnly bool) (*hostsfile.Hosts, error) {
110119
return hf, nil
111120
}
112121

122+
func flushHostsfile(c *cli.Context, hf *hostsfile.Hosts) error {
123+
if c.Bool("safe") {
124+
modified, err := hf.HasBeenModified()
125+
if err != nil {
126+
return err
127+
}
128+
if modified {
129+
return fmt.Errorf("hosts file was modified by another process; reload and retry")
130+
}
131+
}
132+
133+
if c.Bool("backup") {
134+
if err := hf.Backup(); err != nil {
135+
logrus.Warnf("Failed to create backup: %v", err)
136+
} else {
137+
logrus.Debugf("Backup created at: %s", hf.BackupPath())
138+
}
139+
}
140+
141+
return hf.Flush()
142+
}
143+
113144
func outputHostsfile(hf *hostsfile.Hosts, all bool) {
114145
for _, line := range hf.Lines {
115146
if !all {

cmd/check.go

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,96 @@ func Check() *cli.Command {
1212
return &cli.Command{
1313
Name: "check",
1414
Aliases: []string{"c"},
15-
Usage: "Check if ip or host exists",
15+
Usage: "Check if ip or host exists. With IP and hosts: check if all hosts are mapped to IP",
1616
Action: check,
17-
ArgsUsage: "[IP|HOST]",
17+
ArgsUsage: "[IP|HOST] or [IP] [HOST] ([HOST]...)",
18+
Flags: []cli.Flag{
19+
&cli.BoolFlag{
20+
Name: "quiet",
21+
Aliases: []string{"q"},
22+
Usage: "Suppress output, only return exit code",
23+
},
24+
&cli.BoolFlag{
25+
Name: "any",
26+
Usage: "With multiple hosts: succeed if ANY host matches (default: ALL must match)",
27+
},
28+
},
1829
}
1930
}
31+
2032
func check(c *cli.Context) error {
2133
if c.Args().Len() < 1 {
2234
logrus.Infof("No input, pass an ip address or hostname to check.")
2335
return nil
2436
}
2537

26-
hf, err := loadHostsfile(c, true)
38+
hf, err := loadHostsfile(c, true) // readOnly=true, no sudo needed
2739
if err != nil {
2840
return err
2941
}
30-
input := c.Args().First()
3142

32-
if net.ParseIP(input) != nil {
33-
if hf.HasIP(input) {
34-
logrus.Infof("%s exists in hosts file\n", input)
43+
args := c.Args().Slice()
44+
firstArg := args[0]
45+
quiet := c.Bool("quiet")
46+
47+
// Single argument: check if IP or hostname exists (backward compatible)
48+
if c.Args().Len() == 1 {
49+
if net.ParseIP(firstArg) != nil {
50+
if hf.HasIP(firstArg) {
51+
if !quiet {
52+
logrus.Infof("%s exists in hosts file\n", firstArg)
53+
}
54+
return nil
55+
}
56+
}
57+
58+
if hf.HasHostname(firstArg) {
59+
if !quiet {
60+
logrus.Infof("%s exists in hosts file\n", firstArg)
61+
}
62+
return nil
63+
}
64+
65+
return cli.Exit(fmt.Sprintf("%s does not match anything in the hosts file", firstArg), 1)
66+
}
67+
68+
// Multiple arguments: first must be IP, rest are hostnames
69+
if net.ParseIP(firstArg) == nil {
70+
return cli.Exit(fmt.Sprintf("%s is not a valid IP address. When checking multiple hosts, first argument must be an IP.", firstArg), 2)
71+
}
72+
73+
ip := firstArg
74+
hosts := args[1:]
75+
76+
if c.Bool("any") {
77+
// Check if ANY host is mapped to IP
78+
if hf.HasAny(ip, hosts...) {
79+
if !quiet {
80+
logrus.Infof("At least one host is mapped to %s\n", ip)
81+
}
3582
return nil
3683
}
84+
return cli.Exit(fmt.Sprintf("None of the specified hosts are mapped to %s", ip), 1)
3785
}
3886

39-
if hf.HasHostname(input) {
40-
logrus.Infof("%s exists in hosts file\n", input)
87+
// Default: Check if ALL hosts are mapped to IP
88+
if hf.HasAll(ip, hosts...) {
89+
if !quiet {
90+
logrus.Infof("All specified hosts are mapped to %s\n", ip)
91+
}
4192
return nil
4293
}
4394

44-
return cli.Exit(fmt.Sprintf("%s does not match anything in the hosts file", input), 1)
95+
// Report which hosts are missing
96+
if !quiet {
97+
missing := []string{}
98+
for _, host := range hosts {
99+
if !hf.Has(ip, host) {
100+
missing = append(missing, host)
101+
}
102+
}
103+
return cli.Exit(fmt.Sprintf("Missing hosts for %s: %v", ip, missing), 1)
104+
}
105+
106+
return cli.Exit("", 1)
45107
}

cmd/clean.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func clean(c *cli.Context) error {
8787
return debugFooter(c)
8888
}
8989

90-
if err := h.Flush(); err != nil {
90+
if err := flushHostsfile(c, h); err != nil {
9191
return cli.Exit(err.Error(), 2)
9292
}
9393
return debugFooter(c)

cmd/remove.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func remove(c *cli.Context) error {
8585
}
8686

8787
logrus.Debugln("flushing hosts file to disk")
88-
if err := hf.Flush(); err != nil {
88+
if err := flushHostsfile(c, hf); err != nil {
8989
return cli.Exit(err.Error(), 2)
9090
}
9191

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
github.com/magefile/mage v1.15.0
88
github.com/olekukonko/tablewriter v0.0.5
99
github.com/sirupsen/logrus v1.9.3
10-
github.com/stretchr/testify v1.8.2
10+
github.com/stretchr/testify v1.8.4
1111
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816
1212
github.com/urfave/cli/v2 v2.26.0
1313
github.com/uwu-tools/magex v0.10.0
@@ -26,3 +26,6 @@ require (
2626
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
2727
gopkg.in/yaml.v3 v3.0.1 // indirect
2828
)
29+
30+
// Use local hostsfile library with VVV improvements
31+
replace github.com/goodhosts/hostsfile => ../hostsfile

go.sum

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
1111
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1212
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
1313
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
14-
github.com/goodhosts/hostsfile v0.1.6 h1:aK6DxpNV6pZ1NbdvNE2vYBMTnvIJF5O2J/8ZOlp2eMY=
15-
github.com/goodhosts/hostsfile v0.1.6/go.mod h1:bkCocEIf3Ca0hcBustUZoWYhOgKUaIK+47m8fBjoBx8=
1614
github.com/icrowley/fake v0.0.0-20221112152111-d7b7e2276db2 h1:qU3v73XG4QAqCPHA4HOpfC1EfUvtLIDvQK4mNQ0LvgI=
1715
github.com/icrowley/fake v0.0.0-20221112152111-d7b7e2276db2/go.mod h1:dQ6TM/OGAe+cMws81eTe4Btv1dKxfPZ2CX+YaAFAPN4=
1816
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -31,14 +29,10 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
3129
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
3230
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
3331
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
34-
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
35-
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
3632
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
3733
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
38-
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
39-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
40-
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
41-
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
34+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
35+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
4236
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk=
4337
github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA=
4438
github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=

0 commit comments

Comments
 (0)