1

I have the following in my config.txt file.

some text
firmware_version 12.3
some more text ...

The sed -i 's/^firmware_version .*/firmware_version 12.4/' /config.txt command replaces the firmware version but what I'd like to be able to do is that even if sed doesn't find a line occurrence of "firmware_version .*" it still adds the new replacement.

In other words if my config.txt file includes the following:

some text
some more text ...

I'd still like the end result to be

some text
firmware_version 12.4
some more text ...

I just want to come up with a universal one line command for this.

Thanks!

sylvian
  • 709
  • 2
  • 9
  • 19
  • Should it detect 'some text' or 'some more text' as the match with no 'firmware_version' in between? Or can 'firmware_version' go at the end if the 'firmware_version' is not found? Spotting that the firmware version is missing between two lines is probably harder than adding it at the end, though neither is entirely straight-forward. Could there be other lines between 'some text' and 'some more text' than just 'firmware_version'? – Jonathan Leffler Apr 01 '14 at 20:46
  • 'firmware_version' can go to the end if it's not found, and the config file can be completely empty too, so there might be nothing in the config file and 'firmware_version' should still be added if that command is executed. – sylvian Apr 01 '14 at 20:49

3 Answers3

2

Thanks for your responses

I was really looking for an easier and simpler solution and I would expect sed to return a different exit status when the searched text doesn't match and then I could decide what to do next.

There are some discussions on how to make sed return a specific code if it doesn't match a criteria, but I couldn't make it work and the manual doesn't mention anything about it (or I may have missed something)

After a little more research I found not a very neat but an acceptable solution.

grep -q '^firmware_version .*' /config.txt && sed -i 's/^firmware_version .*/firmware_version 12.4/' /config.txt || echo 'firmware_version 12.4' >> /config.txt

So if grep finds a match then we run the sed command otherwise we echo the 'firmware_version 12.4' text to config.txt

Community
  • 1
  • 1
sylvian
  • 709
  • 2
  • 9
  • 19
0

For non-empty files, this works. It's a different model, though. If the 'firmware_version' appears between 'some text' and 'some more text' then delete it. When you come across 'some more text', insert the firmware version. It's tricky enough that I put the editing commands into a file named (unoriginally) sed.script:

/some text/,/some more text/{
/firmware_version .*/d
/some more text/i\
firmware_version 12.4
}

Then, given config-1.txt:

some text
firmware_version 12.3
some more text

and config-2.txt:

some text
some more text

you can run the script like:

sed -f sed.script config-1.txt

and it produces the same output for both inputs:

some text
firmware_version 12.4
some more text

If the input file is empty, I think it is best handled separately. This shell script fragment writes the new file to standard output:

if [ -s ${config_file} ]
then sed -f sed.script ${config_file}
else echo "firmware_version 12.4"
fi

If you want to overwrite the original file and your sed supports -i.bak, then:

if [ -s ${config_file} ]
then sed -i.bak -f sed.script       ${config_file}
else echo "firmware_version 12.4" > ${config_file}
fi
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
0

Regarding @sylvian's answer:

  • You don't need .* for grep since that pattern doesn't enforce anything and grep will return true if it finds a pattern anywhere on a line.
  • You can make your sed expression DRYer by capturing the key: sed -i 's/^(firmware_version) .*/\1 12.4/'
  • If you don't care about position, you can drop the grep entirely:
sed -i '/^firmware_version/d' config.txt; echo 'firmware_version 12.4' >> config.txt

But seriously, awk is much better at parsing fields and this is a key/value pair in columns:

touch config.txt
awk -i inplace \
    -v 'k=firmware_version' \
    -v 'v=12.4' \
    '$1 == k{f=1;$2=v} 1; ENDFILE {if(!f) print k, v}' config.txt

(note: -i inplace is not entirely portable, if it doesn't work for you, consider mktemp or sponge)

The touch will create an empty file if it doesn't exist (a problem that is not addressed by any of the answers so far).

The awk statement says:

  • If the first field is exactly "firmware_version", set the "found" variable (f) to true and set the second field to "12.4".
  • Print all lines (1) (including modified lines).
  • If at the end of the file, the match was never found, print a new line with "firmware_version 12.4"

This avoids regex entirely. Since we're looking at fields, we don't need to be concerned about anchors, we won't match words found in other fields and we won't accidentally match keys like "firmware_version_update".

It's concise and reusable (you can just change the variables k and v) and it avoids using 3 different commands with a true/false chain (which can lead to unexpected results).

This also works with empty files, preserves the placement of "firmware_version" (if it exists) and preserves any text after the new value such as a comment.

firmware_version 12.4      # This comment stays put!

See it in action: https://glot.io/snippets/gbrzh99b0p

Really tiny edge-case

Awk detects fields using a repeating regex space delimiter by default so it will change a line with leading whitespace that looks like this:

         firmware_version ...

If there's a comment marker like # before it, that won't matter. This only matters to lines with explicitly whitespace behind the first column.

If for some incredibly insane reason that means something different for your application, then you would have to use an anchor and pattern matching and $1 == k becomes
BEGIN {p="^"k} $0 ~ p.


I think @anubhava's answer (also using awk) is a misinterpretation of the problem since "some text" is likely an unknown placeholder (also, it does not handle empty files).

If leading/trailing lines in a block of text are known, then @johnathan-leffler's answer is better. Here's a slightly more legible script:

/some text/,/some more text/{
  /^firmware_version /d
  /some more text/ifirmware_version 12.4
}
user1718888
  • 311
  • 2
  • 7