20

I have learned from this Stack Overflow question that it is possible to use vi/vim to comment out a specified range of line numbers. For example, suppose I have the following bash script:

#!/bin/bash

This
is
my
very
very
great
script

Now suppose that I want to comment out line numbers 6 through 8 (which contain the words very, very, and great) using the # comment character. In vi/vim, I can simply type :6,8s/^/# to obtain the following:

#!/bin/bash

This
is
my
#very
#very
#great
script

which comments out lines 6 through 8.

My question is, is it possible to type a similar one liner that will remove the # comment character from lines 6 through 8 (but not any other commented lines in the file)?

Having said this, I realize that there is some debate about whether I am actually using vi or vim. In practice, I open a file script.sh with the command vi script.sh. Also, when I type the command which vi, I obtain /usr/bin/vi. Nevertheless, when I simply type vi and press Enter, I obtain this:

~                              VIM - Vi IMproved
~
~                               version 7.2.330
~                           by Bram Moolenaar et al.
~                 Vim is open source and freely distributable
~
~                           Sponsor Vim development!
~                type  :help sponsor<Enter>    for information
~
~                type  :q<Enter>               to exit
~                type  :help<Enter>  or  <F1>  for on-line help
~                type  :help version7<Enter>   for version info

which seems to suggest that I'm actually using vim. I am accessing a remote Ubuntu Linux cluster using SSH from my PC. I am not using a Ubuntu Linux GUI.

22

You can use:

:6,8s/^#//

But much easier is to use Block Visual selection mode: Go to beginning of line 6, press Ctrl-v, go down to line 8 and press x.

There is also "The NERD Commenter" plugin.

  • 2
    NERD Commenter is the way to go here in my opinion! +1 for that – user1146332 Jul 30 '13 at 16:46
7

I know your question specifies using vi or vim but here are a few other options for doing this without having to manually open the file:

  • Perl

    perl -ne 'if($. <=8 && $. >= 6){s/^\s*#//;}print' foo.sh 
    
  • Perl version >=5.10

    perl -ne '$. ~~ [6..8] && s/^\s*#//;print' foo.sh 
    

    This will print out the contents of the file, you can either redirect to another (> new_file.sh) or use i to edit the file in place:

    perl -i -ne '$. ~~ [6..8] && s/^\s*#//;print' foo.sh 
    
  • sed

    sed '6,8 s/^ *#//' foo.sh
    

    Again, to make this edit the original file in place, use i:

    sed -i '6,8 s/^ *#//' foo.sh
    
  • awk/gawk etc:

    gawk '(NR<=8 && NR>= 6){sub("^ *#","")}{print}' foo.sh
    
  • Pure bash:

    c=1; while read line; do 
      if [ $c -ge 6 ] && [ $c -le 8 ]; then 
         echo "${line/\#/}"
      else 
         echo $line 
      fi
      let c++; done < foo.sh
    
  • 1
    It's not so much a matter of "having to manually open the file", because usually you decide which lines to comment upon visual inspection, while editing :) But sure, it's a good answer for completeness. – Paulo Almeida Jul 30 '13 at 17:09
  • 2
    @PauloAlmeida you're right of course. I just thought it could be useful since the OP already knows the line numbers (because of the first command used to comment them) and, in any case, the tools I show can be applied to a variety of problems. – terdon Jul 30 '13 at 18:02
4

vi is a symbolic link to vim in most GNU/Linux distribution so you're indeed using vim when you type vi.

To remove the comments, you can type: :6,8s/^#// or :6,8s/^\s*#// to discard some leading space before the # symbol.

  • 1
    Thanks so much. It seems that there may be typos. Maybe it should be :6,8s/^#// and 6,8s/^\s*#//? – Andrew Jul 30 '13 at 16:06
3

You are probably using vim.tiny. In any case, you can remove the initial comments with:

:6,8s/^#//

Of course, if you insert them in a different way (for instance, with an extra space), you may need to remove whatever else is there. With full vim, visually selecting columns and inserting/deleting characters is an easier way to do the same thing.

3

Personally my Favourite way is using visual block mode

ctrl + v to enter visual block mode, the use the arrow keys or hjkl to select the lines and press x or del.

Want them back ?

ctrl + v make the selection, then I (capital i) #Esc

3

AFAIK, vi is usually a symlink of vim nowadays (try which vi or type vi and then follow the symlinks). Maybe, even /usr/bin/vi -> /etc/alternatives/vi -> /usr/bin/vim.basic.

Personally, to remove multiple comment lines, I prefer selecting a vertical block via CtrlV and deleting it. If you need to add a comment symbol to multiple lines, you could CtrlV, then ShiftI, type # and Esc, and a comment will be added to multiple lines.

2

The above answers, using

:6,8s/^#//

are perfect solution, but a bit cumbersome to type. This can be simplified by defining new commands in ~/.vimrc.

command -range=% C :<line1>,<line2>s/^/#/
command -range=% D :<line1>,<line2>s/^#//

And you can just type

:6,8C
:6,8D

to place/delete the command.

If you like visual mode, you can define maps

map <F7> :s/^/#/<CR>
map <F8> :s/^#//<CR>

Such that you only have to select a line range in visual mode, and press F7 and F8 to put and remove comments respectively.

1

This answer is here to 1) show the correct code to paste into a .vimrc to get vim 7.4+ to do block commenting/uncommenting while keeping indentation level with 1 shortcut in visual mode and 2) to explain it.

Here is the code:

let b:commentChar='//'
autocmd BufNewFile,BufReadPost *.[ch]    let b:commentChar='//'
autocmd BufNewFile,BufReadPost *.cpp    let b:commentChar='//'
autocmd BufNewFile,BufReadPost *.py    let b:commentChar='#'
autocmd BufNewFile,BufReadPost *.*sh    let b:commentChar='#'
function! Docomment ()
  "make comments on all the lines we've grabbed
  execute '''<,''>s/^\s*/&'.escape(b:commentChar, '\/').' /e'
endfunction
function! Uncomment ()
  "uncomment on all our lines
  execute '''<,''>s/\v(^\s*)'.escape(b:commentChar, '\/').'\v\s*/\1/e'
endfunction
function! Comment ()
  "does the first line begin with a comment?
  let l:line=getpos("'<")[1]
  "if there's a match
  if match(getline(l:line), '^\s*'.b:commentChar)>-1
    call Uncomment()
  else
    call Docomment()
  endif
endfunction
vnoremap <silent> <C-r> :<C-u>call Comment()<cr><cr>

How it works:

  • let b:commentChar='//' : This creates a variable in vim. the b here refers to the scope, which in this case is contained to the buffer, meaning the currently opened file. Your comment characters are strings and need to be wrapped in quotes, the quotes are not part of what will be substituted in when toggling comments.

  • autocmd BufNewFile,BufReadPost *... : Autocommands trigger on different things, in this case, these are triggering when a new file or the read file ends with a certain extension. Once triggered, the execute the following command, which allows us to change the commentChar depending on filetype. There are other ways to do this, but they are more confusing to novices (like me).

  • function! Docomment() : Functions are declared by starting with function and ending with endfunction. Functions must start with a capital. the ! ensures that this function overwrites any previous functions defined as Docomment() with this version of Docomment(). Without the !, I had errors, but that might be because I was defining new functions through the vim command line.

  • execute '''<,''>s/^\s*/&'.escape(b:commentChar, '\/').' /e' : Execute calls a command. In this case, we are executing substitute, which can take a range (by default this is the current line) such as % for the whole buffer or '<,'> for the highlighted section. ^\s* is regex to match the start of a line followed by any amount of whitespace, which is then appended to (due to &). The . here is used for string concatenation, since escape() can't be wrapped in quotes. escape() allows you to escape character in commentChar that matches the arguments (in this case, \ and /) by prepending them with a \. After this, we concatenate again with the end of our substitute string, which has the e flag. This flag lets us fail silently, meaning that if we do not find a match on a given line, we won't yell about it. As a whole, this line lets us put a comment character followed by a space just before the first text, meaning we keep our indentation level.

  • execute '''<,''>s/\v(^\s*)'.escape(b:commentChar, '\/').'\v\s*/\1/e' : This is similar to our last huge long command. Unique to this one, we have \v, which makes sure that we don't have to escape our (), and 1, which refers to the group we made with our (). Basically, we're matching a line that starts with any amount of whitespace and then our comment character followed by any amount of whitespace, and we are only keeping the first set of whitespace. Again, e lets us fail silently if we don't have a comment character on that line.

  • let l:line=getpos("'<")[1] : this sets a variable much like we did with our comment character, but l refers to the local scope (local to this function). getpos() gets the position of, in this case, the start of our highlighting, and the [1] means we only care about the line number, not other things like the column number.

  • if match(getline(l:line), '^\s*'.b:commentChar)>-1 : you know how if works. match() checks if the first thing contains the second thing, so we grab the line that we started our highlighting on, and check if it starts with whitespace followed by our comment character. match() returns the index where this is true, and -1 if no matches were found. Since if evaluates all nonzero numbers to be true, we have to compare our output to see if it's greater than -1. Comparison in vim returns 0 if false and 1 if true, which is what if wants to see to evaluate correctly.

  • vnoremap <silent> <C-r> :<C-u>call Comment()<cr><cr> : vnoremap means map the following command in visual mode, but don't map it recursively (meaning don't change any other commands that might use in other ways). Basically, if you're a vim novice, always use noremap to make sure you don't break things. <silent> means "I don't want your words, just your actions" and tells it not to print anything to the command line. <C-r> is the thing we're mapping, which is ctrl+r in this case (note that you can still use C-r normally for "redo" in normal mode with this mapping). C-u is kinda confusing, but basically it makes sure you don't lose track of your visual highlighting (according to this answer it makes your command start with '<,'> which is what we want). call here just tells vim to execute the function we named, and <cr> refers to hitting the enter button. We have to hit it once to actually call the function (otherwise we've just typed call function() on the command line, and we have to hit it again to get our substitutes to go through all the way (not really sure why, but whatever).

Anyway, hopefully this helps. This will take anything highlighted with v, V, or C-v, check if the first line is commented, if yes, try to uncomment all highlighted lines, and if not, add an extra layer of comment characters to each line. This is my desired behavior; I did not just want it to toggle whether each line in the block was commented or not, so it works perfectly for me after asking multiple questions on the subject.

0

There is this life changing plugin by tpope called vim-commentary

https://github.com/tpope/vim-commentary

This plugin provides:

  • Sanity
  • Properly indented comments
  • Does not comment out empty/unnecessary lines

Usage:

  • Install via Vundle (or Pathogen I guess).
  • Highlight your text and press : which will show as :<,'>
  • Type Commentary here :<,'>Commentary and press enter.
  • Bom. Your done bud.

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.