This is the most portable way I can think to do this, though it still relies on the mostly portable /dev/fd/0 for .dot. Without it though, you could use a single file. In any case, it mostly relies on this shell function I wrote the other day:
_sed_cesc_qt() {
sed -n ':n;\|^'"$1"'|!{H;$!{n;bn}};{$l;x;l}' |
sed -n '\|^'"$1"'|{:n;\|[$]$|!{
N;s|.\n||;bn};s|||
\|\([^\\]\)\\\([0-9]\)|{
s||\1\\0\2|g;}'"
s|'"'|&"&"&|g;'"s|.*|'&'|p}"
}
First I'll show it work, then I'll explain how. So, I'll create a test file base:
printf 'f=%d
echo "$f" >./"$f"
echo "$f" >./"$f\n$f"
echo "$f" >./"$f\n$f\n$f"
' $(seq 10) | . /dev/fd/0
That creates a bunch of files, each named for the number 1-10 that it contains:
ls -qm
1, 1?1, 1?1?1, 10, 10?10, 10?10?10, 2, 2?2, 2?2?2, 3, 3?3, 3?3?3, 4, 4?4, 4?4?4, 5, 5?5, 5?5?5, 6, 6?6, 6?6?6, 7,
7?7, 7?7?7, 8, 8?8, 8?8?8, 9, 9?9, 9?9?9
That's a comma-delimited list of the files in my test directory, each ? representing a newline.
cat ./1*
1
1
1
10
10
10
Each file contains only a single number.
Now I'll do the grep replace:
find ././ \! -type d -exec \
grep -l '[02468]$' \{\} + |
_sed_cesc_qt '\./\./' |
sed 's|.|\\&|g' |
xargs printf 'f=%b
sed "/[02468]\\$/s//CHANGED/" <<-SED >"$f"
$(cat <"$f")
SED\n' |
. /dev/fd/0
Now when I...
cat ./1*
1
1
1
1CHANGED
1CHANGED
1CHANGED
All of the [2468] files are similarly CHANGED. It works recursively as well. Ok, so now I'll explain how.
First, I guess, the function:
- start at
:next label
\|address| argument $1 - a marker
- if current line is
!not a match {
- append it to
Hold buffer
- if current line is
!not $last line {
- overwrite current line with
next line
branch back to :next label
}}
- else if current line is
$last line look at pattern space
- else e
xchange contents of hold and pattern buffers and...
look unequivocally at pattern space
That's the first sed statement - and it's pretty much the meat and potatoes of it. We never print the pattern space at all - we only look at it. This is how POSIX defines the l function:
[2addr] l (The letter ell.) Write the pattern space to standard output
in a visually unambiguous form. The characters listed in the Base
Definitions volume of IEEE Std 1003.1-2001, Table 5-1, Escape
Sequences and Associated Actions ( '\\', '\a', '\b', '\f', '\r', '\t', '\v' ) shall be written as the corresponding escape sequence; the '\n'
in that table is not applicable. Non-printable characters not in that
table shall be written as one three-digit octal number (with a
preceding \backslash) for each byte in the character (most significant
byte first). Long lines shall be folded, with the point of folding
indicated by writing a \backslash followed by a \newline; the length
at which folding occurs is unspecified, but should be appropriate for
the output device. The end of each line shall be marked with a '$'.
So if I do:
printf '\e%s10\n10\n10' '\' | sed -n 'N;N;l'
I get:
\033\\10\n10\n10$
That's almost perfectly escaped for printf. It needs only an extra zero for the octal and to remove the trailing $ - so the next sed statement cleans it up.
I'm not going to do the same level of detail, but basically the next sed statement:
- If line begins with
$1 marker...
- Pulls in the
Next line until the current line ends in $
- If it had to do the above, it removes the trailing
\backslash and \newline character.
- Then it removes the trailing
$
- finds any
\backslashes followed by a number that are not preceded by another \backslash and inserts a zero
- Searches out any
'single quotes and double-quotes them
- Finally it surrounds the entire string with
'single-quotes
So now, when I do:
printf %s\\n ././1* |
_sed_cesc_qt '\./\./'
I get:
'././1'
'././1\n1'
'././1\n1\n1'
'././10'
'././10\n10'
'././10\n10\n10'
The rest is kind of easy. It depends on the fact that the ././ string will resolve, but it will only occur in find/grep's output at the head of every path name - so it becomes my $1 marker.
I -exec grep from find and specify -l for it to output filenames for those files that contain the regex.
I call the function and get its output.
I then \backslash escape every character in its output for xargs.
And with printf I write a script to the |pipe file - which I .dot source as /dev/fd/0. I define the f variable as its current argument - my pathname - and cat that $f argument to a <<heredocument, which is fed to sed, and sed writes back over the source file.
This may involve temporary files - that depends on your shell. bash and zsh will write out a temporary file for every heredocument - but they clean them up, too. dash, on the other hand, will just write the heredocument to an anonymous |pipe.
The important thing about it though is that the file will have to be fully read before its written over - it's just how heredocuments and command substitution work.