10

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?

  • Can't you outsource it? grep -Fq 'export PATH=~/.composer/vendor/bin:$PATH' ~/.bashrc || ex ... (or cat, for that matter)? – muru Sep 25 '15 at 3:12
  • I believe this could be something similar to this approach, but opposite (when string is not there, do something). – kenorb Sep 25 '15 at 20:35
  • ex ~/.bashrc -c "if search('export PATH=\~\/.composer\/vendor\/bin:\$PATH')>0 | norm quit | endif | norm Aexport PATH=~/.composer/vendor/bin:$PATH" – Alex Kroll Oct 1 '15 at 14:33
  • 1
    Note that export is a command, so the rest of the line is a shell word, NOT an assignment. Therefore unlike with a variable assignment (that doesn't use export), you need double quotes or it will break on whitespace. Also see How to correctly add a path to PATH. – Wildcard Mar 14 '16 at 23:42
  • 1
    I found the actual link I was looking for yesterday (although the above links are good also). Are quotes needed for local variable assignment? – Wildcard Mar 15 '16 at 22:41
6

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
2

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")
    
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.

1

I normally use:

perl -i -p0e '$_.="export PATH=~/bin:\$PATH\n" unless m!~/bin:!' ~/.bashrc
1
+50

In order to simplify and avoid the presence of many \, the line to be added will be export PATH=mybin:PATH.

Main strategy: substitute the "non-mybin" text (all file) with text+mybin-path-definition:

 vim  +'s#\v%^((.|\n)(mybin:)@!)*%$#&\rexport PATH=mybin:PATH#e' -cx bashrc

or with more didactic indentation:

s#                                    substitute
   \v                                  very magical to redude \

   %^                                  file begin
   ((.|\n)(mybin:)@!)*                 multi-line str without "mybin:"
                                         (any char not followed by "mybin")*
   %$                                  end of file
 #                                    by

If we have a match:

  1. & has the full text of bashrc and
  2. & does not contain /mybin:/

so:

 #                                    by ...
   &                                   all file
   export PATH=mybin:PATH              and the new path
 #e                                   don't complain if patt not found

-cx                                   save and leave 

UPDATE: Finally, we can make a vim script:

$ cat bashrcfix

#!/usr/bin/vim -s
:e ~/fakebashrc
:s#\v%^((.|\n)(mybin:)@!)*%$#&\rexport PATH=mybin:PATH#e
:x

chmod 755 and install it. Usage: bashrcfix


be careful: in \v mode, = means optional

1

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.

  • I don't think grep -v gives you the exit code you want. But ! grep should do it. – joeytwiddle Oct 4 '15 at 11:39
  • If that's the case then you don’t even need the -v just the !. – Sukima Oct 4 '15 at 13:37
  • Exactly. You could edit your answer to make that correction. – joeytwiddle Oct 4 '15 at 20:23
  • 1
    1. You should use -F so that grep dosn't interpret characters as regular expressions, and 2. Use -q instead of redirecting to /dev/null. See my earlier comment on question. – muru Oct 6 '15 at 22:06
  • 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 7 '15 at 21:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.