drrlvn
drrlvn

Reputation: 8437

Sorting words (not lines) in VIM

The built-in VIM :sort command sorts lines of text. I want to sort words in a single line, e.g. transform the line

b a d c e f

to

a b c d e f

Currently I accomplish this by selecting the line and then using :!tr ' ' '\n' | sort | tr '\n' ' ', but I'm sure there's a better, simpler, quicker way. Is there?

Note that I use bash so if there's a shorter and more elegant bash command for doing this it's also fine.

EDIT: My use-case is that I have a line that says SOME_VARIABLE="one two three four etc" and I want the words in that variable to be sorted, i.e. I want to have SOME_VARIABLE="etc four one three two".

The end result should preferably be mappable to a shortcut key as this is something I find myself needing quite often.

Upvotes: 44

Views: 11128

Answers (7)

Walf
Walf

Reputation: 9348

It's not necessary to select the current line, just prefix a range to !. In the simple case you describe, not too many items, okay to split on shell's IFS, nothing that would be interpreted by echo as an option, you can also do this:

:.!xargs -n 1 echo | sort | xargs echo

It avoids the invisible trailing space being added to the line, that the tr solution does.

For more complex values than single letters, you can make it more robust with printf and paste:

:.!xargs -n 1 printf '\%s\n' | sort | paste -sd ' '

This can also be modified to accept different delimiters, e.g. a colon:

:.!xargs -d : -n 1 printf '\%s\n' | sort | grep . | paste -sd :

which would convert b:a:d:c:e:f to a:b:c:d:e:f.

Or tab-separated:

:.!xargs -d $'\t' -n 1 printf '\%s\n' | sort | grep . | paste -s

The grep prevents an extra delimiter being added to the start of the line; you could remove it manually with 0x.

Upvotes: 0

iElectric
iElectric

Reputation: 5819

Maybe you prefer Python:

!python -c "import sys; print(' '.join(sorted(sys.stdin.read().split())))"

Visual select text, and execute this line.

Upvotes: 7

Ingo Karkat
Ingo Karkat

Reputation: 172648

My AdvancedSorters plugin now has a :SortWORDs command that does this (among other sorting-related commands).

Upvotes: 3

drrlvn
drrlvn

Reputation: 8437

Using great ideas from your answers, especially Al's answer, I eventually came up with the following:

:vnoremap <F2> d:execute 'normal i' . join(sort(split(getreg('"'))), ' ')<CR>

This maps the F2 button in visual mode to delete the selected text, split, sort and join it and then re-insert it. When the selection spans multiple lines this will sort the words in all of them and output one sorted line, which I can quickly fix using gqq.

I'll be glad to hear suggestions on how this can be further improved.

Many thanks, I've learned a lot :)

EDIT: Changed '<C-R>"' to getreg('"') to handle text with the char ' in it.

Upvotes: 23

DrAl
DrAl

Reputation: 72696

In pure vim, you could do this:

call setline('.', join(sort(split(getline('.'), ' ')), " "))

Edit

To do this so that it works over a range that is less than one line is a little more complicated (this allows either sorting multiple lines individually or sorting part of one line, depending on the visual selection):

command! -nargs=0 -range SortWords call SortWords()
" Add a mapping, go to your string, then press vi",s
" vi" selects everything inside the quotation
" ,s calls the sorting algorithm
vmap ,s :SortWords<CR>
" Normal mode one: ,s to select the string and sort it
nmap ,s vi",s
function! SortWords()
    " Get the visual mark points
    let StartPosition = getpos("'<")
    let EndPosition = getpos("'>")

    if StartPosition[0] != EndPosition[0]
        echoerr "Range spans multiple buffers"
    elseif StartPosition[1] != EndPosition[1]
        " This is a multiple line range, probably easiest to work line wise

        " This could be made a lot more complicated and sort the whole
        " lot, but that would require thoughts on how many
        " words/characters on each line, so that can be an exercise for
        " the reader!
        for LineNum in range(StartPosition[1], EndPosition[1])
            call setline(LineNum, join(sort(split(getline('.'), ' ')), " "))
        endfor
    else
        " Single line range, sort words
        let CurrentLine = getline(StartPosition[1])

        " Split the line into the prefix, the selected bit and the suffix

        " The start bit
        if StartPosition[2] > 1
            let StartOfLine = CurrentLine[:StartPosition[2]-2]
        else
            let StartOfLine = ""
        endif
        " The end bit
        if EndPosition[2] < len(CurrentLine)
            let EndOfLine = CurrentLine[EndPosition[2]:]
        else
            let EndOfLine = ""
        endif
        " The middle bit
        let BitToSort = CurrentLine[StartPosition[2]-1:EndPosition[2]-1]

        " Move spaces at the start of the section to variable StartOfLine
        while BitToSort[0] == ' '
            let BitToSort = BitToSort[1:]
            let StartOfLine .= ' '
        endwhile
        " Move spaces at the end of the section to variable EndOfLine
        while BitToSort[len(BitToSort)-1] == ' '
            let BitToSort = BitToSort[:len(BitToSort)-2]
            let EndOfLine = ' ' . EndOfLine
        endwhile

        " Sort the middle bit
        let Sorted = join(sort(split(BitToSort, ' ')), ' ')
        " Reform the line
        let NewLine = StartOfLine . Sorted . EndOfLine
        " Write it out
        call setline(StartPosition[1], NewLine)
    endif
endfunction

Upvotes: 29

user80168
user80168

Reputation:

:!perl -ne '$,=" ";print sort split /\s+/'

Not sure if it requires explanation, but if yes:

perl -ne ''

runs whatever is within '' for every line in input - putting the line in default variable $_.

$,=" ";

Sets list output separator to space. For example:

=> perl -e 'print 1,2,3'
123

=> perl -e '$,=" ";print 1,2,3'
1 2 3

=> perl -e '$,=", ";print 1,2,3'
1, 2, 3

Pretty simple.

print sort split /\s+/

Is shortened version of:

print( sort( split( /\s+/, $_ ) ) )

($_ at the end is default variable).

split - splits $_ to array using given regexp, sort sorts given list, print - prints it.

Upvotes: 7

rampion
rampion

Reputation: 89093

Here's the equivalent in pure vimscript:

 :call setline('.',join(sort(split(getline('.'),' ')),' '))

It's no shorter or simpler, but if this is something you do often, you can run it across a range of lines:

 :%call setline('.',join(sort(split(getline('.'),' ')),' '))

Or make a command

 :command -nargs=0 -range SortLine <line1>,<line2>call setline('.',join(sort(split(getline('.'),' ')),' '))

Which you can use with

:SortLine
:'<,'>SortLine
:%SortLine

etc etc

Upvotes: 14

Related Questions