I want to find the last index of any character in the [abc] set in the abcabc string but the search should start from the end of the string:

" Returns the 0th index but I want the 5th.
let a=match('abcabc', '[abc]')

I skimmed through Vim's "4. Builtin Functions" (:h functions) but the only method that looked promising,the reverse method, only operates on lists. A limitation I don't understand because functions like len were designed to work even with strings, numbers, and lists.

To solve the problem I came up with my following function:

function! s:Rvrs(str)
  let a=len(a:str)      
  let b=a - 1
  let c=''
  while b >= 0
    let c.=a:str[b]
    let b-=1
  return c

So I can say let a=match(s:Rvrs('abcabc'), '[abc]').

  • Maybe I am misunderstanding, but shouldn’t your example of looking for [abc] in abcacb show that the last matching index is 5 (the zero-based index of the last character of abcabc), not 3? What if you look for [abc] in abcd? Should that be 2? Will you need to generalize beyond looking for the last occurrence of a character class (e.g. needing to match something that is longer than a single character)? – Chris Johnsen Dec 15 '12 at 8:01
  • @ChrisJohnsen The last matching index should be 5, indeed. And yes, once I found the first match I want to have it find further matches. Also a variant that returns all matched indexes at once would be of help. Thanks for pointing out my mistakes. – Tim Friske Dec 15 '12 at 9:16

I looked around, but did not find any built in function that looked like it would do what you want.

You might find the following functions useful though: (variations included for overlapping, and non-overlapping matches starting from the beginning or the end of the string; all of them support multi-character patterns with some restrictions or limitations around uses of \zs and/or \ze)

function! s:AllOverlappableMatches(str, pat)
    " Restriction: a:pat should not use \ze
    let indicies = []
    let index = 0
    let splits = split(a:str, '\ze'.a:pat, 1)
    for segment in splits
        if len(segment) == 0
            call add(indicies, index)
            let index += len(segment)
    return indicies
function! s:AllOverlappableMatchesFromEnd(str, pat)
    " Restriction: a:pat should not use \ze
    return reverse(s:AllOverlappableMatches(a:str, a:pat))

function! s:AllNonoverlappingMatches(str, pat)
    " If a:pat uses \zs, the returned indicies will be based on that
    " position.
    " If a:pst uses \ze, subsequent matches may re-use characters
    " after \ze that were consumed, but not 'matched' (due to \ze)
    " in earlier matches.
    let indicies = []
    let start = 0
    let next = 0
    while next != -1
        let next = match(a:str, a:pat, start)
        if next != -1
            call add(indicies, next)
            let start = matchend(a:str, a:pat, start)
    return indicies
function! s:AllNonoverlappingMatchesFromEnd(str, pat)
    " If a:pat uses \zs, the returned indicies will be based on that
    " position.
    let str = a:str
    let indicies = []
    let start = len(a:str) - 1
    while start >= 0
        let next = match(str, '.*\zs' . a:pat, start)
        if next != -1
            call add(indicies, next)
            let str = str[ : next - 1]
        let start -= 1
    return indicies

echo s:AllOverlappableMatchesFromEnd('abcabc', '[abc]')
" -> [5, 4, 3, 2, 1, 0]

echo s:AllOverlappableMatchesFromEnd('dabcabc', '[abc]')
" -> [6, 5, 4, 3, 2, 1]

echo s:AllOverlappableMatchesFromEnd('dab - cabc', '[abc]')
" -> [9, 8, 7, 6, 2, 1]

echo s:AllOverlappableMatchesFromEnd('dab - cabce', '[abc]')
" -> [9, 8, 7, 6, 2, 1]

echo s:AllOverlappableMatchesFromEnd('dab - cabc', '[abc]\{2}')
" -> [8, 7, 6, 1]

echo s:AllOverlappableMatches('dab - cabc', '[abc]\{2}')
" -> [1, 6, 7, 8]              0123456789

echo s:AllNonoverlappingMatches('dab - cabc', '[abc]\{2}')
" -> [1, 6, 8]                   0123456789

echo s:AllNonoverlappingMatchesFromEnd('dab - cabca', '[abc]\{2}')
" -> [9, 7, 1]                          0123456789A

echo s:AllNonoverlappingMatchesFromEnd('ab - cabca', '[abc]\{2}')
" -> [8, 6, 0]                          0123456789

echo s:AllNonoverlappingMatchesFromEnd('abcabc', '[abc]\{2}')
" -> [4, 2, 0]                          012345

echo s:AllNonoverlappingMatchesFromEnd(' ab c abcd', '[abc]\{2}')
" -> [7, 1]                             0123456789

echo s:AllNonoverlappingMatchesFromEnd('abcabc', '[abc]\{2}')
" -> [4, 2, 0]                          012345

echo s:AllNonoverlappingMatches( 'abcabcabbc', 'abc')
" -> [0, 3]                       0123456789
echo s:AllNonoverlappingMatchesFromEnd( 'abcdabcabbc', 'abc')
" -> [4, 0]                              0123456789A

" A multi-character, overlappable pattern
echo s:AllOverlappableMatchesFromEnd( 'aaaabcaaac', 'aaa')
" -> [6, 1, 0]                         0123456789

If I read the question,

echo match('abcabc', '.*\zs[abc]')

is an answer: it will return the beginning of the last occurrence of the pattern in the text.

If you want the other occurrences that are before the last one, you'll have to cut the string to work on 'abcabc'[0:start_of_match+len(matched_string)-1] if you want to accept overlaps (which makes no sense in your case as you're looking for [abc], and not abc), or 'abcabc'[0:start_of_match-1] otherwise.

EDIT: Sorry I've missed that it was what Chris Johnsen's code was doing.

  • Yours is a bit shorter. :) – joeytwiddle Jul 29 '14 at 1:35
  • :h pattern-atoms explains: \zs Matches at any position, and sets the start of the match there: The next char is the first char of the whole match. – joeytwiddle Jul 29 '14 at 1:36

Add following function to your .vimrc file

function! Rvrs( str, chars )
    "" 'a:chars' is a string. Convert it to a list.
    let l:chars_list = split( a:chars, '\zs' )

    "" Process 'str' from the end one character each time. 
    "" I remove that character from the list if found.
    let l:i = len( a:str )
    while l:i >= 0 && ! empty( l:chars_list ) 
        let l:i = l:i - 1 
        if index( l:chars_list, strpart( a:str, l:i, 1 ) ) != -1
          let l:dummy = remove( l:chars_list, index( l:chars_list, strpart( a:str, l:i, 1))) 

    "" If the loop go throught all the string means that couldn't find 
    "" all characters of the list, so return and incorrect code. 
    "" Otherwise the position where the list got empty.
    if i < 0 
        return -1
        return l:i 

and execute it like:

:echo Rvrs( 'abcabcabbc', 'abc' )

That yields:

  • I already wondered at other times whether the split method could take a string character-wise apart, i.e. without any delimiters getting eaten up. I didn't know about the '\zs' zero-width regex atom. Great, thanks! – Tim Friske Dec 15 '12 at 9:26
  • But still, is there a Vim built-in function that comes close to my question? – Tim Friske Dec 15 '12 at 9:28
  • @TimFriske: After your edit this answer doesn't solve it. It gives the last position of any of the letters, not of all of them. So, in the example it gets the last position of letter 'a', because it's the first one with no repeated appearance after it. I don't know of a built-in but perhaps this solution can help you, because it seems simpler now. – Birei Dec 15 '12 at 11:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.