52

With a single input file that only contains comments (starting with #) and VARIABLE=value lines, is it possible to replace a value for a single variable if found and, otherwise, append the pair to the end of file if not found?

My current method works by deleting it in a first pass, then appending it to the end of the file in a second pass, but this method messes up the line ordering (and is also two different commands):

sed -r "/^FOOBAR=.*$/d"      -i samefile &&
sed -r "$ a\FOOBAR=newvalue" -i samefile

Is there anyway to do this, ie. keeping line order, in a single sed line? If some other utility (awk, ...) does this, I'ld take it over sed.

BlakBat
  • 1,279

11 Answers11

57

It's actually quite simple with sed: if a line matches just copy it to the hold space then substitute the value.
On the la$t line exchange hold space and pattern space then check if the latter is empty. If it's not empty, it means the substitution was already made so nothing to do. If it's empty, that means no match was found so replace the pattern space with the desired variable=value then append to the current line in the hold buffer. Finally, exchange again:

sed '/^FOOBAR=/{h;s/=.*/=newvalue/};${x;/^$/{s//FOOBAR=newvalue/;H};x}' infile

The above is gnu sed syntax. Portable:

sed '/^FOOBAR=/{
h
s/=.*/=newvalue/
}
${
x
/^$/{
s//FOOBAR=newvalue/
H
}
x
}' infile
don_crissti
  • 2,864
38

Here is a simpler sed approach, as I don't find sed hold space easy to work with. If you are comfortable with hold space, using don_crissti's approach gives additional opportunity to preserve anything from the existing line and its position as well.

In the below approach, you just print all but the line that you want to drop and then at the end, append the replacement.

sed -n -e '/^FOOBAR=/!p' -e '$aFOOBAR=newvalue' infile

The line gets moved to the end of the file as pointed out by @alek, so make sure that this is acceptable in your case.

haridsv
  • 699
21

This can probably be shortened. It's not a single sed command and it also uses grep, but this seems to be basically what you're wanting. It's a single line, and it edits the file in-place (no temp files).

grep -q "^FOOBAR=" file && sed "s/^FOOBAR=.*/FOOBAR=newvalue/" -i file || 
    sed "$ a\FOOBAR=newvalue" -i file
snapshoe
  • 1,186
4

Simply use grep and echo to create an empty record :

grep -q '^FOOBAR=' somefile || echo 'FOOBAR=VALUE' >> somefile
sed -i 's/FOOBAR=.*$/FOOBAR=VALUE/' somefile 

Each line escapes with error code zero.

2

Based on the other answers, if what you want to do is replace a variable's value if that variable is present in the file and append it to the end of the file if it is not (which is not what your posted sed commands do), you could try this:

perl -ne '$c=1 if s/^FOOBAR=.*$/FOOBAR=newvalue/;  
             print; 
             END{print "FOOBAR=newvalue" unless $c==1}' file > tmpfile && 
mv tmpfile file
terdon
  • 54,564
0

It's a bit easier in awk, although the "in place editing" is not automatic:

awk -v varname="FOOBAR" -v newval="newvalue" '
    BEGIN {FS = OFS = "="}
    $1 == varname {$2 = newval; found = 1}
    {print}
    END {if (! found) {print varname, newval}}
' file > tempfile &&
mv tempfile file
glenn jackman
  • 27,524
-1

for me I tried most of the above solutions and they didn't work using sed (GNU sed) 4.4! So I tried something similar to @snapshoe solution but a bit different:

envVar=FOOBAR
envVal=newvalue
file=samefile
grep -q "^${envVar}=" ${file} && sed -i -e "s/^${envVar}=.*/${envVar}=${envVal}/g" ${file} || echo "${envVar}=${envVal}" >> ${file}

This will replace FOOBAR if found or append to end of file if not found

one line: envVar=FOOBAR && envVal=newvalue && file=samefile && grep -q "^${envVar}=" ${file} && sed -i -e "s/^${envVar}=.*/${envVar}=${envVal}/g" ${file} || echo "${envVar}=${envVal}" >> ${file}

Waqleh
  • 109
-1

sed hold can handle one entry only. I enhanced glenn jackman's answer to handle multiple entries:

awk 'BEGIN {FS = OFS = "="}{idx=0;
  varname[++idx]="FOOBAR1";newval[idx]="val1";
  varname[++idx]="FOOBAR2";newval[idx]="val2";
  ...
}
{for (i in varname){if ($1 == varname[i]){sub($2".*",newval[i]);found[i]=1;}}print}
END {for (i in varname){if (!found[i]) print varname[i], newval[i]}
}' file > tempfile && cp tempfile file && rm tempfile
-1

As a programmer not accustomed to shell scripts it was easier to construct a more readable (for me) command by first deleting any existing match and then appending the value to the end of file

my-script.sh

CALCULATED_VALUE="some_end_result"

delete any old value and append the new one

sed -i '' '/^MY_ENV_VAR/d' .env || true echo "MY_ENV_VAR=${CALCULATED_VALUE}" >> .env

sed -i '' is because I'm on a macos, I think on ubuntu it'll be just sed -i


sed -i '' '/^MY_ENV_VAR/d' .env || true

Matches the line I want to update and deletes it if it exists. || true covers cases where a matching line does not exist in .env

echo "MY_ENV_VAR=${CALCULATED_VALUE}" >> .env

Adds the new line to the end of file

kidroca
  • 99
-2

Actually works with the following: sed -i '/^FOOBAR=/{h;s/=.*/=newvalue/};${x;/^$/{s//FOOBAR=newvalue/;H};x}' infile In the answer choosen -i is missing.

Yogesh Kamat
  • 327
  • 2
  • 2
-2

to do this with perl and in-place this works fine for me:

grep ^FOOBAR= my.file && perl -i -ple "s/^FOOBAR=.+/FOOBAR=newvalue/g" my.file || echo FOOBAR=newvalue >> my.file