247

I need to add the following line to the end of a config file:

include "/configs/projectname.conf"

to a file called lighttpd.conf

I am looking into using sed to do this, but I can't work out how.

How would I only insert it if the line doesn't already exist?

nbro
  • 15,395
  • 32
  • 113
  • 196
Benjamin Dell
  • 2,841
  • 3
  • 18
  • 12
  • If you are trying to edit an ini-file the tool [`crudini`](https://github.com/pixelb/crudini) might be a good option (but not for lighthttpd yet) – rubo77 Dec 25 '19 at 02:20
  • Perhaps see also [sed,awk.grep - add a line to the end of a configuration section if the line doesn't already exist](https://stackoverflow.com/questions/16102213/sed-awk-grep-add-a-line-to-the-end-of-a-configuration-section-if-the-line-does) – tripleee May 10 '21 at 07:49

25 Answers25

459

Just keep it simple :)

grep + echo should suffice:

grep -qxF 'include "/configs/projectname.conf"' foo.bar || echo 'include "/configs/projectname.conf"' >> foo.bar

Edit: incorporated @cerin and @thijs-wouters suggestions.

anatoly techtonik
  • 19,847
  • 9
  • 124
  • 140
drAlberT
  • 22,059
  • 5
  • 34
  • 40
  • 1
    This one does the reverse of what I need. It only adds the line to the file IF the line already exists. I only want to add it if it doesnt exist. Any ideas? – Benjamin Dell Aug 24 '10 at 15:56
  • 1
    sorry forgotten a "-v" optiont to grep, or substitute && with || :) I'm going to correct, thanks – drAlberT Aug 24 '10 at 16:27
  • 2
    I would add the -q switch to grep to suppress output: `grep -vq ...` – Dave Kirby Aug 24 '10 at 16:33
  • 2
    FYI, using -v and && doesn't look to do the trick, whereas || without -v works. – bPizzi Aug 25 '10 at 08:21
  • 1
    Thanks guys, but this command still continues to add a brand new line if that line already exists. It should only write one if it doesnt exist. For testing i have been running: grep -vq 'include "/configs/projectname.conf"' text.txt || echo 'include "/configs/projectname.conf"' >> text.txt – Benjamin Dell Aug 25 '10 at 13:14
  • OK i've only managed to get it work by doing this: grep 'include "/configs/projectname.conf"' text.txt || echo 'include "/configs/projectname.conf"' >> text.txt – Benjamin Dell Aug 25 '10 at 13:20
  • 7
    This only works for very simple lines. Otherwise, grep interprets most non-alpha characters in your line as patterns, causing it to not detect the line in the file. – Cerin Jul 10 '14 at 20:34
  • 2
    Note, using `-F` seems to fix my issue. – Cerin Jul 10 '14 at 20:46
  • The way was indicated .. the answer was for a specific question :) Of course anybody can/has to adjust to his needs ... maybe -F gives a bit more robustness too .. Tnx – drAlberT Jul 11 '14 at 10:12
  • 2
    Beautiful solution. This also works for triggering more complicated expressions, of course. Mine uses the `echo` to trigger a `cat` of a multiline heredoc into a config file. – Eric L. Jun 15 '15 at 16:10
  • Doesn't work when sudo is required? Prepending `sudo` to the `grep` and `echo` commands doesn't work.. – Kapé Jan 18 '17 at 20:32
  • 2
    Add `-s` to ignore errors when the file does not exist, creating a new file with just that line. – Frank Mar 21 '18 at 18:09
  • 3
    You need to add an -x option to ensure only the entire line matches, instead of only part of a line. – Thijs Wouters Dec 30 '18 at 11:09
  • Would be better if can add a example to show appending multiple lines to a file if it does not exist . – Mithril Aug 16 '19 at 07:19
  • 5
    The text to be appended appears in the expression twice. How about prepending the command with `ADDTEXT='include "/configs/projectname.conf"'` and then using `$ADDTEXT`? The same goes for the file name `foo.bar`. – Jean Spector Nov 06 '19 at 17:09
  • How about appending multiple lines only if they don't exist – hunter_tech Mar 25 '20 at 07:35
  • `grep` + `echo` is usually more simply done with `awk` – SenhorLucas Feb 19 '21 at 13:05
  • With the -x option it doesn't work for me, it just adds it over and over `grep -qxF '/src/styles/_js_variables.scss' .gitignore || echo "/src/styles/_js_variables.scss" >> .gitignore` But works without the -x, I think with the -x it interprets it as a regex and defeats the -F flag – Tofandel Feb 01 '22 at 11:35
  • @Tofandel what OS and grep version are you using? – drAlberT Feb 03 '22 at 08:54
  • 1
    @JeanSpector agreed, like `s='include "/configs/projectname.conf"'; f=foo.bar; grep -qxF "$s" "$f" || echo "$s" >> "$f"` – mykhal Feb 13 '23 at 10:31
107

This would be a clean, readable and reusable solution using grep and echo to add a line to a file only if it doesn't already exist:

LINE='include "/configs/projectname.conf"'
FILE='lighttpd.conf'
grep -qF -- "$LINE" "$FILE" || echo "$LINE" >> "$FILE"

If you need to match the whole line use grep -xqF

Add -s to ignore errors when the file does not exist, creating a new file with just that line.

rubo77
  • 19,527
  • 31
  • 134
  • 226
  • 8
    If it's to a file where you need root permissions: `sudo grep -qF "$LINE" "$FILE" || echo "$LINE" | sudo tee -a "$FILE"` – nebffa Dec 08 '17 at 00:50
  • 1
    This actually doesn't work when the line starts with a `-`. – hyperknot Feb 06 '18 at 22:03
  • 4
    @zsero: good point! I added `--` in the grep command, so it will not interpret a $LINE starting with a dash as options any more. – rubo77 Feb 07 '18 at 01:05
104

Try this:

grep -q '^option' file && sed -i 's/^option.*/option=value/' file || echo 'option=value' >> file
kev
  • 155,172
  • 47
  • 273
  • 272
  • 8
    For an explanation see: [explainshell](https://explainshell.com/explain?cmd=grep+-q+%27%5Eoption%27+file+%26%26+sed+-i+%27s%2F%5Eoption.*%2Foption%3Dvalue%2F%27+file+%7C%7C+echo+%27option%3Dvalue%27+%3E%3E+file) – Darwyn Jan 19 '19 at 19:00
  • Marc Wäckerlin's answer is more comprehensive and does everything requested in one sed. Additionally, the use of && and || is ambiguous (can the || apply to grep and the sed, what happens if sed returns false? – Dev Null Mar 31 '21 at 19:18
  • I used this for a task at my work. Worked like a charm. Thanks for sharing – gjjhhh_ Apr 03 '23 at 22:25
43

Using sed, the simplest syntax:

sed \
    -e '/^\(option=\).*/{s//\1value/;:a;n;ba;q}' \
    -e '$aoption=value' filename

This would replace the parameter if it exists, else would add it to the bottom of the file.

Use the -i option if you want to edit the file in-place.


If you want to accept and keep white spaces, and in addition to remove the comment, if the line already exists, but is commented out, write:

sed -i \
    -e '/^#\?\(\s*option\s*=\s*\).*/{s//\1value/;:a;n;ba;q}' \
    -e '$aoption=value' filename

Please note that neither option nor value must contain a slash /, or you will have to escape it to \/.


To use bash-variables $option and $value, you could write:

sed -i \
    -e '/^#\?\(\s*'${option//\//\\/}'\s*=\s*\).*/{s//\1'${value//\//\\/}'/;:a;n;ba;q}' \
    -e '$a'${option//\//\\/}'='${value//\//\\/} filename

The bash expression ${option//\//\\/} quotes slashes, it replaces all / with \/.

Note: Just trapped into a problem. In bash you may quote "${option//\//\\/}", but in the sh of busybox, this does not work, so you should avoid the quotes, at least in non-bourne-shells.


All combined in a bash function:

# call option with parameters: $1=name $2=value $3=file
function option() {
    name=${1//\//\\/}
    value=${2//\//\\/}
    sed -i \
        -e '/^#\?\(\s*'"${name}"'\s*=\s*\).*/{s//\1'"${value}"'/;:a;n;ba;q}' \
        -e '$a'"${name}"'='"${value}" $3
}

Explanation:

  • /^\(option=\).*/: Match lines that start with option= and (.*) ignore everything after the =. The \(\) encloses the part we will reuse as \1later.
  • /^#?(\s*'"${option//////}"'\s*=\s*).*/: Ignore commented out code with # at the begin of line. \? means «optional». The comment will be removed, because it is outside of the copied part in \(\). \s* means «any number of white spaces» (space, tabulator). White spaces are copied, since they are within \(\), so you do not lose formatting.
  • /^\(option=\).*/{…}: If matches a line /…/, then execute the next command. Command to execute is not a single command, but a block {…}.
  • s//…/: Search and replace. Since the search term is empty //, it applies to the last match, which was /^\(option=\).*/.
  • s//\1value/: Replace the last match with everything in (…), referenced by \1and the textvalue`
  • :a;n;ba;q: Set label a, then read next line n, then branch b (or goto) back to label a, that means: read all lines up to the end of file, so after the first match, just fetch all following lines without further processing. Then q quit and therefore ignore everything else.
  • $aoption=value: At the end of file $, append a the text option=value

More information on sed and a command overview is on my blog:

Alexis Wilke
  • 19,179
  • 10
  • 84
  • 156
Marc Wäckerlin
  • 662
  • 6
  • 7
  • how this would be for files like "option value"? When I replace = for a space the function fix the option, but the rest of the file is lost :( – jlanza Aug 13 '18 at 14:36
  • @jlanza, I am very sorry, there was a typo in my code. The command is «ba», not «:ba». I edited the text and fixed it. Please try again. – Marc Wäckerlin Aug 15 '18 at 08:24
  • You could escape the `/` character by using another character in the substitution command (like `!`): `sed -i -e '/^\(option=\).*/{s!!\1value!;:a;n;ba;q}' -e '$aoption=value' filename` – jmlemetayer Feb 08 '19 at 15:54
  • Yes,you can use any character for the sed command. In fact I often use the comma: `sed 's,from,to,g'`, but to allow arbitrary name value pairs, e.g. arguments from a function call, you still need to escape them. – Marc Wäckerlin Feb 13 '19 at 09:04
  • 3
    It is worth mentioning that a file `filename` or `$3` must not be empty – antonbormotov Apr 05 '20 at 07:42
  • Hi, this helped me, but I have an issue, if I have `#option=value` at the first line of the file and `option=value` as the last line of the file, I ended up with two line with the same information. Is there anyway to handle this? – Juan Jun 21 '21 at 15:29
  • Also requires non empty file to exist – Edward Kigwana Dec 08 '21 at 17:22
  • Awesome answer. To make it perfect I would add `local -r` to vars to make it local and inmutable, like `local -r filepath="${3}"` – Francisco Puga Jun 20 '23 at 14:59
21

If writing to a protected file, @drAlberT and @rubo77 's answers might not work for you since one can't sudo >>. A similarly simple solution, then, would be to use tee --append (or, on MacOS, tee -a):

LINE='include "/configs/projectname.conf"'
FILE=lighttpd.conf
grep -qF "$LINE" "$FILE"  || echo "$LINE" | sudo tee --append "$FILE"
adlaika
  • 107
  • 1
  • 8
hamx0r
  • 4,081
  • 1
  • 33
  • 46
  • You can also use sed so you could insert this line where you want. `grep -qF "$LINE" "$FILE" || sudo sed -i "/line_where_you_want_to_insert_before/i $LINE" "$FILE"` – Juan Jul 23 '21 at 08:05
15

Here's a sed version:

sed -e '\|include "/configs/projectname.conf"|h; ${x;s/incl//;{g;t};a\' -e 'include "/configs/projectname.conf"' -e '}' file

If your string is in a variable:

string='include "/configs/projectname.conf"'
sed -e "\|$string|h; \${x;s|$string||;{g;t};a\\" -e "$string" -e "}" file
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • For a command that replaces a partial string with the complete/updated config file entry, or appends the line as needed: `sed -i -e '\|session.*pam_mkhomedir.so|h; ${x;s/mkhomedir//;{g;tF};a\' -e 'session\trequired\tpam_mkhomedir.so umask=0022' -e '};:F;s/.*mkhomedir.*/session\trequired\tpam_mkhomedir.so umask=0022/g;' /etc/pam.d/common-session` – bgStack15 May 31 '16 at 16:01
  • Nice, it helped me a lot +1. could you please, explain your sed expression ? – Rahul Jun 10 '16 at 10:39
  • I'd love to see an annotated explanation of this solution. I've used `sed` for years but mostly just the `s` command. The `hold` and `pattern` space swaps are beyond me. – nortally Jun 24 '16 at 15:50
12

If, one day, someone else have to deal with this code as "legacy code", then that person will be grateful if you write a less exoteric code, such as

grep -q -F 'include "/configs/projectname.conf"' lighttpd.conf
if [ $? -ne 0 ]; then
  echo 'include "/configs/projectname.conf"' >> lighttpd.conf
fi
Igor Milla
  • 2,767
  • 4
  • 36
  • 44
Marcelo Ventura
  • 581
  • 7
  • 19
  • 4
    Thanks for your comment Graham! Yes, it's normal shell syntax. And yes, any competent admin ought to understand it. However, it works as such based on a side effect of the expression evaluation algorithm within shell. So you can call it all you like, except straightforward -- which was my original point. – Marcelo Ventura Oct 17 '18 at 19:07
  • 1
    See also [Why is testing "$?" to see if a command succeeded or not, an anti-pattern?](https://stackoverflow.com/questions/36313216/why-is-testing-to-see-if-a-command-succeeded-or-not-an-anti-pattern) – tripleee Jul 18 '20 at 16:36
10

another sed solution is to always append it on the last line and delete a pre existing one.

sed -e '$a\' -e '<your-entry>' -e "/<your-entry-properly-escaped>/d"

"properly-escaped" means to put a regex that matches your entry, i.e. to escape all regex controls from your actual entry, i.e. to put a backslash in front of ^$/*?+().

this might fail on the last line of your file or if there's no dangling newline, I'm not sure, but that could be dealt with by some nifty branching...

Jean Spector
  • 916
  • 9
  • 9
Robin479
  • 1,606
  • 15
  • 16
  • 3
    Nice one-liner, short and easy to read. Works great for updating etc/hosts: `sed -i.bak -e '$a\' -e "$NEW_IP\t\t$HOST.domain\t$HOST" -e "/.*$HOST/d" /etc/hosts` – Noam Manos Apr 26 '18 at 16:38
  • A smaller version: `sed -i -e '//d; $a '` – Jérôme Pouiller Apr 24 '19 at 09:21
  • @Jezz I think this will fail, if the entry already is at the end of the file, because the `d` command will restart the sed programm and thereby skip the `a`ppend, if the delete happened to be on the last line. – Robin479 Apr 24 '19 at 10:05
  • 2
    @Robin479 Damned, `a`ppend have to be executed before `d`elete: `sed -i -e '$a' -e '//d'`. It is nearly the same than your original answer. – Jérôme Pouiller Apr 24 '19 at 20:55
8

Here is a one-liner sed which does the job inline. Note that it preserves the location of the variable and its indentation in the file when it exists. This is often important for the context, like when there are comments around or when the variable is in an indented block. Any solution based on "delete-then-append" paradigm fails badly at this.

   sed -i '/^[ \t]*option=/{h;s/=.*/=value/};${x;/^$/{s//option=value/;H};x}' test.conf

With a generic pair of variable/value you can write it this way:

   var=c
   val='12 34' # it handles spaces nicely btw
   sed -i '/^[ \t]*'"$var"'=/{h;s/=.*/='"$val"'/};${x;/^$/{s//c='"$val"'/;H};x}' test.conf

Finally, if you want also to keep inline comments, you can do it with a catch group. E.g. if test.conf contains the following:

a=123
# Here is "c":
  c=999 # with its own comment and indent
b=234
d=567

Then running this

var='c'
val='"yay"'
sed -i '/^[ \t]*'"$var"'=/{h;s/=[^#]*\(.*\)/='"$val"'\1/;s/'"$val"'#/'"$val"' #/};${x;/^$/{s//'"$var"'='"$val"'/;H};x}' test.conf

Produces that:

a=123
# Here is "c":
  c="yay" # with its own comment and indent
b=234
d=567
MoonCactus
  • 1,456
  • 1
  • 11
  • 9
6

As an awk-only one-liner:

awk -v s=option=value '/^option=/{$0=s;f=1} {a[++n]=$0} END{if(!f)a[++n]=s;for(i=1;i<=n;i++)print a[i]>ARGV[1]}' file

ARGV[1] is your input file. It is opened and written to in the for loop of theEND block. Opening file for output in the END block replaces the need for utilities like sponge or writing to a temporary file and then mving the temporary file to file.

The two assignments to array a[] accumulate all output lines into a. if(!f)a[++n]=s appends the new option=value if the main awk loop couldn't find option in file.

I have added some spaces (not many) for readability, but you really need just one space in the whole awk program, the space after print. If file includes # comments they will be preserved.

stepse
  • 341
  • 3
  • 6
4

Here's an awk implementation

/^option *=/ { 
  print "option=value"; # print this instead of the original line
  done=1;               # set a flag, that the line was found
  next                  # all done for this line
}
{print}                 # all other lines -> print them
END {                   # end of file
  if(done != 1)         # haven't found /option=/ -> add it at the end of output
    print "option=value"
}

Run it using

awk -f update.awk < /etc/fdm_monitor.conf > /etc/fdm_monitor.conf.tmp && \
   mv /etc/fdm_monitor.conf.tmp /etc/fdm_monitor.conf

or

awk -f update.awk < /etc/fdm_monitor.conf | sponge /etc/fdm_monitor.conf

EDIT: As a one-liner:

awk '/^option *=/ {print "option=value";d=1;next}{print}END{if(d!=1)print "option=value"}' /etc/fdm_monitor.conf | sponge /etc/fdm_monitor.conf
rzymek
  • 9,064
  • 2
  • 45
  • 59
3
 sed -i 's/^option.*/option=value/g' /etc/fdm_monitor.conf
 grep -q "option=value" /etc/fdm_monitor.conf || echo "option=value" >> /etc/fdm_monitor.conf
Lesmana
  • 25,663
  • 9
  • 82
  • 87
Jack
  • 5,801
  • 1
  • 15
  • 20
3

use awk

awk 'FNR==NR && /configs.*projectname\.conf/{f=1;next}f==0;END{ if(!f) { print "your line"}} ' file file
ghostdog74
  • 327,991
  • 56
  • 259
  • 343
2

here is an awk one-liner:

 awk -v s="option=value" '/^option/{f=1;$0=s}7;END{if(!f)print s}' file

this doesn't do in-place change on the file, you can however :

awk '...' file > tmpfile && mv tmpfile file
Kent
  • 189,393
  • 32
  • 233
  • 301
2

Using sed, you could say:

sed -e '/option=/{s/.*/option=value/;:a;n;:ba;q}' -e 'aoption=value' filename

This would replace the parameter if it exists, else would add it to the bottom of the file.


Use the -i option if you want to edit the file in-place:

sed -i -e '/option=/{s/.*/option=value/;:a;n;:ba;q}' -e 'aoption=value' filename
devnull
  • 118,548
  • 33
  • 236
  • 227
  • 8
    This doesn't seem to work at all. It adds "option=value" after every line. Not sure how it got two upvotes. You could do '$aoption=value' but I can't seem to make that conditional on the previous match so it just appends to the end regardless. – sosiouxme Sep 01 '14 at 01:44
2
sed -i '1 h
1 !H
$ {
   x
   s/^option.*/option=value/g
   t
   s/$/\
option=value/
   }' /etc/fdm_monitor.conf

Load all the file in buffer, at the end, change all occurence and if no change occur, add to the end

NeronLeVelu
  • 9,908
  • 1
  • 23
  • 43
1

The answers using grep are wrong. You need to add an -x option to match the entire line otherwise lines like #text to add will still match when looking to add exactly text to add.

So the correct solution is something like:

grep -qxF 'include "/configs/projectname.conf"' foo.bar || echo 'include "/configs/projectname.conf"' >> foo.bar
Thijs Wouters
  • 840
  • 1
  • 8
  • 16
1

Using sed: It will insert at the end of line. You can also pass in variables as usual of course.

grep -qxF "port=9033" $light.conf
if [ $? -ne 0 ]; then
  sed -i "$ a port=9033" $light.conf
else
    echo "port=9033 already added"
fi

Using oneliner sed

grep -qxF "port=9033" $lightconf || sed -i "$ a port=9033" $lightconf

Using echo may not work under root, but will work like this. But it will not let you automate things if you are looking to do it since it might ask for password.

I had a problem when I was trying to edit from the root for a particular user. Just adding the $username before was a fix for me.

grep -qxF "port=9033" light.conf
if [ $? -ne 0 ]; then
  sudo -u $user_name echo "port=9033" >> light.conf
else
    echo "already there"    
fi
Rakib Fiha
  • 473
  • 8
  • 13
  • Welcome to stackoverflow! Please read the question carefully before posting an answer - the OP asked how to insert using sed – Markoorn Feb 27 '19 at 06:17
1

Try:

LINE='include "/configs/projectname.conf"'
sed -n "\|$LINE|q;\$a $LINE" lighttpd.conf >> lighttpd.conf

Use the pipe as separator and quit if $LINE has been found. Otherwise, append $LINE at the end.

Since we only read the file in sed command, I suppose we have no clobber issue in general (it depends on your shell settings).

juj
  • 451
  • 3
  • 7
1

Almost all of the answers work but not in all scenarios or OS as per my experience. Only thing that worked on older systems and new and different flavours of OS is the following.
I needed to append KUBECONFIG path to bashrc file if it doesnt exist. So, what I did is

I assume that it exists and delete it.
with sed I append the string I want.

sed -i '/KUBECONFIG=/d' ~/.bashrc

echo 'export KUBECONFIG=/etc/rancher/rke2/rke2.yaml' >> ~/.bashrc
Gajendra D Ambi
  • 3,832
  • 26
  • 30
0

I elaborated on kev's grep/sed solution by setting variables in order to reduce duplication.

Set the variables in the first line (hint: $_option shall match everything on the line up until the value [including any seperator like = or :]).

_file="/etc/ssmtp/ssmtp.conf" _option="mailhub=" _value="my.domain.tld" \
        sh -c '\
            grep -q "^$_option" "$_file" \
            && sed -i "s/^$_option.*/$_option$_value/" "$_file" \
            || echo "$_option$_value" >> "$_file"\
        '

Mind that the sh -c '...' just has the effect of widening the scope of the variables without the need for an export. (See Setting an environment variable before a command in bash not working for second command in a pipe)

Community
  • 1
  • 1
Jonas Eberle
  • 2,835
  • 1
  • 15
  • 25
0

You can use this function to find and search config changes:

#!/bin/bash

#Find and Replace config values
find_and_replace_config () {
   file=$1
   var=$2
   new_value=$3
   awk -v var="$var" -v new_val="$new_value" 'BEGIN{FS=OFS="="}match($1, "^\\s*" var "\\s*") {$2=" " new_val}1' "$file" > output.tmp && sudo mv output.tmp $file
}

find_and_replace_config /etc/php5/apache2/php.ini max_execution_time 60
Guray Celik
  • 1,281
  • 1
  • 14
  • 13
0

If you want to run this command using a python script within a Linux terminal...

import os,sys
LINE = 'include '+ <insert_line_STRING>
FILE = <insert_file_path_STRING>                                
os.system('grep -qxF $"'+LINE+'" '+FILE+' || echo $"'+LINE+'" >> '+FILE)

The $ and double quotations had me in a jungle, but this worked. Thanks everyone

M1NDF1V3D
  • 1
  • 3
0

Using only sed I'd suggest the following solution:

sed -i \
    -e 's#^include "/configs/projectname.conf"#include "/configs/projectname.conf"#' \
    -e t \
    -e '$ainclude "/configs/projectname.conf"' lighttpd.conf

s replace the line include "/configs/projectname.conf with itself (using # as delimiter here)

t if the replacement was successful skip the rest of the commands

$a otherwise jump to the last line and append include "/configs/projectname.conf after it

Danny Lo
  • 1,553
  • 4
  • 26
  • 48
-2

I needed to edit a file with restricted write permissions so needed sudo. working from ghostdog74's answer and using a temp file:

awk 'FNR==NR && /configs.*projectname\.conf/{f=1;next}f==0;END{ if(!f) { print "your line"}} ' file > /tmp/file
sudo mv /tmp/file file
webdevguy
  • 975
  • 10
  • 17
  • This doesn't look correct, it will lose the other lines from the file when the config line is missing. – tripleee Mar 15 '17 at 14:13