Decode Vim find/replace string that calls a function - vim

I am using the following Vim command line that inserts line numbers to the beginning of lines:
:let i = 1 | %s/^/\='LINE_' . Inc()/g
Inc() is a function that increments the i variable.
This is all working fine. My questions:
1) What does the dot do in the replacement part?:
:let i = 1 | %s/^/\='LINE_' . Inc()/g
^
2) What does the pipe character do? Is there actual piping going on, or is it just syntax?
3) What does the \= do? I think it is used to call the function, but Vim help only shows information for \= as being a quantifier in regex.
4) I have not been able to insert a space after the line number and the first character of the actual line. How can I do this? Anything I place after Inc() in the replacement part is either being ignored or causing an E15 invalid expression error.
I am using Vim 7.3 on Windows 7.

Some explanation:
. expression will concatenate two strings. See :h expr-.
| will separate to ex-commands. See :h :bar
A replacement starting with \= in :s command means the rest of the replacement is to be treated as an vim expression. See :h :s\=
Concatenate a string with a space after the Inc() function call. :let i = 1 | %s/^/\='LINE_' . Inc() . ' '/g

Related

Indent a block of code on the basis of a single character

The question's title might sound a little vague, so I'll explain the situation here more clearly.
I have these lines of code in a file which I want to align with respect to the character =.
const service = require('./service');
const baseService = require('./baseService');
const config = require('../config');
const Promise = require('bluebird');
const errors = require('../errors');
I want the above lines to somehow look like this
const service = require('./service');
const baseService = require('./baseService');
const config = require('../config');
const Promise = require('bluebird');
const errors = require('../errors');
I want all the = characters to lie in the same column and shift the after-coming code accordingly. What can I do to achieve this task?
A plugin capable of doing this would be nice, but it'd be great if I could do this without the aid of any plugin. That way I'd also learn something.
Using GNU tools
:%!column -t -s= -o=
% ............. current file
! ............. use external command
-t ............. use tabs
-s ............. input separator
-o ............. output separator
Instead of whole file it can be a range of lines 1,5 or you can select a paragraph with vip
I ended up creating a function called AlignText that uses column command to solve this issue:
" Align text by a chosen char
" https://stackoverflow.com/questions/57093175/
" https://vi.stackexchange.com/a/2412/7339
if !exists('*AlignText')
function! AlignText(param) range
execute a:firstline . ',' . a:lastline . '!column -t -s' . a:param . ' -o' . a:param
endfunction
endif
command! -range=% -nargs=1 Align <line1>,<line2>call AlignText(<q-args>)
" :Align =
" :8,$ Align =
If you wnat to test this function before put it into your vimrc, copy the code to your clipboard and then try this:
:#+
Now you can use something like this:
:15,22Align /
:Align ,
I by any change you want to use the function call instead of the command one, don't forget to pass the range and put the argument between quotes.
A slitly different version that accepts nor arguments and uses just "column -t"
" https://stackoverflow.com/questions/57093175/
" https://vi.stackexchange.com/a/2412/7339
function! AlignText(...) range
if a:0 < 1
execute a:firstline . ',' . a:lastline . '!column -t'
else
execute a:firstline . ',' . a:lastline . '!column -t -s' . a:1 . ' -o' . a:1
endif
endfunction
command! -range=% -nargs=? Align <line1>,<line2>call AlignText(<f-args>)
there are plugins can do this kind of alignment. E.g.
Align or easy-align. I have been using Align for a long time, for your requirement, I just select those lines and do <leader>t=
https://github.com/vim-scripts/Align
https://github.com/junegunn/vim-easy-align
You can of course code by yourself, you find out the max length before the =, then you know how many spaces you should insert between partBefore and =. (the diff) on each line.
(NOTE: Originally answered at the Vi and Vim Stack Exchange.)
If you're in a pinch and want to get the expressions aligned, without having to install and learn any plug-ins, here is a quick way to do it.
Select the lines on a visual selection. For example, if this is your whole buffer, you could use ggVG, but if these lines are in the middle of a file, just select appropriately. Perhaps V4j?
Insert enough whitespace before the =, which you can do with :normal f=9i . (Note the "space" at the end.) This will add 9 spaces before each = in the lines of the visual selection. If 9 is not enough, add more (like 99 or 999, as much as you want.) Note that when you type : with the visual selection, Vim will automatically insert the range, so the actual command is :'<,'>normal f=9i , but you don't need to type those characters.
Move to the column where you want the =s to be flushed to. In this case, line 2 has the longest variable name, so move to two spaces after the end of that name, which is where the =s should be at the end. If this is the whole buffer, you could use 2G2e2l to get there.
Staying on that same column, move to the first line of the block. In this case, you're moving from line 2 to line 1, so k is enough.
Start visual-block selection, pressing Ctrl-V.
Move to the last line of the block. If this is the whole buffer, you could use G, if this is the middle of a file, you could use 4j to go four lines down, etc.
(Optional) Use $ to select until the end of the line, on every line of the visual block selection. UPDATE: This step is actually not necessary here, since < will work correctly even if you don't select lines to the end. Using $ is important when using e.g. A to append to the end of all lines though.
Now you can use the < command to shift the lines left, but until they hit the left of the visual block. Each < will shift them by one 'shiftwidth' only, so you're likely to need more than one. So, to be sure, use 9< (or 99<, or 999<, if you added tons of spaces in step 2.)
VoilĂ !
This is a pretty cool technique and it can be helpful when you need more flexibility than plug-ins can afford you. It's a good one to learn and keep on your Vim toolbox.

Vimscript remove control characters

I have a vimscript function shown below that performs a search and replace on the currently yanked/copied text buffer and pastes them into a file.
function Repaste(s, ...)
for i in a:000
let sub = substitute(getreg('"'), a:s, i, 'ge')
let sane = substitute(sub, '[^[:print:]]', '\n', 'ge')
call append(line('.'), sane)
endfor
endfunction
command -nargs=* RP call Repaste(<f-args>)
When I call this function I get ^# characters in place of new lines.
Here is an example of the yanked/copied text
set cindent
Here is an example of the command executed
:RP c d e f
Here is the output
set findent ^#
set eindent ^#
set dindent ^#
How do i remove these characters and why do they appear? Thanks.
The append() function is a low-level one. :help append() shows that the {expr} as a String type is inserted as one text line, regardless of newlines in its content. The ^# is the representation of \n inside a line; cp :help <Nul>.
If you really want to keep using append(), you have to provide a List type; to obtain this, you can split() your String:
call append(line('.'), split(sane, '\n'))
However, I think you're better off by using a higher-level function to insert the created lines, using :put with the expression register to insert the contents of your variable:
put =sane
This will:
automatically handle embedded newlines
set the change marks '[ and '] to the inserted text
print a message 4 more lines (if the inserted number exceeds the 'report' threshold).
Replacing the call to append with put=sub produces the required result.

Vim: Replace first word on each line of selection with number in list

I have a large file containing a selection with jumbled numbers. It's supposed to be a seqeunce 1, 2, 3, ... but a few lines got screwed up. I would like to change something like
foo
bar
1 foobar
12345 foobar
6546458 foobar
4 foobar
to
foo
bar
1 foobar
2 foobar
3 foobar
4 foobar
I know I can use something like 3,$ to select the lines I care about and put = range(1,1000) to make new lines starting with the numbers I want, but I want to put these numbers on lines that currently have data, not new lines. The jumbled numbers are several characters long, but always one word. Thanks.
Do the following:
:let i=1
:g/^\d\+/s//\=i/|let i=i+1
Overview
Setup some variable (let i=1) to use as our counter. On every line that starts with a number (:g/^\d\+/) we execute a substitution (:s//\=i/) to replace the pattern with our counter (\=i) and then increment our counter (let i=i+1).
Why use :g at all? Why not just :s?
You could do this with just a substitution command however the sub-replace-expression, \=, needs an expression to evaluate to a value (see :h sub-replace-expression). As let i = i + 1 is a statement it will not be useful.
There are a few ways to overcome this issue:
Create a function that increments a variable and then returns it
Use an array instead and then alter the number inside (in-place) then return the value out of the array. e.g. map(arr, 'v:val+1')[0]
If you only have 1 substitution per line then do the :g trick from above
Full example using in-place array modification:
:let i=[1]
:%s/^\d\+/\=map(i,'v:val+1')[0]
Personally, I would use whatever method you can remember.
More help
:h :s
:h sub-replace-expression
:h :g
:h :let
:h expr
:h map(
:h v:val
/^\d\+\s -- Searches for the first occurrence
ciw0<Esc> -- Replaces the word under cursor with "0"
yiw -- Copies it
:g//norm viwp^Ayiw
-- For each line that matches the last search pattern,
-- Replace the current word with copied text,
-- Increment it,
-- Copy the new value.
(<Esc> is just Esc. ^A is entered as Ctrl+V, Ctrl+A)
ciw -- Change inner word. (:help c)
:g -- Global search. (:help :g)
viw -- Select inner word. (:help v)
p -- Paste (replacing selection) (:help v_p)
^A -- Increment. (:help CTRL-A)
yiw -- Yank inner word. (:help y)
You can use the following function:
function Replace()
let n = 1
for i in range(0, line('$'))
if match(getline(i), '\v^\d+\s') > -1
execute i . 's/\v^\d+/\=n/'
let n = n + 1
endif
endfor
endfunction
It traverses the whole file, check if each line begins with a number followed by a space character and does the substitution with the counter that increments with each change.
Call it like:
:call Replace()
That in your example yields:
foo
bar
1 foobar
2 foobar
3 foobar
4 foobar

How to make Vim's global command :g/ work on per occurrence basis

Typically Vim's global command :g// works on per line basis. Is it possible to make it work on per occurrence basis as there could be more than one occurrence on a line.
Not a direct answer, but you could use something like :rubydo, which will run some ruby scriptlet per line of code. Combining that with gsub in ruby should get you the ability to do just about anything per occurrence of a match. Of course, you will need to do it with ruby code, which may not give you access to everything that you might need without hassle (register appending would be annoying for instance)
:[range]rubyd[o] {cmd} Evaluate Ruby command {cmd} for each line in the
[range], with $_ being set to the text of each line in
turn, without a trailing <EOL>. Setting $_ will change
the text, but note that it is not possible to add or
delete lines using this command.
The default for [range] is the whole file: "1,$".
You can try:
command! -bang -nargs=1 -range OGlobal
\ <line1>,<line2>call s:Global("<bang>", <f-args>)
function! s:Global(bang, param) range
let inverse = a:bang == '!'
" obtain the separator character
let sep = a:param[0]
" obtain all fields in the initial command
let fields = split(a:param, sep)
" todo: handle inverse
let l = a:firstline
while 1
let l = search(fields[0], 'W')
if l == -1 || l > a:lastline
break
endif
exe fields[1]
endwhile
endfunction
Which you can use as :global, except that the command name is different (and that the bang option as not been implemented yet)

How to "apply" backspace characters within a text file (ideally in vim)

I have a log file with backspace characters in it (^H). I'm looking through the file in Vim and it can be quite hard to see what's going on.
Ideally I'd like to be able to "apply" all the ^H on a given line/range so that I can see the final result.
I'd much rather do this within Vim on a line-by-line basis, but a solution which converts the whole file is better than nothing.
Turn on the 'paste' option (using :set paste), and then press dd i <CTRL-R> 1 <ESC> on each line that you want to apply the backspaces to. This also works if you delete multiple lines, or even the whole file.
The key here is that you are using <CTRL-R> 1 in insert mode to 'type out' the contents of register 1 (where your deleted lines just got put), and 'paste' option prevents Vim from using any mappings or abbreviations.
I googled this while trying to remember the command I had used before to `apply' backspaces, and then I remembered it: col -b - here is the manpage. (It does a little more and comes from BSD or more exactly AT&T UNIX as the manpage says, so if you are on Linux you may need to install an additional package, on debian its in bsdmainutils.)
Simplistic answer:
:%s/[^^H]^H//g
where ^^H is:
Literal ^ character
Ctrl-V Ctrl-H
and repeat it couple of times (until vim will tell you that no substitutions have been made
If you want without repetition, and you don't mind using %!perl:
%!perl -0pe 's{([^\x08]+)(\x08+)}{substr$1,0,-length$2}eg'
All characters are literal - i.e. you don't have to do ctrl-v ... anywhere in above line.
Should work in most cases.
All right, here is a bare-metal solution.
Copy this code into a file named crush.c:
#include <stdio.h>
// crush out x^H sequences
// there was a program that did this, once
// cja, 16 nov 09
main()
{
int c, lc = 0;
while ((c = getchar()) != EOF) {
if (c == '\x08')
lc = '\0';
else {
if (lc)
putchar(lc);
lc = c;
}
}
if (lc)
putchar(lc);
}
Compile this code with your favorite compiler:
gcc crush.c -o crush
Then use it like this to crush out those bothersome sequences:
./crush <infilename >outfilename
Or use it in a pipeline ("say" is a speech-to-text app on the Mac)
man date | ./crush | say
You can copy crush to your favorite executable directory (/usr/local/bin, or some such) and then reference it as follows
man date | crush | say
Just delete all occurrences of .^H (where . is the regex interpretation of .):
:s/.^H//g
(insert ^H literally by entering Ctrl-V Ctrl-H)
That will apply to the current line. Use whatever range you want if you want to apply it to other lines.
Once you done one :s... command, you can repeat on another line by just typing :sg (you need to g on the end to re-apply to all occurrences on the current line).
How about the following function? I've used \%x08 instead of ^H as it's easier to copy and paste the resulting code. You could type it in and use Ctrl-V Ctrl-H if you prefer, but I thought \%x08 might be easier. This also attempts to handle backspaces at the start of the line (it just deletes them).
" Define a command to make it easier to use (default range is whole file)
command! -range=% ApplyBackspaces <line1>,<line2>call ApplyBackspaces()
" Function that does the work
function! ApplyBackspaces() range
" For each line in the selected lines
for index in range(a:firstline, a:lastline)
" Get the line as a string
let thisline = getline(index)
" Remove backspaces at the start of the line
let thisline = substitute(thisline, '^\%x08*', '', '')
" Repeatedly apply backspaces until there are none left
while thisline =~ '.\%x08'
" Substitute any character followed by backspace with nothing
let thisline = substitute(thisline, '.\%x08', '', 'g')
endwhile
" Remove any backspaces left at the start of the line
let thisline = substitute(thisline, '^\%x08*', '', '')
" Write the line back
call setline(index, thisline)
endfor
endfunction
Use with:
" Whole file:
:ApplyBackspaces
" Whole file (explicitly requested):
:%ApplyBackspaces
" Visual range:
:'<,'>ApplyBackspaces
For more information, see:
:help command
:help command-range
:help function
:help function-range-example
:help substitute()
:help =~
:help \%x
Edit
Note that if you want to work on a single line, you could do something like this:
" Define the command to default to the current line rather than the whole file
command! -range ApplyBackspaces <line1>,<line2>call ApplyBackspaces()
" Create a mapping so that pressing ,b in normal mode deals with the current line
nmap ,b :ApplyBackspaces<CR>
or you could just do:
nmap ,b :.ApplyBackspaces<CR>
Here's a much faster Awk filter that does the same:
#!/usr/bin/awk -f
function crushify(data) {
while (data ~ /[^^H]^H/) {
gsub(/[^^H]^H/, "", data)
}
print data
}
crushify($0)
Note that where ^^H appears, the first caret in ^^H is a caret (shift-6) and the second caret with H is entered (into vim) by typing CTRL-v CTRL-H
Here's a Bash-based filter you can use to process the whole file:
#!/bin/bash
while read LINE; do
while [[ "$LINE" =~ '^H' ]]; do
LINE="${LINE/[^^H]^H/}"
done
echo "$LINE"
done
Note that where ^H appears, it is entered into vim using CTRL-v CTRL-h, and the ^^H is entered as SHIFT-6 CTRL-v CTRL-h.

Resources