2

I have some Tython functions that I want to insert in a file. Inserting multiple lines in itself works well using a variable and some \n, but the indentation isn't kept. Because it's Python code, that's a big issue, the code can't work as it is.

Here is what I tried:

cat sed-insertlines.sh

#!/bin/bash

read -r -d '' lines_to_insert << 'EOF'
def string_cleanup(x, notwanted):\n
    for item in notwanted:\n
        x = re.sub(item, '', x)\n
    return x\n
EOF

lines_to_insert=$(echo ${lines_to_insert} )

sed  -i "/import re  # Regular Expression library/a $lines_to_insert" sed-insertlines.txt

But here is what I get in the end when I cat sed-insertlines.txt:

#!/bin/python

import re  # Regular Expression library
def string_cleanup(x, notwanted):
 for item in notwanted:
 x = re.sub(item, '', x)
 return x


def string_replace(i_string, pattern, newpattern):
    string_corrected = re.sub(pattern, newpattern, i_string)
    return string_corrected

Lines are there but the indentation is gone.

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
Djidiouf
  • 786
  • 1
  • 10
  • 23

3 Answers3

8

First, let's get the data cleanly into a shell variable. Here's one way:

lines_to_insert=$(cat<<'EOF'
def string_cleanup(x, notwanted):
    for item in notwanted:
        x = re.sub(item, '', x)
    return x
EOF
)

Note that there are no \n added; you can just use the text you want to insert unmodified with the sole restriction that it can't contain a line consisting of exactly EOF (and if it does, you can change the here-doc delimiter.) Unfortunately, the later use of sed will modify the text by interpreting some backslash-sequences.

The correct syntax for the sed a command would be the following:

sed -i '/^import re/a \
def string_cleanup(x, notwanted):\
    for item in notwanted:\
        x = re.sub(item, '', x)\
    return x
'

(The commonly-seen sed 'a line to insert' is not Posix standard, and does not allow you to put leading spaces on the line. The correct syntax is as shown above; an a followed by whitespace, followed by a continuation marker and a newline.)

Note that every line except the last ends with a continuation marker (a trailing backslash). We could have put those in the text above, but that would defeat the goal of allowing you to use precisely the text you want inserted.

Instead, when we interpolate the shell variable into the sed command, we'll insert the backslashes using the global search-and-replace syntax:

# The following works with bash 4.3 and up
sed -i.bak "/^import re/a \
${lines_to_insert//$'\n'/$'\\\n'}
" sed-insertlines.txt

# Prior to v4.3, quoting worked differently in replacement
# patterns, and there was a bug with `$'...'` quoting. The
# following will work with all bashes I tested (starting with v3.2):
nl=$'\n' bsnl=$'\\\n'
sed -i.bak "/^import re/a \
${lines_to_insert//$nl/$bsnl}
" sed-insertlines.txt

Another solution is to use the mapfile command to read the lines into an array:

mapfile -t lines_to_insert <<'EOF'
def string_cleanup(x, notwanted):
    for item in notwanted:
        x = re.sub(item, '', x)
    return x
EOF

Now we can add the backslashes using printf:

sed -i.bak "/^import re/a \
$(printf '%s\\\n' "${lines_to_insert[@]}")
" sed-insertlines.txt

(The search-and-replace syntax would work on the array as well, but I think the printf command is more readable.)

Unfortunately, that adds an extra newline after the text because all of the lines in the original text were continued. If that's undesired, it could easily be removed in the second solution by inserting the backslash and newline at the beginning of the printf instead of the end, making a slightly less-readable command:

sed -i.bak "/^import re/a $(printf '\\\n%s' "${lines_to_insert[@]}")
" sed-insertlines.txt

Finally, based on a nice answer by Benjamin W, here's a version which uses the sed r command and process substitution (to avoid a temporary file):

sed '/^import re/r '<(cat<<'EOF'
def string_cleanup(x, notwanted):
    for item in notwanted:
        x = re.sub(item, '', x)
    return x
EOF
) sed-insertlines.txt
Community
  • 1
  • 1
rici
  • 234,347
  • 28
  • 237
  • 341
  • Sorry but the ````sed -i.bak "/^import re/a ${lines_to_insert//$'\n'/$'\\n\n'}" sed-insertlines.txt```` doesn't work. I ended with ````sed: -e expression #1, char 55: unknown command: `f'````. However, the solution with mapfile works like a charm. Thank you – Djidiouf May 16 '16 at 04:31
  • @Djidiouf: Sorry, copy-and-pasted the wrong command from my terminal; there's an extraneous `n` in the one I pasted. I fixed it. I like the mapfile one better, anyway. – rici May 16 '16 at 04:34
  • I copied your fix command and tried again but it's insert the text in one line only. code that I tried: http://pastebin.com/phfuite1 – Djidiouf May 16 '16 at 04:45
  • @Djidiouf: I just tried it in a clean terminal and it worked perfectly. You didn't remove the newlines from the sed command, did you? If you did that, you'd get a single line. – rici May 16 '16 at 04:48
  • @Djidiouf: Still works perfectly for me. I tried it with different shells, just in case. dash (sh) gives me a bad substitution error; ksh and bash work fine; zsh inserts one line. You've got a bash shebang line in the snippet, so it shouldn't use any other shell; I have no idea what might be going wrong. What version of bash do you have? – rici May 16 '16 at 05:01
  • Just to let you know that I tried on both ````GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu)```` and ````GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)```` and still don't have new lines inserted with the heredoc/sed solution – Djidiouf May 16 '16 at 05:16
  • @rici : Nice, is there a reason why you have preferred `cat` over `read`? – sjsam May 16 '16 at 05:37
  • 1
    @Djidiouf: OK, I'm pretty sure it's a bash bug, but I haven't found the patch yet. It works fine in 4.3.0 but fails in 4.2.11; it's something to do with the use of `$'\\'` in replacements. – rici May 16 '16 at 05:49
  • @sjsam: mostly a habit. cat is less fiddly. – rici May 16 '16 at 06:07
  • @rici : I see, mine too ;) – sjsam May 16 '16 at 06:13
  • @Djidiouf: It must have something to do with the change to quoting behaviour in replacements, which was implemented in between 4.3alpha and 4.3beta, I think. Anyway, I added a workaround (put the awkward strings into variables) which will work in all bash versions I tried it with. – rici May 16 '16 at 06:51
3

I would use the sed r command, which inserts the contents of a file after the current cycle:

#!/bin/bash

# Write code to be inserted into 'insertfile' with proper indentation
cat <<'EOF' > insertfile
def string_cleanup(x, notwanted):
    for item in notwanted:
        x = re.sub(item, '', x)
    return x
EOF

# Sed with r command
sed -i '/import re  # Regular Expression library/r insertfile' sed-insertlines.txt

# Remove temp file
rm -f insertfile

resulting in

import re  # Regular Expression library
def string_cleanup(x, notwanted):
    for item in notwanted:
        x = re.sub(item, '', x)
    return x


def string_replace(i_string, pattern, newpattern):
    string_corrected = re.sub(pattern, newpattern, i_string)
    return string_corrected
Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
1

Awk solution for this in case you're interested :

python_file:

#!/bin/python

import re  # Regular Expression library

def string_replace(i_string, pattern, newpattern):
    string_corrected = re.sub(pattern, newpattern, i_string)
    return string_corrected

Our Script

#!/bin/bash
read  -rd '' lines_to_insert << 'EOF'
def string_cleanup(x, notwanted):
    for item in notwanted:
        x = re.sub(item, '', x)
    return x
EOF
awk -v from_shell="$lines_to_insert" '
{
if ($0 ~ /import re  # Regular Expression library/){
printf "%s\n%s\n",$0,from_shell
}
else{
print $0
}
}' python_file

Output:

#!/bin/python

import re  # Regular Expression library
def string_cleanup(x, notwanted):
    for item in notwanted:
        x = re.sub(item, '', x)
    return x

def string_replace(i_string, pattern, newpattern):
    string_corrected = re.sub(pattern, newpattern, i_string)
    return string_corrected

Note :

I have removed the \ns from the $lines_to_insert.

sjsam
  • 21,411
  • 5
  • 55
  • 102