Reputation: 8437
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
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
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
Reputation: 172648
My AdvancedSorters plugin now has a :SortWORDs
command that does this (among other sorting-related commands).
Upvotes: 3
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
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
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
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