# Extracting an IP address from a hosts file by hostname

I am currently working with Vagrant for the first time, and therefore also working with Ruby for the first time. I have a specific hostname and IP address I would like set for my VM, both of which are stored in my hosts file. I would like to define the hostname in my Vagrantfile, and then have it automatically pull the matching IP address out of my hosts file for me. To do so, I have written the following code in my Vagrantfile:

Vagrant.configure("2") do |config|
hostname = "vagrantvm"
hostfile = "/etc/hosts"

ip_address = File.foreach(hostfile).grep(/\b#{hostname}\b/) { |m| /^([0-9\.]*)\b.*/.match(m)[1] }.first

config.vm.hostname = hostname
config.vm.provision :shell, :path => "bootstrap.sh"
end


Line 5 where I am assigning ip_address works, however I am not sure if it is the right way to do it. It seems a little ugly and long. I was thinking that maybe I should not put the code block passed to grep all on the same line. Or is it even necessary to have 2 separate regexes in there? Can it be done in 1 step?

It also seems strange to me that I need to add .first on the end in order to get the IP address string out of the array returned by grep. I feel like there is a better way to do this that I am missing.

Unless you are trying to parse arbitrary hosts files why not use the built in IP libraries?

require 'ipaddr'
...


You could also look at this vagrant add-in: https://github.com/cogitatio/vagrant-hostsupdater This might work too https://github.com/devopsgroup-io/vagrant-hostmanager though I haven't looked at it in depth.

Just a note with your code: you should probably check for commented out lines in your hosts file (at least if it looks anything like mine)

• Well then. That seems like a much better solution. Unfortunately those plugins don't seem do to quite what I want, since I have a pre-existing host file with pre-defined IPs, rather than having the VMs define their own. However, I think using IPSocket.getaddress is the way to go. Thanks! – gla3dr May 11 '17 at 21:40

You can probably get by with this:

ip = File.read(hostfile).match(/^\s*([\d\.]+)\s+\b#{hostname}\s*\$/)[1]


A more "complete" way might be to do:

hosts = File.foreach(hostfile)
.grep(/^\s*([\d\.:]+)/)
.map { |line| line.strip.split(/\s+/).reverse }
.to_h


Which gives you a { hostname => IP } hash for everything in the file (IPv6 included for good measure). I.e. you'd just do

ip_address = hosts[hostname]


It seems a little ugly and long

It is a long line, but you can always break it up. You can chain off of the end keyword just as well as off of a }, so something like this would help readability:

File.foreach(hostfile).grep(/\b#{hostname}\b/) do |m|
/^([0-9\.]*)\b.*/.match(m)[1]
end.first


In the 2nd approach above, I could've used grep to both match and map the lines, but I thought it nicer to separate the two.

It also seems strange to me that I need to add .first on the end in order to get the IP address string out of the array returned by grep

grep is being called for each line, meaning it'll produce an array of matching lines. Hence why you need first. You're basically filtering and mapping every line in the file. After filtering there's (presumably!) only one line left, but it'll still be in an array.

You could use detect if you're only interested in the first matching line:

line = File.foreach(hostfile).detect { |line| line =~ %r[\b#{hostname}\b] }


That'll give you only the line containing the hostname you're looking for (or nil), which you can then parse further.

• This is awesome, thanks for all the tips and explanation! – gla3dr May 11 '17 at 21:40