29

So I've been using 'sed' on linux for a while, but have had a bit of difficulty trying to use it on OSX since 'POSIX sed' and 'GNU sed' have so many little differences. Currently I'm struggling with how to insert a line of text after a certain line number. (in this case, line 4)

On linux I would do something like this:

sed --in-place "4 a\  mode '0755'" file.txt

So on OSX I tried this:

sed -i "" "4 a\ mode '0755'" file.txt

However this keeps giving me a 'extra characters after \ at the end of a command' error. Any ideas what's wrong here? Do I have a typo? Or am I not understanding another difference between versions of sed?

CRThaze
  • 542
  • 2
  • 6
  • 13

6 Answers6

32

Strictly speaking, the POSIX specification for sed requires a newline after a\:

[1addr]a\
text

Write text to standard output as described previously.

This makes writing one-liners a bit of a pain, which is probably the reason for the following GNU extension to the a, i, and c commands:

As a GNU extension, if between the a and the newline there is other than a whitespace-\ sequence, then the text of this line, starting at the first non-whitespace character after the a, is taken as the first line of the text block. (This enables a simplification in scripting a one-line add.) This extension also works with the i and c commands.

Thus, to be portable with your sed syntax, you will need to include a newline after the a\ somehow. The easiest way is to just insert a quoted newline:

$ sed -e 'a\
> text'

(where $ and > are shell prompts). If you find that a pain, bash [1] has the $' ' quoting syntax for inserting C-style escapes, so just use

sed -e 'a\'$'\n''text'

[1] since version 2.0 (1996) and ksh93 (where it comes from), zsh (3.1.5+), mksh (r39b+) and some Almquist shell derivatives (e.g., /bin/sh in FreeBSD 9+)

jw013
  • 51,212
16

As described in this answer, it's helpful when using sed commands like i and a to use multiple -e "..." clauses. Each of these clauses will be understood to be separated by newlines. The i and a commands are hard to use in inline sed scripts otherwise (they're designed for use in a multiline sed script file invoked using sed -f file ...). It looks like you can't use the implicit newline introduced by the end of an -e clause to separate the a\ and the line of text that's to be appended. But you can use it to terminate the line of text that's to be appended.

In this specific case, what you're trying to do might in fact be accomplished with a single -e ... clause. You just have to use the a command correctly. By the POSIX standard, a needs to be followed by a \, then that needs to be followed by a newline, then the remainder of the next line will be inserted (until a newline or the end of the -e clause is encountered). So you could do:

sed -i "" -e $'4 a\\\n'"mode '0755'" file.txt
dubiousjim
  • 2,698
  • 2
    If you want to sort out what features you've been relying on are Gnu-isms, this page may help: http://wiki.alpinelinux.org/wiki/Regex. It's limited to just the regex features, though; not the other sed commands. – dubiousjim Oct 17 '12 at 21:40
7

Perl doesn't suffer from such platform dependent GNU vs. non-GNU vs. "proprietary" idiosyncrasies. You could do:

perl -ni.old -e 'print;if ($.==4) {print "mode 0755\n"}' file

The -n option creates a loop that reads every line of the input file. Unlike its cousin -p (not used here) it doesn't automatically print every line read. The -i invokes in-place replacement. The argument to -i (viz. ".old") can be dropped or changed. It leaves a backup of the unmodified file. The -e signals the beginning of the script.

The $. denotes the line-number, beginning with line-1. Thus the command line reads file and when the line-number equals 4, it prints that line followed by whatever you want to inject.

I would hasten to add that Perl owes its roots to sed, awk and C, so the syntax for simple substitutions, etc. isn't a very steep learning curve.

JRFerguson
  • 14,740
  • 1
    Nice answer! But you can simplify it a bit. Try this: perl -wlpi.old -e 'print "mode 0755" if $. == 5' file.txt. The -p switch works well here (since we want to print every line); we just have to change the logic from "printing after line 4" to "printing before line 5". And the -l switch makes it so that "\n" is printed automatically with every print, so you don't have to print it yourself. – J-L Jun 26 '19 at 22:35
6

GNU sed seems to be a lot more commonly used than POSIX sed, based on examples/tutorials I've used anyway.

I've found that it's much easier just to install GNU sed on any OSX machine I use. If you're interested, the best way to do this is install it through Homebrew.

You can either just $ brew install gnu-sed, or get all of the common GNU utilities with $ brew install coreutils.

Then when you run into a syntax issue with date or some other program you can just use the GNU version. Eventually I just decided it was easier to always use the GNU versions and put them earlier than the system versions in my PATH.

Glorfindel
  • 815
  • 2
  • 10
  • 19
Dean
  • 541
0

A simple demo:

$ date | sed -E $'1i\\\nfoo\n'
foo
Thu Aug  6 07:09:01 CDT 2020

The $'...' is a bashism to force escape sequence expansion. When using it, you must use \\ to get a literal \ in the string.

0

Using Raku (formerly known as Perl_6)

raku -pe 'put "mode 0755" if ++$ == 5'  

OR

raku -ne 'put "mode 0755" if ++$ == 5; .put;'   

These Raku solutions don't perform in-place replacement, nor do they automatically create backups. That you do yourself, generally with shell-redirection. Of note, for persons coming over from Perl(5), ++$ is Raku's anonymous state variable (scalar), prefix-incremented.

Sample Input:

1
2
3
4
5
6
7

Sample Output:

1
2
3
4
mode 0755
5
6
7

https://docs.raku.org/language/variables#index-entry-anon_state_variables
https://raku.org

jubilatious1
  • 3,195
  • 8
  • 17