0

I have a file in which i have to replace all the words like $xyz and for them i have to substitutions like these:

$xyz with ${xyz}.
$abc_xbs with ${abc_xbc}
$ab,$cd  with ${ab},${cd}

This file also have some words like ${abcd} which i don't have to change. I am using this command

sed -i 's?\$([A-Z_]+)?\${\1}?g' file

its working fine on command line but not inside a perl script as

sed -i 's?\$\([A-Z_]\+\)?\$\{\1\}?g' file;

What i am missing? I think adding some backslashes would help.I tried adding some but no success.

Thanks

confused
  • 77
  • 8

1 Answers1

1

In a Perl script you need valid Perl language, just like you need valid C text in a C program. In the terminal sed.. is understood and run by the shell as a command but in a Perl program it is just a bunch of words, and that line sed.. isn't valid Perl.

You would need this inside qx() (backticks) or system() so that it is run as an external command. Then you'd indeed need "some backslashes," which is where things get a bit picky.

But why run a sed command from a Perl script? Do the job with Perl

use warnings;
use strict;
use File::Copy 'move';

my $file     = 'filename';
my $out_file = 'new_' . $file;

open my $fh,     '<', $file     or die "Can't open $file: $!";
open my $fh_out, '>', $out_file or die "Can't open $out_file: $!"; 

while (<$fh>) 
{
    s/\$( [^{] [a-z_]* )/\${$1}/gix;
    print $fh_out $_;
}
close $fh_out;
close $fh;

move $out_file, $file or die "Can't move $out_file to $file: $!";

The regex uses a negated character class, [^...], to match any character other than { following $, thus excluding already braced words. Then it matches a sequence of letters or underscore, as in the question (possibly none, since the first non-{ already provides at least one).

With 5.14+ you can use the non-destructive /r modifier

print $fh_out s/\$([^{][a-z_]*)/\${$1}/gir;

with which the changed string is returned (and original is unchanged), right for the print.

The output file, in the end moved over the original, should be made using File::Temp. Overwriting the original this way changes $file's inode number; if that's a concern see this post for example, for how to update the original inode.

A one-liner (command-line) version, to readily test

perl -wpe's/\$([^{][a-z_]*)/\${$1}/gi' file

This only prints to console. To change the original add -i (in-place), or -i.bak to keep backup.


A reasonable question of "Isn't there a shorter way" came up.

Here is one, using the handy Path::Tiny for a file that isn't huge so we can read it into a string.

use warnings;
use strict; 
use Path::Tiny;

my $file     = 'filename';
my $out_file = 'new_' . $file;

my $new_content = path($file)->slurp =~ s/\$([^{][a-z_]*)/\${$1}/gir;

path($file)->spew( $new_content );

The first line reads the file into a string, on which the replacement runs; the changed text is returned and assigned to a variable. Then that variable with new text is written out over the original.

The two lines can be squeezed into one, by putting the expression from the first instead of the variable in the second. But opening the same file twice in one (complex) statement isn't exactly solid practice and I wouldn't recommend such code.

However, since module's version 0.077 you can nicely do

path($file)->edit_lines( sub { s/\$([^{][a-z_]*)/\${$1}/gi } );

or use edit to slurp the file into a string and apply the callback to it.

So this cuts it to one nice line after all.

I'd like to add that shaving off lines of code mostly isn't worth the effort while it sure can lead to trouble if it disturbs the focus on the code structure and correctness even a bit. However, Path::Tiny is a good module and this is legitimate, while it does shorten things quite a bit.

zdim
  • 64,580
  • 5
  • 52
  • 81
  • This is working fine,but its not changing the file but printing the output on STDOUT , do i have to write it in a temp file , then copy that file .Isn't there a shorter way to accomplish my objective? – confused Oct 13 '17 at 06:41
  • @confused Edited -- let me know if more explanations would be helpful. – zdim Oct 13 '17 at 06:57
  • @confused Added a much shorter way. – zdim Oct 13 '17 at 08:16
  • Hey i have noticed that , there are also some strings like $(xyz) , i don't want to substitute them either. What change , i have to do in the present expression ? – confused Oct 13 '17 at 08:37
  • got it just added backslash before ( – confused Oct 13 '17 at 08:39
  • @confused For `$(xyz)` just add the `(` to the not-these list, so `[^{(]`. The `[^...]` means "any character not listed", so `[^{(]` is any one character that is neither `{` nor `(`. In general, if you have more such cases you can add to the list. The `[^{([]` would be any char other than either of `{`, `(`, `[`. The _right_ `]` would have to be escaped of course (along with a few others) The "_character class_" lists individual chars to match, so `[ab]` matches either `a` or `b` -- need not be the whole `ab`. Same for negated `[^..]`. – zdim Oct 13 '17 at 08:46