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

I need to be able to search a binary file for a string of hexadecimal characters and then prompt the user so they can decide if they want to change it. If they enter a "y" then the program changes the string and prints the change to the new file, if they enter a "n" then the program moves on to the next match. The goal is to create an identical file except where the program has replaced the search string with 00s. You can download an example of a file I would be modifying from:

http://members.lycos.co.uk/jfulley/example.bin

You're going to have to copy and paste the location into your browser since it's hosted on a free website.

The program below stops responding after it finds the first string match and prompts the user for a response. As soon as you enter a y or n the program stops responding.

$original = &promptUser("Enter original filename"); $modified = &promptUser("Enter modified filename"); open($ORIG, "$original"); binmode($ORIG); open($MOD, ">$modified"); binmode($MOD); $/ = "\x00"; while (<$ORIG>) { $count = (($change = $_) =~ s/(\x33\x33\x39\x39.*?\x00)/"\x00" x lengt +h($1)/e); if ($count == null) { print $MOD $_; } else { $answer = &promptUser("Modify $1?"); if ($answer == "y") { print $MOD $change; } else { print $MOD $_; } } } close $MOD; close $ORIG; sub promptUser { local($promptString,$defaultValue) = @_; if ($defaultValue) { print $promptString, "[", $defaultValue, "]: "; } else { print $promptString, ": "; } $| = 1; # force a flush after print $_ = <STDIN>; # get the input from STDIN chomp; if ("$defaultValue") { return $_ ? $_ : $defaultValue; # return $_ if it has a value } else { return $_; } }

Replies are listed 'Best First'.
Re: perl command line prompts
by Forsaken (Friar) on May 16, 2005 at 07:52 UTC
    After some tinkering:
    use strict; use warnings; my $original = &promptUser("Enter original filename"); my $modified = &promptUser("Enter modified filename"); open(my $ORIG, "$original") or die "could not open $original"; binmode($ORIG); open(my $MOD, ">$modified") or die "could not open $modified"; binmode($MOD); # commented out $/ = "\x00"; while (<$ORIG>) { my $count = ((my $change = $_) =~ s/(\x33\x33\x39\x39.*?\x00)/"\x00" + x length($1)/e); if ($count == 0) { print $MOD $_; } else { my $answer = &promptUser("Modify $1?"); if ($answer eq "y") # <-- eq for test, == is for numerical compari +sons { print $MOD $change; } else { print $MOD $_; } } } close $MOD; close $ORIG; sub promptUser { my($promptString, $defaultValue) = @_; if ($defaultValue) { print $promptString, "[", $defaultValue, "]: "; } else { print $promptString, ": "; } $| = 1; # force a flush after print my $line = <STDIN>; # get the input from STDIN chomp($line); if ($defaultValue) { return $line ? $line : $defaultValue; } # return $line if it ha +s a value else { return $line; } }
    Turns out it was your $/ that caused it. Btw, is there supposed to be only 1 change in the entire file?

    Oh, and please, in the future, for the sake of your own sanity as well as ours:
    - use strict;
    - use warnings;
    - indent
    - just like in english(or other languages), a space after a comma please
    - (personal opinion) assign $_ to a named variable if you're going to use that value for more than the most basic of functions.

    Remember rule one...

Re: perl command line prompts
by northwind (Hermit) on May 16, 2005 at 05:52 UTC

    <disclaimer>Code untested</disclaimer>
    Quick list of things that hit me:

    • Are you sure you want local($promptString,$defaultValue) instead of my ($promptString,$defaultValue) ?
    • $| needs to be either localized (best) or turned on and off immediately around the print in question.  How it is used now is equivalent to putting $| = 1; at the top of your posted code segment.
    • Ideally, the three argument form of open leaves less ambiguity.
    • As your record separator is set to the same thing you are s///ing for, why not use chomp and be done with it? I rethought this one, my idea would not work.
    • I know advocating a One True Brace Style will spark a holy war.  But as it stands, it is difficult to tell at a glance what is inside what block.
    • Have you split $count = (($change = $_) =~ s/(\x33\x33\x39\x39.*?\x00)/"\x00" x length($1)/e); up into its constituent parts to verify it's doing what you want?
    • Have you injected print STDERR $somevar at strategic locations in your code?  You've stated that it fails after the first y user input.  This would answer how far after the user input it fails (which could give some important clues).

      I've tried your suggestions and the program still stalls at the same point. As for the "$count =((...." line, that's probably the only part of the program that I'm sure is working properly. The $count variable takes the form of "1" if a change is made and stays null if no changes are made. If a match is found, the $change variable copies the $_ variable and then all of the $change variable characters are replaced with 00s. I think that there is some kind of conflict between the loop and the promptUser subroutine. I've tried using the promptUser command within a while loop and a foreach loop and it only works once in each of them and then stalls. However, the promptUser command works fine outside of a loop. Where is the conflict? Why is this happening?
Re: perl command line prompts
by tlm (Prior) on May 16, 2005 at 09:35 UTC

    I think Forsaken is on the right track (your program is waiting for $/, which is now "\x00", to finish reading the user's input, but it is getting newlines instead), but I also think that you need that $/ = "\x00" for what your program is doing. Refraining from refactoring your code too much:

    my $orig_IRS = $/; # save the original input record separator $/ = "\x00"; while (<$ORIG>) { $count = (($change = $_) =~ s/(\x33\x33\x39\x39.*?\x00)/"\x00" x length($1)/e); if ($count == null) { print $MOD $_; } else { local $/ = $orig_IRS; # restore input record separator locally $answer = &promptUser("Modify $1?"); if ($answer == "y") { print $MOD $change; } else { print $MOD $_; } }

    Alternatively, you could set the terminal to raw mode, so that the user did not need to hit Enter after y/n. See the ReadMode function of Term::ReadKey.

    Update: Another possibility is to make this single change to your script:

    while ( defined ( $_ = do { local $/ = "\x00"; <$ORIG> } ) ) { # ... }

    the lowliest monk

      The program doesn't stall anymore after a command line prompt, but it replaces the matched strings even if you enter a "n".

      if ($answer == "y") { print $MOD $change; } else { print $MOD $_; }
      No matter what character is entered at the command prompt, the program replaces all of the matches. How can I fix this? Thanks.
        two ways:
        if ($answer eq "y") { #this is a string comparison. == is for numbers +only
        or (maybe better)
        if ($answer =~ /^y/) {
        The latter will also work if the user types "yes", "yup", etc.


        holli, /regexed monk/

        When reading your original post I missed the bug that holli's and Transient's replies fix. You must have seen this advice a trillion times: always run with strict and warnings. If you had, perl would have told you what the problem was.

        the lowliest monk