Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl-Sensitive Sunglasses
 
PerlMonks  

Read in hostfile, modify, output

by razmeth (Acolyte)
on Dec 16, 2016 at 23:59 UTC ( [id://1177963]=perlquestion: print w/replies, xml ) Need Help??

razmeth has asked for the wisdom of the Perl Monks concerning the following question:

I'm looking to do something which is relatively simple, so I assume there's a way to do it simpler/more elegantly than I am doing it. If that's the case, I could use any assistance to cleanup my code. The goal is to read in the host file from my pc, and comment/uncomment a specific line. The below code works (as long as perl.exe is set to run as an Admin), I am just looking to find if there's a simpler version.
use strict; open(my $In, '<', "C:/Windows/System32/drivers/etc/hosts") or die $!; my @Output = <$In>; map{if($_ =~ /^(#)?(10\.10\.1\.2.*)/){$_ = $1 eq '#' ? $2 : "#$2";}} @ +Output; close $In; open(Out, '>', "C:/Windows/System32/drivers/etc/hosts") or die $!; print Out @Output;

Replies are listed 'Best First'.
Re: Read in hostfile, modify, output
by eyepopslikeamosquito (Archbishop) on Dec 17, 2016 at 01:50 UTC

    Let me start by cautioning you to not run your code directly on your system hosts file. It's much safer to take a local copy, run your script on the local copy, then do a "diff" with the original as a check, then copy it over manually. Do it this way at least until you are certain your script is sound and bullet-proof ... which it is not currently, as described in detail below.

    It is a bit hard to critique your code because you have not clearly stated the intent. From a cursory scan of your code I will assume that the intent is:

    Update the hosts file in place. If the hosts file does not contain the ip address 10.10.1.2 do nothing. If it contains a line starting with host 10.10.1.2, comment it out. If it contains a commented out line starting with host 10.10.1.2, uncomment it. It is unclear to me whether your intent is a specific ip address, namely 10.10.1.2, or all ip addresses that start with 10.10.1.2 (which is what your code is actually doing). Please let us know.
    In any case, most of my comments are general and independent of your specific intent.

    First, you should always start your Perl code with:

    use strict; use warnings;
    You forgot the "use warnings". Adding that reveals that your code throws a warning:
    Use of uninitialized value in string eq
    in this line:
    map {if($_ =~ /^(#)?(10\.10\.1\.2.*)/){$_ = $1 eq '#' ? $2 : "#$2";}} +@Output;
    Looking at this line of code gives me a headache. You should aim for simplicity, clarity and correctness in your code. I further noticed from running your program that the above line contains a bug in that it loses the terminating newline.

    Staring at the offending line of code, we see that you are using map in void context -- which is generally frowned upon. You see, map returns a list, yet you are throwing it away! According to Perl luminary tchrist (Tom Christiansen), this "makes for muddled code".

    Next, we see you are modifying a list with map, which is also frowned upon. Effective Perl Programming item 20, "Use foreach, map and grep as appropriate", provides a nice summary of when to use foreach, map and grep:

    • Use foreach to iterate read-only over each element of a list
    • Use map to create a list based on the contents of another list
    • Use foreach to modify elements of a list
    • Use grep to select elements in a list
    Here you are modifying the elements of a list and so should use foreach in preference to map.

    Finally, be aware that the way you've written the code is unsound in that it offers the potential for permanent (and possibly silent) data loss. Consider what happens if your line:

    print Out @Output;
    fails. You're not checking print's return value, so you may not even be aware that it's failed. If it fails, your hosts file will be corrupted. This is an important file. If you don't have a backup copy, you will have suffered permanent data loss. This topic is discussed at length in:

    In summary then, I would write your program something like this:

    use strict; use warnings; # Update $fname re-runnably. # $fcontents is either a string or a reference to an array of lines. sub atomic_update { my ( $fname, $fcontents ) = @_; print "updating '$fname'..."; my $bak = $fname . $$ . '.tmp'; -e $bak and ( unlink($bak) or die "error: unlink '$bak': $!" ); open( my $fh, '>', $bak ) or die "error: open '$bak': $!"; print {$fh} ref($fcontents) ? @{$fcontents} : $fcontents or die "error: print '$bak': $!"; close($fh) or die "error: close '$bak': $!"; rename( $bak, $fname ) or die "error: rename '$bak' '$fname': $!"; print "done.\n"; } my $hostfile = shift || "C:/Windows/System32/drivers/etc/hosts"; print "hostfile : $hostfile\n"; open(my $In, '<', $hostfile) or die "error: open '$hostfile': $!"; my @Output = <$In>; close $In; # If 10.10.1.2 is commented out, uncomment it. # If it is not commented out, comment it out. my $nchanges = 0; for (@Output) { if ( s/^#(?=10\.10\.1\.2\b)// ) { ++$nchanges; next; } if ( s/^(?=10\.10\.1\.2\b)/#/ ) { ++$nchanges; next; } } if ($nchanges > 0) { atomic_update( $hostfile, \@Output ); } print "There were $nchanges changes made to file '$hostfile'\n";

    Please note that my code is checking specifically for ip address 10.10.1.2, not anything starting with 10.10.1.2. To instead change ip addresses starting with 10.10.1.2, simply lose the "\b" in the regexes, like this:

    s/^#(?=10\.10\.1\.2)// s/^(?=10\.10\.1\.2)/#/

Re: Read in hostfile, modify, output
by choroba (Cardinal) on Dec 17, 2016 at 00:30 UTC
    Use map if you want to create a new list from an old one. To change an existing array, use for instead.

    Also, (#)? would be more readable as (#?) , as you want to match the octothorpe if present, not to match it always, but maybe zero times.

    You correctly used a lexical filehandle for the input. Use a lexical filehandle for the output, too.

    The preferred way of processing files, though, is to read them line by line if possible. You need to print to a temporary file, as you can't read a file and print to it at the same time.

    #!/usr/bin/perl use warnings; use strict; my $file = 'C:/Windows/System32/drivers/etc/hosts'; my %replace = ( '#' => q(), '' => '#' ); open my $In, '<', $file or die $!; open my $Out, '>', "$file.new" or die $!; while (<$In>) { s/(#?)(10\.10\.1\.2)/$replace{$1}$2/; print {$Out} $_; } close $Out or die $!; rename "$file.new", $file or die $!;

    ($q=q:Sq=~/;[c](.)(.)/;chr(-||-|5+lengthSq)`"S|oS2"`map{chr |+ord }map{substrSq`S_+|`|}3E|-|`7**2-3:)=~y+S|`+$1,++print+eval$q,q,a,
Re: Read in hostfile, modify, output
by Marshall (Canon) on Dec 17, 2016 at 04:48 UTC
    I will demo another way (of many) to write the regex code below.

    As others have said, this line,

    map{if($_ =~ /^(#)?(10\.10\.1\.2.*)/){$_ = $1 eq '#' ? $2 : "#$2";}} @ +Output;
    gives me a headache.

    Do not make the mistake of assuming that fewer lines of Perl code means more efficient or "better" code. This map is an implied foreach loop whether you want to write the source code that way or not.

    I prefer to translate $1, $2, etc into named variables when possible. Compared with firing up the regex engine, this is very "cheap" CPU wise. Also, white space consumes no MIPs! Space things out and write something that you will understand 2 years from now.

    #!/usr/bin/perl use strict; use warnings; my @hosts = ("10.10.1.2 # some comment ...\n", "#10.10.1.56\n", "14.1.2.89\n"); my $pattern = '10.10.1'; foreach my $line (@hosts) { if ( my ($comment, $rest) = $line =~ /^(#)?($pattern.*\n)/) { if (defined $comment) { $line = $rest; # delete the beginning # comment char } else { $line = "#$rest"; # add a beginning # comment char } } } print @hosts; __END__ #10.10.1.2 # some comment ... 10.10.1.56 14.1.2.89
    Your idea of reading the hosts file into memory, my @Output = <$In>; is a good one - this file will not be "huge" and this is fine.

    The idea of over-writing the original file with the modified contents is not so good. That will work most of the time, but I would make a temp file of some sort, write to that, then rename the temp file to the host file name. If for nothing else, this will help with the debugging!

    There is no truly "atomic" operation on the file system - something can always go wrong within a fileop. Good system code plans for that and also makes the "window of vulnerability" as short as possible.

      There is no truly "atomic" operation on the file system

      From the Open Group Specification of rename:

      That specification requires that the action of the function be atomic
      That is, the rename(1) system call is required to be atomic on all Unix systems. As tye points out, rename is also atomic on Windows.

      In practice, on Unix and Windows, so long as both files reside on the same OS file system, you should be able to rely on the Perl rename function being atomic.

        Geez, I see that my poorly explained comment about atomic operations generated some discussion.

        To digress a bit, Wiki Linearizability contains a good discussion about what "atomic" means. I'll just quote part: Atomicity is a guarantee of isolation from concurrent processes. Additionally, atomic operations commonly have a succeed-or-fail definition, they either successfully change the state of the system, or have no apparent effect.

        In the case of a single rename(), as long as the system software and hardware work as expected, a single rename can be considered atomic. There are ways (like system reset (hardware,software or human induced) or power supply transient) that can mess things up.

        To replace an important file with a new version, first get the new file onto the system in its entirety. Now a quick two step dance happens. At a minimum, rename the existing file to something else, probably some sort of .bak. Then as soon as possible, rename the new file to what the existing file's name was. With everything working perfectly, there is a brief interval where "File" does not exist - that happens between the 2 renames. Any open() attempted during that very short interval will fail.

        At the lowest level (disk writing hardware), a disk write operation is not "atomic" and can fail this test: either successfully change the state of the system, or have no apparent effect. When writing a sector on the HD, the sequence: a) Old data, b)No data(the write is occuring), c)New data occurs. If something goes wrong during step (b), then we are left with unknown garbage. Even the very fast rename function goes through these 3 steps.

        Summary: Rename is the fastest and best function to use for replacing files. Get the new file completely ready in advance. Do not mess around with writing the new file line by line - that is too slow and opens up corruption issues.

Re: Read in hostfile, modify, output -- oneliner
by Discipulus (Canon) on Dec 17, 2016 at 09:06 UTC
    Hello razmeth

    If you want to keep it short i'd go with a oneliner: -i as described in perlrun let you to specify an additional extension for the backup file.

    Also note that, as you are running into an unfriendly OS, you can lost you if use Perl 32bit instead of Perl 64bit: read Re: Cannot execute external process: Win32 Win64 SysWOW64 and System32 redirection and you'll know why i use %WINDIR%\Sysnative\drivers\etc\hosts weird syntax. Other infos at my homenode

    The following oneliner comment/uncomment any 10.10.1.2* association in your file

    perl -i.OLD -nle "$re=qr(^(#?)\s*(10\.10\.1\.2.*));if(/$re/){$1?print +$2:print qq(#$2)}" %WINDIR%\Sysnative\drivers\etc\hosts

    L*

    There are no rules, there are no thumbs..
    Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://1177963]
Approved by kcott
Front-paged by 1nickt
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others wandering the Monastery: (7)
As of 2024-04-23 07:45 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found