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
:n
ext label
\|
address|
argument $1
- a marker
- if current line is
!
not a match {
- append it to
H
old buffer
- if current line is
!
not $
last line {
- overwrite current line with
n
ext line
b
ranch back to :n
ext label
}}
- else if current line is
$
last line l
ook at pattern space
- else e
x
change contents of hold and pattern buffers and...
l
ook unequivocally at pattern space
That's the first sed
statement - and it's pretty much the meat and potatoes of it. We never p
rint the pattern space at all - we only l
ook 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 \n
ewline; 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
N
ext line until the current line ends in $
- If it had to do the above, it removes the trailing
\
backslash and \n
ewline 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.