It would always bother me that the venerable sed can’t work recursively on a directory. Something common I like to do is “replace all occurrences of x with y in all files under some directory d.”

So the workaround is usually 1. get the files I want and then 2. pipe those back to sed, which is a bit tedious. Ideally I should be able to do something like sed -i 's/this/that/g' my_dir.

The script

Here is an attempt at that, in script form. First parse the arguments - store the directory, the last arg, in dir and the rest in sedargs:

args=("$@")
dir="${args: -1}"
sedargs="${args[@]:0:${#args[@]}-1}"

Then, save the sed command into sedstr while handling the -i option for “in place replacement”.

first="${args[0]}"
if [[ $first = "-i" ]]; then
  sedstr="${args[@]:1:${#args[@]}-2}";
else
  sedstr="${args[@]:0:${#args[@]}-1}";
fi

(Note that the -i option is actually a GNU extension and is not POSIX standard.)

Next, get the string to be searched and replaced. Assume the backslash (“/”) delimiter:

IFS='/' read -r -a search_arr <<< "$sedstr"
to_search="${search_arr[1]}"

Lastly, get all files containing to_search and pipe them to the final sed invocation. Here I am using ripgrep, which is essentially acting as a single replacement for the classic find/grep duo.

rg_res=$(rg -l $to_search)
sed ${sedargs[@]} $rg_res

And now to test, save everything into a script (I’m calling it rsed for “recursive sed”), and voila:

$ tree temp
temp
├── dir1
│   └── file2
└── file1

2 directories, 2 files
$ echo "hello, son" > temp/file1
$ echo "how are you, son" > temp/dir1/file2
$ rsed -i 's/son/child/g' temp
$ cat temp/file1
hello, child
$ cat temp/dir1/file2
how are you, child

The non-script alternative is to do something like this:

$ rg -l "child" ./temp | xargs sed -i 's/child/man/g'

which also works, but it is always nice to type fewer characters.

Closing thoughts

Though this may seem like a lot of work for a trivial task, the omission of excessive features unrelated to the core task of stream-editing a file might actually be a good thing, as it adheres to the Unix philosophy of “do one thing well.” I’d be pretty annoyed if I was maintaining some project and users kept asking for bells and whistles that can easily be achieved elsewhere.

To that point, here is a complaint from Brian Kernighan from Unix: A History and a Memoir that captures this sentiment pretty well:

These are maxims to program by, though not always observed. One example: the cat command that I mentioned in Chapter 3. That command did one thing, copy input files or the standard input to the standard output. Today the GNU version of cat has (and I am not making this up) 12 options, for tasks like numbering lines, displaying non-printing characters, and removing duplicate blank lines. All of those are easily handled with existing programs; none has anything to do with the core task of copying bytes, and it seems counter-productive to complicate a fundamental tool.