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

Hello fellow Monks,

I would like to substitute globally in a File with s///g and then print the changed lines and line numbers to a log. How can such thing be done? I would use a while (s///) but in some cases that can lead to infinite loops because substitute starts always from the beginning (eg. s/T/TT/).

Andre

Replies are listed 'Best First'.
Re: Global substitution and report
by FunkyMonk (Bishop) on Aug 27, 2007 at 08:21 UTC
    Investigate perl's -i option in perlrun to edit files in place. You'll need to open your log file, too.

    Your main loop could look something like:

    while ( <$IN> ) { print $LOG $_ if ( s/t/tt/ig ); print $OUT $_; }

    A global substitute, as above, isn't going to cause an infinite loop. It'll change

    I would like to substitute globally

    to

    I would like tto substtittutte globally

    In-place editing isn't always appropriate, so you may have to write a temp file and rename it once you've processed the input.

    Why don't you have a go and come back here if you get stuck?

    Good luck!

      But this would disable multi-line substitutions. So I need a way to replace in a text buffer that is loaded from a file.
      I would like to use the same feature also for already loaded files in a editor window.
      I also create a backup of the file before applying the changes so that they can be undone latter.
        In which case, I'd slurp the file in, do the substitutions and write it back out to a temp file. Then I'd run diff on the original & temp files.

Re: Global substitution and report
by Anno (Deacon) on Aug 27, 2007 at 08:33 UTC
    You can use while ( s///g ) safely (note the /g modifier). In scalar context, s///g will restart matching where the last match left off, so no infinite loop. However, tracing lines and particularly line numbers will be hard.

    Since you mention changed lines and line numbers it appears that you expect changes to be limited to single lines, as opposed to changes spanning multiple lines. If so, you'd be much better off looping over the file line by line and logging changes as they happen. Note the $. variable, which will give you the line number.

    Anno

      No, but the line where the substitution starts would be enough. So going line by line isn't really an option since this would disable multi-line substitutions. Isn't there a cool trick using the e modifier for substitutions?
        You could try to push the value of pos onto an array.

        $str =~ s{$pattern}{push @positions, pos $str; $substitution}eg

        I never worked with pos so I don't know if that helps you.

        How do you expect /e could help? It only changes the behavior of the substitution side of s///.

        One possibility is to step through substrings of the file content, starting at each new line. Here is a sketch:

        my $content = do { local $/; <DATA> }; my ( $ln, $start) = ( 1, 0); while ( $start < length $content ) { if ( substr( $content, $start) =~ s/.../.../ ) { print "line $ln changed\n"; } $start = 1 + index( $content, "\n", $start); ++ $ln; } __DATA__ aaa bbb ccc ddd eee
        Anno
Re: Global substitution and report
by moritz (Cardinal) on Aug 27, 2007 at 08:22 UTC
    If you use the /g modifier, you won't have infinite loops.

    while (my $line = <$infile>){ if ($line =~ s/T/TT/g){ # log $line and $. (line number) } print $new_file $line; } # overwrite old file with new file here
      But this would disable multi-line substitutions. So I need a way to replace in a text buffer that is loaded from a file.
      I would like to use the same feature also for already loaded files in a editor window.