3

I am fairly new to regexes and it appears I am missing something in my understanding. I have the following file, and I am trying to insert a value if it is missing.

The file looks like

[composefs]
enabled = yes
[sysroot]
readonly = true

I would like to insert the following only if it is absent

[etc]
transient = true

My sed search and replace looks like

sed -zi '/^[etc]\n^transient = true/!s/$/[etc]\ntransient = true/' file

When I run sed it adds the value as expected, but it keeps adding the value un subsequent runs so I assume my matching is not working.

How can I do this correctly?

3 Answers 3

4

[etc] in regex means any one of these characters, so it won't match a literal [etc].

with sed:

sed -zi '/\[etc\]\ntransient *= *true/!s|\(.*\)\(.*\)|\1\2[etc]\ntransient = true\n|' file
  • '/\[etc\]\ntransient *= *true/!

    • checks whether the block [etc] followed by transient = true exists in the file.
  • transient *= *true

    • Matches the string transient = true, with optional spaces around the = sign (* allows for zero or more spaces).
  • !

    • This negates the match, so the subsequent replacement will only occur if the pattern is not found.
  • s|\(.*\)\(.*\)|\1\2[etc]\ntransient = true\n|

    • The substitution part, which operates on the content that doesn’t match the pattern from above.
  • \(.*\)

    • captures any characters (except newline) before the matched pattern.
  • \1\2

    • These are references to the parts of the input that were matched by the first two \(.*\) patterns:
  • [etc]\ntransient = true\n

    • Will be inserted if the pattern is not found.

With grep/printf:

if ! grep -qPzo '\[etc\]\s*\n\s*transient\s*=\s*true' file; then
  printf '%s\n%s\n' "[etc]" "transient = true" >> file
fi

Or with grep/ed:

if ! grep -qPzo '\[etc\]\s*\n\s*transient\s*=\s*true' file; then
  ed -s file <<'EOF'
$a
[etc]
transient = true
.
w
q
EOF
fi

grep will check for the pattern and if it is not found ed will run:

  • -s

    • tells ed to run in "silent" mode, suppressing most output.
  • $a

    • appends text after the last line in the file.
  • .

    • ed command to indicate the end of the text input.
  • w

    • write the changes to the file.
  • q

    • quit
2
  • 1
    Alternative grep pipeline to detect existing text: grep -xF -A 1 '[etc]' file | grep -q -xF 'transient = true' Commented May 10 at 13:54
  • 1
    Why s|\(.*\)\(.*\)|\1\2? Why not just s/$/[etc]\ntransient = true\n/, like OP had done? Also, . can match newlines even when not using -z. You can verify that with sed 'N;s/.*/<&>/' <<< $'foo\nbar' Commented May 10 at 20:31
3

Since you're using -z, ^ corresponds to the beginning of each optionally-null-terminated record (i.e. the beginning of the file, and after each null byte but the last if it's the last byte). That means that the first ^ won't catch [etc] when it's at the start of any other line but the first. You can use (\n|^) for that. For the second ^, ^ will never match after the beginning of the match, so \n^ will always fail. Finally, there's the point that jesse_b pointed out, that [etc] is regex for one character of those 3.

Those are the issues with the pattern, now the remaining issue is with the substitution, where you're lacking the trailing newline. sed would normally add it itself (more accurately, restore it from the input, since the last terminator is optional), but since you're using -z, the "separator"/terminator is set to a null byte rather than newline.

Once you fix those 4 issues (and added -E to avoid some escapes), you have:

sed -Ezi '/(\n|^)\[etc\]\ntransient = true/!s/$/[etc]\ntransient = true\n/' file
0
1

You could just do this with any version of awk so it'll work in any shell on every Unix box without needing to escape regexp metachars and without needing to repeat any parts of the string you're trying to match to be able to just print it as-is (count how many times the string transient appears in each potential solution, for example):

$ awk -v str='[etc]\ntransient = true' -v RS= '
    index("\n"$0"\n","\n"str"\n") { f=1 }
    { print }
    END { if (!f) print str }
' file
[composefs]
enabled = yes
[sysroot]
readonly = true
[etc]
transient = true

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.