6

I have a multi-line string, downloaded from the Web:

toast the lemonade
blend with the lemonade
add one tablespoon of the lemonade
grill the spring onions
add the lemonade
add the raisins to the saucepan
rinse the horseradish sauce

I have assigned this to $INPUT, like this:

INPUT=$(lynx --dump 'http://example.net/recipes' \
     | python -m json.tool \
     | awk '/steps/,/]/' \
     | egrep -v "steps|]" \
     | sed 's/[",]\|^ *//g; $d')

At this point, $INPUT is ready for substitution into my target file as follows:

sed -i "0,/OLDINPUT/s//$INPUT/" /home/test_file

Of course, sed complains about an unterminated s command - herein lies the problem.

The current workaround I am using is to echo $INPUT prior to giving it to sed, but then the newlines are not preserved. echo strips newlines - which is the problem.

The correct output should maintain its newlines. How can sed be instructed to preserve the newlines?

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
Earl Tsui
  • 61
  • 1
  • 4
  • 1
    `unterminated 's'` almost always means that there is a '/' char in your "data". Just use a different char to delimit `s@string@repl@[g]`. Posix `sed` expects `\@string@repl@`. Note the leading back-slash char. Good luck. – shellter Feb 23 '15 at 00:28
  • @shelter how would that work for this? `sed -i "0,/OLDINPUT/s//$INPUT/" /home/test_file` Like so? `sed -i "0,@OLDINPUT/s@/$INPUT@" /home/test_file` – Earl Tsui Feb 23 '15 at 00:43
  • I believe you would need `\@matchInput@s@string@repl@` And of course your `$INPUT` can't have any `@` chars. If it does, use `~` or something else that is not in `$INPUT`.So any place you would use `/` now needs to be your new separator character. Good luck. – shellter Feb 23 '15 at 00:58
  • And after rereading your question, please clarify, is your goal to insert multiple lines of text (`$INPUT`), via `sed`? This requires use of `sed`'s `i` (insert) or `a` (append) command. You should get that working in a simpler context before you try and work with variables. (not recommended!) Sorry, but/and good luck! – shellter Feb 23 '15 at 01:04
  • @shelter, the input is line by line and that is what I am trying to replace/insert using sed substitution. The problem is it only works when I use echo which changes the input to be one line rather than line by line - it's the only way sed is working... – Earl Tsui Feb 23 '15 at 01:32
  • consider reducing your problem to a very small test case. Right now it is hard for us to be sure we're trying to solve the right problem. 1-2 lines of input, expected output from that input will help clarify a lot. Add that to your question using the `{}` format tool at the topleft of the edit box. Good luck. – shellter Feb 23 '15 at 02:02
  • Saying the solution needs to be sed since you are using `-i` is like choosing your new car based on you owning a Toyota gas cap. How to write back to the original file is simply not a problem for any solution. – Ed Morton Feb 23 '15 at 18:07
  • Possible duplicate of [sed replace with variable with multiple lines](http://stackoverflow.com/questions/6684487/sed-replace-with-variable-with-multiple-lines) – Toby Speight Sep 01 '16 at 10:48
  • "*`echo` strips newlines*" is a mistaken belief. When you write `$INPUT` without quotes, your **shell** breaks it into works at whitespace, and then `echo` outputs spaces between its arguments. The combined effect is to convert all whitespace to single spaces. (Pedants can substitute `${IFS-$' \t\n'}` for "whitespace" and `${IFS::1}` for "space" as appropriate) – Toby Speight Mar 27 '17 at 08:26

5 Answers5

4

The hacky direct answer is to replace all newlines with \n, which you can do by adding

| sed ':a $!{N; ba}; s/\n/\\n/g'

to the long command above. A better answer, because substituting shell variables into code is always a bad idea and with sed you wouldn't have a choice, is to use awk instead:

awk -i inplace -v input="$INPUT" 'NR == 1, /OLDINPUT/ { sub(/OLDINPUT/, input) } 1' /home/test_file

This requires GNU awk 4.1.0 or later for the -i inplace.

Wintermute
  • 42,983
  • 5
  • 77
  • 80
  • Hi, thanks! I tried to append: `| sed ':a $!{N; ba}; s/\n/\\n/g'` to the long command but still get: sed: -e expression #1, char 30: unterminated `s' command (tried again) sed: -e expression #1, char 28: unterminated `s' command – Earl Tsui Feb 23 '15 at 00:14
  • here is a sample of the command with your addition: (note that it stills fails the sed part) ...`[root@test]# lynx --dump 'http://somesite.net/recipe/' | python -m json.tool | awk '/steps/,/]/' | egrep -v "steps|]" | sed 's/"//g' |sed 's/,//g' | sed 's/^ */ /g' | sed '$d' | sed ':a $!{N; ba}; s/\n/\\n/g'` sift the coffee \nbarbeque the cucumbers \nbring the cucumbers to the boil \ngrill the cod liver oil \nbring the cucumbers to the boil \nsaute the pickle \nadd one tablespoon of the pickle – Earl Tsui Feb 23 '15 at 00:35
  • Works for me. Hmm...you don't happen to use Mac OS X or *BSD, do you? – Wintermute Feb 23 '15 at 09:12
3

If you're using Bash, you can substitute \n for the newlines:

INPUT="${INPUT//
/\\n}"

If you don't like the literal linefeed in your parameter expansion, you might prefer

INPUT="${INPUT//$'\n'/\\n}"

Side note - you probably mean to change the matched lines to your input, not substitute each of them. In which case, you don't want to quote the newlines, after all...

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
2

To clean up your code some.

This:

lynx --dump 'http://somesite.net/recipes' | python -m json.tool | awk '/steps/,/]/' | egrep -v "steps|]" | sed 's/"//g' |sed 's/,//g' | sed 's/^ *//g' | sed '$d'

Can be replaced with this:

lynx --dump 'http://somesite.net/recipes' | python -m json.tool | awk '/]/ {f=0} f {if (c--) print line} /steps/{f=1} {gsub(/[",]|^ */,"");line=$0}'

It may be shorten more, but I do not now what this does: python -m json.tool

This:

awk '/]/ {f=0} f {if (c--) print line} /steps/{f=1} {gsub(/[",]|^ */,"");line=$0}'

Does:

  1. Print line after pattern steps to line before ] - awk '/steps/,/]/' | egrep -v "steps|]"
  2. Removes ", , and all space in front of all lines. - sed 's/"//g' |sed 's/,//g' | sed 's/^ *//g'
  3. Then remove last line of this group. - sed '$d'

Example:

cat file
my data
steps data
 more
 do not delet this
hei "you" , more data
extra line
here is end ]
this is good

awk '/]/ {f=0} f {if (c--) print line} /steps/{f=1} {gsub(/[",]|^ */,"");line=$0}' file
more
do not delet this
hei you  more data
Jotne
  • 40,548
  • 12
  • 51
  • 55
  • `python -m json.tool` basically just pretty-prints JSON. See e.g. http://stackoverflow.com/questions/352098/how-can-i-pretty-print-json for a sample. – tripleee Feb 23 '15 at 12:04
  • 1
    All good advice, but this makes no attempt to answer the question - how to substitute a multi-line string using `s///`. – Toby Speight Sep 01 '16 at 10:14
2

You'll want to use an editor instead of sed's substitution:

$ input="toast the lemonade
blend with the lemonade
add one tablespoon of the lemonade
grill the spring onions
add the lemonade
add the raisins to the saucepan
rinse the horseradish sauce"

$ seq 10 > file

$ ed file <<END
1,/5/d
1i
$input
.
w
q
END

$ cat file
toast the lemonade
blend with the lemonade
add one tablespoon of the lemonade
grill the spring onions
add the lemonade
add the raisins to the saucepan
rinse the horseradish sauce
6
7
8
9
10
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • No, the problem is that `sed "s/string/$INPUT/"` breaks on the newlines. You'd have to backslash all the newlines in `$INPUT` to make this palatable to `sed`. – tripleee Feb 23 '15 at 12:18
  • Ah, I see. `ed` to the rescue – glenn jackman Feb 23 '15 at 14:59
  • @glennjackman Thanks a lot for your input. You definitely hit the nail on the head. The only thing with your solution is that I need to programatically handle this prior to passing it to sed. This is all being ran from a shell script... – Earl Tsui Feb 23 '15 at 17:47
  • 1
    `ed` can easily be scripted, as I demonstrated: pass the required commands to ed on stdin. – glenn jackman Feb 23 '15 at 18:27
2

Assuming your input JSON fragment looks something like this:

{ "other": "random stuff",
  "steps": [
    "toast the lemonade",
    "blend with the lemonade",
    "add one tablespoon of the lemonade",
    "grill the spring onions",
    "add the lemonade",
    "add the raisins to the saucepan",
    "rinse the horseradish sauce"
  ],
  "still": "yet more stuff" }

you can extract just the steps member with

jq -r .steps

To interpolate that into a sed statement, you'd need to escape any regex metacharacters in the result. A less intimidating and hopefully slightly less hacky solution would be to read static text from standard input:

lynx ... | jq ... |
sed -i -e '/OLDINPUT/{s///; r /dev/stdin' -e '}' /home/test_file

The struggle to educate practitioners to use structure-aware tools for structured data has reached epic heights and continues unabated. Before you decide to use the quick and dirty approach, at least make sure you understand the dangers (technical and mental).

Community
  • 1
  • 1
tripleee
  • 175,061
  • 34
  • 275
  • 318