Watch at EaseStories. Answers. Perspective.

Technology · PRACTICAL GUIDE

sed says “unterminated s command”? Prove the shell-data boundary before adding escapes

The diagnostic says the substitution program that reached sed has no usable closing boundary. Capture the actual argument and bytes, classify four failure families, then repair and test without in-place editing.

Diagram tracing shell source through the actual argument vector to the sed parser
Illustration: inspect the exact argument and bytes that reached sed before changing escapes. Watch at Ease

“Unterminated s command” is a parser report, not a complete diagnosis. The substitution program that reached sed no longer contains a usable closing delimiter in the place sed expected it. A separator may be missing in the source, data may contain the chosen separator, an expanded value may carry a carriage return or newline, or another layer may have consumed a quote or backslash first.

Start at the process boundary. Capture the exact argument passed to sed and inspect its bytes before changing escapes. Once the broken boundary is visible, repair only the owning layer, test the transformed text on a tiny fixture without in-place editing, and add platform-specific -i behavior only after the output diff is exact.

1. Capture what sed actually received

Reproduce with the smallest input and remove -i. Use shell tracing only as a clue, then print one shell-escaped argument per line where the shell supports it. A byte view such as od or xxd can reveal a delimiter, backslash, carriage return or newline hidden by ordinary terminal rendering.

Record the owning layers in order: interactive shell or script, parameter expansion, command substitution, YAML or CI template, SSH or container wrapper, then sed. Do not use eval to inspect or repair the command; it adds another parser and can change the evidence.

2. Classify the broken boundary

If the final sed argument never contained the third delimiter, fix the command syntax. If pattern or replacement data contains the delimiter, choose a delimiter absent from the data or escape that field for sed. If a value contains CR or newline bytes, decide whether multi-line data belongs in this command at all.

If the argument is correct locally but breaks in CI, YAML, SSH or command substitution, compare the bytes at each handoff. The first boundary where they differ owns the repair; adding backslashes later only hides the real owner.

3. Escape pattern and replacement separately

The regexp field and replacement field are different languages. Regex metacharacters belong to the pattern decision. In the replacement, an unescaped ampersand inserts the matched text and backslashes introduce replacement escapes. Quote for the shell first, then apply the minimum sed escaping required by that field.

For data-rich substitutions, pass values through a small field-specific encoder or generate a sed script file from a controlled template. Never concatenate untrusted text into executable shell or sed syntax when a fixed-format tool, structured parser or language library can make the boundary explicit.

4. Test a failure matrix, not one happy value

Use a tiny fixture and test an empty value, spaces, the selected delimiter, ampersand, backslash, carriage return and newline. Also test a value that does not match. Compare stdout with the exact expected file so a command that merely stops throwing is not mistaken for a correct transformation.

Repeat through the real wrapper that previously failed. A direct terminal success does not validate YAML quoting, an SSH command string, a container entrypoint or a build-system variable. Preserve the captured argument and expected output in the regression test.

5. Add in-place editing only after the diff is exact

Run the accepted expression against a copy before the original. Check the sed implementation because GNU, BSD and BusyBox forms of -i and backup suffixes differ. Keep a backup or atomic replacement path when the file matters.

The repair is complete only when the exact wrapper emits the reviewed bytes, the fixture diff is exact, edge cases stay literal, and the target copy changes once. If any owner or byte sequence is still unknown, stop at stdout and preserve the evidence instead of guessing.

Four-column matrix for classifying and repairing an unterminated sed substitution
Concept illustration: repair the owning failure class, then pass the no-in-place acceptance test. Watch at Ease