1

I want to match a pattern in a file and replace it.

This command works with egrep, xargs and sed:

egrep -lRZ "hello" . | xargs -0 -l sed -i -e 's/hello/world/g'

The problem: It does not work on MacOS because the xargs of MacOS does not support the argumente -l.

xargs: illegal option -- l
usage: xargs [-0opt] [-E eofstr] [-I replstr [-R replacements]] [-J replstr]
             [-L number] [-n number [-x]] [-P maxprocs] [-s size]
             [utility [argument ...]]

How is this solvable on MacOS?

hardfork
  • 2,470
  • 1
  • 23
  • 43
  • 1
    I cannot believe that your command (as shown here) is working. You are missing a pipe `|` and half of the `s/search/replace/` expression for `sed`. – Socowi Feb 02 '19 at 21:14
  • that's right. I fixed it. – hardfork Feb 02 '19 at 21:22
  • If you [edit] your question to show us what that script is intended to do (i.e. provide a [mcve]) then we can help you write it sensibly using `find` to **find** files, for example, and other standard UNIX tools as appropriate. – Ed Morton Feb 03 '19 at 06:50

2 Answers2

2

There are actually three incompatibilities you're going to run into here between the GNU (Linux) vs. bsd (macOS) utilities.

  • The one you're getting an error message from is that bsd's xargs doesn't accept the -l option. But -l is equivalent to -L except that -L requires an argument specifying the maximum number of lines to pass per invocation of the command, while -l defaults to one if it isn't specified. Thus, you can just replace -l with -L1. -L is understood the same way by both the GNU and bsd versions of xargs, so using this is portable between Linux and macOS.

    But in this particular case, there's another even easier option: sed is perfectly capable of operating on multiple files per invocation, so there's no reason to limit it to one per invocation. This'll even be slightly faster, since it doesn't have to spend as much time launching new processes. So just leave -l off.

  • The GNU and bsd versions of egrep (and others in the grep family) both take the option -Z, but they use it to mean completely different things. With GNU, egrep -Z prints zero bytes (ASCII NUL characters) after each filename (matching what xargs -0 expects). But with bsd, egrep -Z is equivalent to zgrep -- it treats its input files as zip archives, and expands them before searching their contents.

    Fortunately, both versions understand --null to invoke zero-byte delimiters, so you can use that portably on both platforms.

  • Both the GNU and bsd versions understand -i<suffix> to mean "edit in place, but make a backup copy, and back up the original with the specified filename suffix". And for both of them, if the suffix is zero-length, it doesn't keep a backup. Unfortunately, the way you specify a zero-length suffix is different and (as far as I've been able to find) irreconcilably incompatible. Specifically, GNU requires the suffix to be directly attached to the -i (e.g. -i.bkp), so just specifying -i by itself is enough to specify in-place-without-backup mode. But bsd allows the suffix to be passed as a separate argument (e.g. -i .bkp), so if you just specify -i by itself, it'll use whatever the next argument is as a suffix (e.g. sed -i -e 's/hello/world/g' will use "-e" as a suffix). To specify in-place-without-backup mode, you need to follow -i with an explicit empty argument (e.g. sed -i '' -e 's/hello/world/g'). But if you do that with GNU's sed, it'll try to execute the empty argument as its script, which will fail.

With all that, here's the macOS version of your command:

egrep -lR --null "hello" . | xargs -0 sed -i '' -e 's/hello/world/g'

...which will almost work on Linux -- the only difference is that you need to remove the '' argument to sed. If you want something that's fully portable between Linux and macOS, you need to specify a backup suffix (and attach it directly to the -i option, as in -i.bkp).

Community
  • 1
  • 1
Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
2

The grep options to recursively search for files are best avoided - they just clutter up your grep args and make your scripts non-portable. There's already a perfectly good tool designed to find files with a very obvious name.

Are you just trying to replace hello with world in all your files? If so that's just

find . -type f |
while IFS= read -r file; do
    sed 's/hello/world/g' "$file" > "tmp$$" &&
    mv "tmp$$" "$file"
done

That'll work in any shell on any UNIX box unless your file names contain newlines. If you didn't want to change timestamps etc. on files that don't contain hello one way is:

find . -type f -exec grep -q 'hello' {} \; -print |
while IFS= read -r file; do
    sed 's/hello/world/g' "$file" > "tmp$$" &&
    mv "tmp$$" "$file"
done
Ed Morton
  • 188,023
  • 17
  • 78
  • 185
  • * note, to do a dry run first use `echo "tmp$$" "$file"` instead of `mv "tmp$$" "$file"`. – l'L'l Feb 03 '19 at 09:01
  • Thanks. I want to match not only raw text but regular expressions later, so it's `sed -e`, right? – hardfork Feb 03 '19 at 10:59
  • sed ONLY operates on regular expressions and `-e` is optional per POSIX given just one script to interpret. If by raw text you mean literal strings - you wouldn't use sed at all for that, you'd use awk since awk has functionality for literal strings. See https://stackoverflow.com/q/29613304/1745001 for the hoops you'd have to jump through to get sed to act as if it was operating on literal strings. – Ed Morton Feb 03 '19 at 13:14