TechX

sed: unescaped newline inside substitute pattern

A sed command copied from any Linux answer fails on macOS with an error that names the wrong problem. The cause is one missing argument, and the error text points at the file instead.

Tested on MacBook Pro (Mac16,8) · Apple M4 Pro · macOS 26.3.1 (25D771280a) · July 2026

The command is one you have run a hundred times on Linux. On macOS it fails and blames your file:

sed -i 's/a/b/' notes.txt
sed: 1: "notes.txt
": unescaped newline inside substitute pattern

There is no newline inside the substitute pattern, and notes.txt is not a pattern. Both halves of the message are misleading, and the actual fault is that -i on macOS requires an argument that was not supplied.

The fix is to give it an empty one, as two separate shell words:

sed -i '' 's/a/b/' notes.txt
2
Shell words n=1 Argument count required after -i on BSD sed for in-place editing with no backup

Why the error names the wrong thing

macOS ships BSD sed, not GNU sed. On BSD sed, -i takes a mandatory argument: the suffix for the backup file it creates before editing. Pass -i.bak and you get notes.txt.bak alongside the edited original.

When you write sed -i 's/a/b/' notes.txt, BSD sed reads the arguments in order:

  1. -i needs a suffix, so it takes the next word.
  2. The next word is s/a/b/. That becomes the backup suffix.
  3. sed now needs a script. The next word is notes.txt, so that becomes the script.
  4. There are no arguments left, so sed reads from standard input.

Step three is where the error comes from. sed is attempting to parse the literal text notes.txt as a sed program. It sees n, a valid command, then otes.txt which is not, and by the time it reaches the end of the line it reports the newline it hit while trying to complete a substitution it thought it was in the middle of.

-i takes the next word as a backup suffix suffix = 's/a/b/' sed still needs a script, takes the next word script = notes.txt Parsing notes.txt as a sed program unescaped newline inside substitute pattern refused
How BSD sed consumes the arguments you gave it. The error is reported at the last stage, three steps after the actual mistake.

The message is accurate about what sed was doing. It is simply describing a situation three steps downstream of the actual mistake.

The fix, and why the quotes matter

sed -i '' 's/a/b/' notes.txt

The '' is an empty backup suffix, meaning do not keep a backup. It has to be a separate word. These do not work:

sed -i'' 's/a/b/' notes.txt     # shell strips the quotes, back to bare -i
sed -i"" 's/a/b/' notes.txt     # same

In both cases the shell removes the empty quotes before sed ever sees the command line, so sed receives a bare -i again and fails identically. The space is doing real work.

Three ways to write this portably

If the script only ever runs on macOS, sed -i '' is fine. If it might run anywhere, you need one of these.

Backup suffix on both platforms

sed -i.bak 's/a/b/' notes.txt && rm -f notes.txt.bak

Attaching the suffix to the flag satisfies BSD sed, and GNU sed accepts the same form. You then delete the backup. It works everywhere and leaves a moment where a stray .bak file exists, which matters if something else is watching the directory.

Temporary file and move

sed 's/a/b/' notes.txt > notes.txt.tmp && mv notes.txt.tmp notes.txt

No -i at all, so no incompatibility. This is what I use in anything that has to survive contact with CI. The && matters: without it, a failing sed leaves you having already clobbered nothing, but a failing mv after a successful sed would be silent.

One caveat: this replaces the file rather than editing it, so it does not preserve the original inode, ownership or extended attributes. For ordinary text files that is irrelevant. For anything with a hard link or an ACL you care about, it is not.

Detect the platform

if sed --version >/dev/null 2>&1; then
  sed -i 's/a/b/' notes.txt          # GNU
else
  sed -i '' 's/a/b/' notes.txt       # BSD
fi

BSD sed has no --version flag and exits non-zero, which makes it a reliable probe. This is more code than the temporary file approach and buys nothing extra, so I would only reach for it when in-place editing specifically matters.

The same trap in other tools

BSD and GNU disagree in more places than sed, and every one of them produces a confusing error on macOS. On this machine:

date -d "2026-01-01"
# date: illegal option -- d

stat -c %s package.json
# stat: illegal option -- c

Both are GNU flags that BSD versions of the tools never had. date -d becomes date -j -f, and stat -c %s becomes stat -f %z. Neither error hints that a different implementation is involved, which is the recurring problem: the tool has the right name, so nothing signals that it is a different program.

If you want the GNU versions on your own machine, Homebrew’s coreutils installs them prefixed with g, giving you gsed, gdate and gstat. That fixes your machine and does nothing for anyone else running your script, so it is a convenience rather than a solution.

Confirming which sed you have

sed --version 2>/dev/null | head -1 || echo "BSD sed (no --version flag)"

On macOS with the stock tool that prints the fallback message. If it prints a GNU version banner, you are running something you installed, and the plain -i form will work.

A checklist for the whole class of problem

BSD and GNU divergence produces a recurring shape: a command that works on Linux fails on macOS with an error that describes a symptom rather than the incompatibility. Recognising the shape is worth more than memorising individual differences.

The tell is an error that makes no sense given what you typed. sed complaining about a newline in a pattern you did not write. date refusing a flag every example uses. stat rejecting a format string copied from documentation. In each case the tool exists, the name is right, and the implementation underneath is a different program.

The ones that come up most often:

Task GNU BSD (macOS)
In-place edit, no backup sed -i sed -i ''
Parse a date string date -d "2026-01-01" date -j -f "%Y-%m-%d" "2026-01-01"
File size in bytes stat -c %s file stat -f %z file
Follow symlinks fully readlink -f path readlink -f path (works on recent macOS)
Run with a time limit timeout 60 cmd not present, see below
Base64, no wrapping base64 -w0 base64 (no wrapping by default)

timeout is the harshest of these because it is absent entirely rather than different, so the failure is command not found and there is no flag to adjust.

Making a script survive both

Three approaches, in increasing order of effort.

Write to a temporary file and move it. This sidesteps -i entirely and works identically everywhere:

sed 's/a/b/' file > file.tmp && mv file.tmp file

Detect the implementation once at the top of the script and set a variable:

if sed --version >/dev/null 2>&1; then SED_INPLACE=(-i); else SED_INPLACE=(-i ''); fi
sed "${SED_INPLACE[@]}" 's/a/b/' file

BSD sed has no --version and exits non-zero, which makes the probe reliable. Note the array rather than a plain string: a variable holding -i '' would be word-split into -i and '' by the shell only under specific settings, and the array removes the ambiguity.

Or step outside the tool entirely. For anything beyond a simple substitution, perl -pe behaves identically on both platforms because it is the same perl:

perl -i -pe 's/a/b/' file

perl -i takes an optional suffix and does not require one, which is the behaviour people expect from sed -i and do not get. For scripts that already have several sed invocations, this is frequently the smallest total change.

Installing GNU tools, and what it does not solve

brew install coreutils gnu-sed

That gives you gsed, gdate, gstat and the rest, prefixed so they do not shadow the system versions. Adding the gnubin directory to PATH makes them the defaults, which is convenient and introduces a different problem: your scripts now work on your machine and fail on any Mac without the same setup, and the failure happens at someone else’s desk rather than yours.

For personal interactive use, install them. For anything you will hand to another person or run in CI, write it portably in the first place.

Takeaways

  • BSD sed requires an argument after -i. GNU sed does not accept one attached that way.
  • The error text mentions the substitute pattern because sed consumed your filename as the backup suffix and then read the script as the file.
  • sed -i "" is the macOS form, and it must be two separate shell words.
  • For scripts that must run on both platforms, a temporary file and mv avoids the incompatibility entirely.

Questions

Why does sed -i.bak work but sed -i does not?
Because -i.bak attaches the suffix to the flag, so BSD sed has its required argument. With a bare -i it takes the next word on the command line as the suffix, and that word is your script.
Will sed -i "" work on Linux too?
No. GNU sed reads the empty string as the script and then finds nothing to do, or errors. There is no single form that works on both, which is why the temporary file approach is the portable answer.
Should I just install GNU sed with Homebrew?
It helps on your own machine, where it installs as gsed. It does not help a script that has to run on a colleague's Mac or in CI, so the portable form is still worth knowing.

Sources

What we read. Distinct from what we measured, which is in the article itself.

About the author

Teja Pagidimarri

11+ years in content marketing and SEO, based in Hyderabad, India. Every measurement published on TechX was taken by hand on the hardware listed in how we test.