1

Newbie learning Linux and perl:

I would like to (a) search-replace strings, (b) match filename pattern .myfile., (c) look recursively in subdirectories, and (d) print the line number, file name, original line and changed line.

I am looking for the two versions using sed and in perl syntax. I found a starting point in this question but:

First: sed

Need: Add syntax for pattern matching for .myfile., recursive search, print line number, and get rid of w /dev/fd/2 --I don't know what it does, I want to print results to terminal, not an output file. Note: I am using Visual Studio terminal on mac OS):

find . -type f -printf '\n%p:\n' -exec sed -i '/foo/{
h
s//bar/g
H
x
s/\n/ >>> /
w /dev/fd/2
x
}' {} \;

Output:

./file1:
foo stuff >>> bar stuff
more foo >>> more bar

./file2:

./file3:
foo first >>> bar first
third: foo >>> third: bar

Second: perl:

Need: Add syntax for pattern matching for .myfile., and recursive search. ALSO the line number output by $. is wrong. It is correct for the first file, but it keeps counting and doesn't reset as it searches more files (see attached screenshot for output).

$ find . -type f | 
   xargs perl -i -ne '$was=$_; chomp($was);
                      s/abc/def/ && print STDERR "$ARGV($.): $was : $_"' 
./foo(1): fooabcbar : foodefbar

1 Answer 1

2

Those codes modify files in place (well, would be for the perl one if -p was used in place of -n; with -n, that results in files being emptied) and at the same time print to stderr (via w /dev/fd/2 or print STDERR) what was modified.

However find . | xargs is wrong as the output of find is not compatible with the input format expected by xargs. Also, in perl, $. which contains the current input line number is not reset between files unless you close the ARGV handle at the end of each file so it should be:

find . -name '*.myfile' -exec perl -i -pe '
  chomp($was = $_);
  print STDERR "$ARGV($.): $was : $_" if s/abc/def/;
  close ARGV if eof' -- {} +

For sed, not sure macos supports /dev/fd/2 to mean stderr, but even on Linux-based systems (macos is not Linux btw), using w /dev/fd/2 would be wrong unless stderr goes to specific types of files such as pipes or terminal devices. -printf is specific to the GNU implementation of find and is not found in any other implementation including macos'.

Also on macos' sed like on FreeBSD's on which it is based, you need -i '' instead of -i for in-place editing. sed has = to report the line number but you can't make any manipulation on it like embed it in a string. It's also not reset between files in POSIX-compliant sed implementations (in the case of GNU sed, it's reset when using the -i or -s non-standard options).

Note that it will edit (and replace) every .myfile file even those that don't contain abc. You could work around that with:

find . -name '*.myfile' -type f -exec grep -l --null abc {} + |
  xargs -0 perl -i -pe '
  chomp($was = $_);
  print STDERR "$ARGV($.): $was : $_" if s/abc/def/;
  close ARGV if eof' --

The grep on macos has an API based on some old version of GNU grep (as it used to have GNU grep or at least FreeBSD on which it is based used to) so you'll find some GNU options in there including that --null (though not the -Z short variant). Its xargs also has GNU's -0 option but unfortunately not the -r option (yet), so you'll get an error by perl if find doesn't find any file.

If you don't want to modify the files in place and just print the matches and how they would be substituted, then that becomes:

find . -name '*.myfile' -type f -exec perl -nle '
  $was = $_;
  print "$ARGV($.): $was : $_" if s/abc/def/;
  close ARGV if eof' -- {} +

No -inplace, -n to not print the line at the end of each cycle; -l does the chomp on $_ automatically (and print is told to add a \n on output). We print on stdout instead of stderr (redirect the whole command to stderr with >&2 is you want it to go to stderr instead; at the prompt of an interactive shell in a terminal, both stdout and stderr should go to the terminal so it shouldn't make a difference).

3
  • This is excellent. Thank you so much. One last question, if I want to make this non-recursive, what do I do? I assumed the "{} +" is the recursive notation, and I removed it, but I got an error.
    – Susie
    Commented Nov 30, 2023 at 17:20
  • @Susie, no it's not. Rather than assuming, you could read the manual of your find implementation. To stop find from descending into a directory you need to call -prune on that director. So find . ! -name . -prune -o -name '*.myfile' -type f -exec ..... Some find implementations also support some -mindepth and -maxdepth predicates. Here is you want the files at depth 1 (the children of the current directory) , that would be -mindepth 1 -maxdepth 1. Commented Nov 30, 2023 at 19:18
  • Got it. Much appreciated. (I've been reading the manual for perl, still deciphering everything, thank you). :)
    – Susie
    Commented Nov 30, 2023 at 22:48

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .