Sorting words (not lines) in VIM
Asked Answered
B

7

43

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.

Beadsman answered 25/8, 2009 at 12:30 Comment(2)
This is one of those occasions where I think the disease is better than the cure. In particular, I think your tr recipe is much better than those proposed here as answers.Tambac
An alternative is !tr ' ' \\n | sort | xargsQualified
B
23

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.

Beadsman answered 25/8, 2009 at 17:55 Comment(3)
+1 Nice! One comment is that if I have a line "abc def ghi", and select + sort "def ghi", it seems to remove the space before the first word and append it after the last one, instead of preserving it.Blackshear
I had the same problem. Change 'normal i' to 'normal a' to solve it.Representative
If you use this to sort a list of comma-delimited words it won't handle redistributing the commas.Genie
C
28

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
Caribbean answered 25/8, 2009 at 13:49 Comment(3)
How would I modify this to sort the words of the selected text instead of the whole line?Beadsman
@depesz - Sure, but it sorts the entire line, I asked how to sort the selected text.Beadsman
@spatz: Sorting part of a line is rather complicated as commands can't take sub-line ranges, but I've added an example of how to do this.Caribbean
B
23

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.

Beadsman answered 25/8, 2009 at 17:55 Comment(3)
+1 Nice! One comment is that if I have a line "abc def ghi", and select + sort "def ghi", it seems to remove the space before the first word and append it after the last one, instead of preserving it.Blackshear
I had the same problem. Change 'normal i' to 'normal a' to solve it.Representative
If you use this to sort a list of comma-delimited words it won't handle redistributing the commas.Genie
T
14

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

Trudi answered 25/8, 2009 at 13:52 Comment(3)
As @spatz mentioned in the comment for answer from @Al, this sorts the whole line, not a selection on the line.Fowliang
@Trudi Nice solution. Can you explain your 2 use case :'<,'>SortLine ?Imprint
@Imprint - sure; when switching from visual mode to command mode with :, vim autopopulates the command line with '<,'>. This is an example of a command line range, often used to indicate a set of lines. In this case '< means the first line in the visual selection and '> means the last line in the visual selection. Ranges can also be specified using actual line numbers (e.g. 112,499) or use some other special characters, like ^ for the first line of the file and $ for the last. When given a range, call runs the given expression on each line in the range. SortLine does the same.Trudi
S
7
:!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.

Sena answered 25/8, 2009 at 12:54 Comment(2)
I'd hardly say a convoluted Perl script is an improvement…Eddieeddina
I loved it. Simple and elegant.Apocalypse
G
7

Maybe you prefer Python:

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

Visual select text, and execute this line.

Gianna answered 25/8, 2009 at 13:42 Comment(3)
Still new to some of the language integration into Vim, but does this use compiled-in Python support or use an external executable?Fowliang
To clarify, it does not use compiled-in python support in vim. It uses the external python binary as a shell command.Biddie
Ah, sorry. I wasn't aware we are talking about this one-liner. I thought we are talking about Vim at general.Gianna
M
3

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

Medicine answered 23/12, 2014 at 14:43 Comment(1)
Works really fine! Thanks for the plugin!Ire
L
0

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.

Luxor answered 23/7, 2024 at 2:44 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.