15

I would like to join lines together only for lines which have certain pattern (such as ;), however when using g/;/j it doesn't work as expected unless called couple of times.

For example the following content:

a
1;
2;
3;
4;
5;
b
6;
7;
8;
9;
c

when using: :g/;/j the output is:

a
1; 2;
3; 4;
5; b
6; 7;
8; 9;
c

or :g/;/-j gives:

a 1; 2; 3; 4; 5;
b 6; 7; 8; 9;
c

similar with: :g/;\_.\{-};/j.

My expected output is:

a 
1; 2; 3; 4; 5;
b
6; 7; 8; 9;
c

or something similar, so all lines containing the pattern are joined together.

How this can be achieved?

kenorb
  • 18,433
  • 18
  • 72
  • 134
  • 3
    FWIW, :g/;/j doesn't work because it is done in two passes: first the buffer is scanned, then the command is applied to the matching lines. – romainl Dec 31 '15 at 06:59

2 Answers2

19

Possible explanation of the problem

I think the reason why :g/;/j doesn't work is because the :g command operates with a 2-pass algorithm:

  • during the first pass it marks the lines containing the pattern ;
  • during the second pass it operates on the marked lines

During the second pass, :g joins the line 1; with line 2; because 1; was marked during the first pass. However I suspect (not sure) that it doesn't join 1; 2; with 3; because the line 2; doesn't exist anymore, its content has been merged with the line 1; which has already been processed.

So :g looks for the next line which was marked during first pass (3;) and joins it with the following one (4;). After that the problem repeats, it can't join 3; 4; with 5; because the line 4; doesn't exist anymore.

Solution 1 (with vimscript)

Maybe you could call a function whenever a line containing ; is found to check whether the previous line also contains a semicolon:

function! JoinLines()
    if getline(line('.')-1) =~ ';'
        .-1join
    endif
endfunction

Then use the following global command:

:g/;/call JoinLines()

Or without a function:

:g/;/if getline(line('.')-1) =~ ';' | -j | endif

Solution 2 (without vimscript)

:g/;/.,/^[^;]*$/-1j

Whenever the global command :g finds the pattern ; it executes the command: .,/^[^;]*$/-1j

It can be broken down like this:

:g/pattern/a,bj

Where :

pattern = ;
a       = .           = number of current line
b       = /^[^;]*$/-1 = number of next line without any semicolon minus one

b can be broken down further like this:

/    = look for the number of the next line matching the following pattern
^    = a beginning of line
[^;] = then any character except a semicolon
 *   = the last character can be repeated 0 or more times
 $   = an end of line
 /   = end of pattern
 -1  = removes one to the number you just got

j is the abbreviated form of the Ex command :join which like most other Ex commands can be preceded by a range.
Here it's preceded by the range: .,/^[^;]*$/-1 (a,b)
A range follows the form a,b where a and b are generally 2 line numbers, and allows you to operate on a group of lines whose number is between a and b, instead of just one.

So the j command joins all the lines between the current one (a) and the next one which doesn't contain any semicolon minus one (b).

For more information, see:

:help :global
:help :join
:help :range
saginaw
  • 6,756
  • 2
  • 26
  • 47
  • I can't tell you how many times I visited your answer. When I think "ok this time I got it and can do it myself" then somehow I find myself in this answer :) Thank you. – caltuntas Sep 02 '22 at 06:56
3

I do similar joining all the time with a global search and replace:

s/;\n/;/

\n matches newline.

To find and delete blank lines:

s/^$\n//

I am not sure why, but if want to insert a new line you have to use \r

rlh100
  • 31
  • 2
  • s alone will work only for one line, to make it global, you need to use %s, but then it'll join almost all the lines, including non ; lines – kenorb Jan 01 '16 at 20:39
  • 2
    @kenorb Ehm no, I think you can use the :s command exactly for what you want. I think this %s/;\n\(.*;\)\@=/;/ does what you need. – Christian Brabandt Jan 01 '16 at 21:01
  • @Christian Brabandt Your solution %s/;\n\(.*;\)\@=/;/ works perfectly, but what is \@= ? – Mark Jun 19 '20 at 16:19
  • @Mark I just converted your answer to a comment; in doing so, I thought I would point out that you can use :help followed by almost anything to get more advice. Even just plain :help is very useful. – D. Ben Knoble Jun 19 '20 at 19:36