11

I'd like to edit a file in-place by appending a line, only if doesn't exist yet to make my script bullet-proof.

Normally I'd do something like:

cat >> ~/.bashrc <<EOF
export PATH=~/.composer/vendor/bin:\$PATH
EOF

It is also possible to do it via ansible (line+insertafter=EOF+regexp), but it's another story.

In vi/ex I could do something like:

ex +'$s@$@\rexport PATH=\~/.composer/vendor/bin:$PATH@' -cwq ~/.bashrc

but then how do I check if the line is already there (and thus do nothing) ideally without repeating the same line?

Or maybe there is some easier way to do it in Ex editor?

statox
  • 49,782
  • 19
  • 148
  • 225
kenorb
  • 18,433
  • 18
  • 72
  • 134

6 Answers6

7

If you want bulletproof, you probably want something a little more portable than the complicated options above. I would stick to POSIX features of ex and make it stupid-simple: Just remove the line if it's there (regardless of how many times it's there), add it to the end of the file, then save and exit:

ex -sc 'g/^export PATH=\~\/\.composer\/vendor\/bin:\$PATH$/d
$a
export PATH=~/.composer/vendor/bin:$PATH
.
x' ~/.bashrc

I anchored the regex for added robustness, though I think it's almost impossible to accidentally match this regex. (Anchored means using ^ and $ so the regex will only match if it matches an entire line.)

Note that the +cmd syntax is not required by POSIX, nor is the common feature of allowing multiple -c arguments.


There are a multitude of other simple ways to do this. For instance, add the line to the end of the file, then run the last two lines of the file through the external UNIX filter uniq:

ex -sc '$a
export PATH=~/.composer/vendor/bin:$PATH
.
$-,$!uniq
x' input

This is very very clean and simple, and is also fully POSIX-compliant. It uses the Escape feature of ex for external text filtering, and the POSIX-specified shell utility uniq

The bottom line is, ex is designed for in-place file editing and it is by far the most portable way to accomplish that in a non-interactive fashion. It predates even the original vi, actually—the name vi is for "visual editor", and the non-visual editor it was built upon was ex.)


For even greater simplicity (and to reduce it to a single line), use printf to send the commands to ex:

printf %s\\n '$a' 'export PATH=~/.composer/vendor/bin:$PATH' . '$-,$!uniq' x | ex input
Wildcard
  • 4,409
  • 26
  • 49
3

A possible solution is to create a function to do that:

function! AddLine()

    let l:res = 0
    g/export PATH=\~\/.composer\/vendor\/bin:\\\$PATH/let l:res = l:res+1

    if (l:res == 0)
        normal G
        normal oexport PATH=~/.composer/vendor/bin:\$PATH
    endif

endfunction

First we create a local variable l:res which will be used to count the number of occurrences of the line.

We use the global command: each time the line is matched, l:res is incremented.

Finally if l:res is still 0 it means that the line doesn't appear in the file so we go to the last line thanks to G, and we create a new line with o on which we write the line to add.

Note 1 The method I used to set the value of l:res is a little bit heavy and could be replaced by the one suggested by @Alex in his answer with something like:

let l:res = search('export PATH=\~\/.composer\/vendor\/bin:\\\$PATH')

Note 2 Also to make the function more flexible you can use arguments:

function! AddLine(line)

    let l:line =  escape(a:line, "/~$" )

    let l:res = 0
    exe "g/" . l:line . "/let l:res = l:res+1"

    if (l:res == 0)
        normal G
        exe "normal o" . a:line
    endif

endfunction
  • Note the trick with escape() to add a \ in front of the characters which might cause a problem in the search.
  • Note also that you'll still have to escape the \ by yourself when calling the function (I didn't manage to escape \ automatically):

    call AddLine("export PATH=~/.composer/vendor/bin:\\$PATH")
    
statox
  • 49,782
  • 19
  • 148
  • 225
2

Try:

ex ~/.bashrc -c "if search('export PATH=\~\/.composer\/vendor\/bin:\$PATH')>0 | norm quit | endif | norm Aexport PATH=~/.composer/vendor/bin:$PATH"

Note that :append won't work inside if endif-block with Ex mode (see VimDoc) so I have to put norm A... outside.

kenorb
  • 18,433
  • 18
  • 72
  • 134
Alex Kroll
  • 1,069
  • 10
  • 16
2

Most of these answers seem complicated how about good old shell commands:

grep composer/vender ~/.bashrc || cat >> ~/.bashrc <<EOF
export PATH=~/.composer/vendor/bin:\$PATH
EOF

This could easily be a Bash function:

addBashrcPath() {
  if ! grep "$1" ~/.bashrc >/dev/null 2>&1; then
    printf 'export PATH=%s:$PATH' "$1" >> ~/.bashrc
  fi
}

addBashrcPath "~/.composer/vendor/bin"

EDIT: The exit code of grep is important and in this case it needs to be reversed use ! grep; grep -v will not exit as expected in this situation. Thanks you @joeytwiddle for the tip.

Sukima
  • 1,212
  • 8
  • 20
  • Good points. Curious how portable is -q and -F options? I thought the >/dev/null was more universal among different platforms (sun vs hp vs Mac OS X vs Ubuntu vs FreeBSD vs …) – Sukima Oct 07 '15 at 21:46
  • Both -q and -F are completely standard and portable. Unless you're on some weird, ancient, noncompliant system. – Wildcard Feb 17 '17 at 23:50