4

I would like to define a custom Vim command that takes arguments grouped by single or double quotes. For example, if my arguments are a "b c" then I want a to be considered the first argument and "b c" considered the second argument.

I've unsuccessfully tried commands using <q-args> and <f-args> like this:

function! MyFunc(...)
    echo a:000
endfunction

command! -nargs=* MyCommandQ call MyFunc(<q-args>)
command! -nargs=* MyCommandF call MyFunc(<f-args>)
:MyCommandQ a "b c"     --> ['a "b c"']   (one argument passed)
:MyCommandF a "b c"     --> ['a', '"b', 'c"']   (three arguments passed)

If I use <q-args>, then I can process a:000 in this function to split on single or double quotes and discard whitespace:

function! MyFunc(...)
    echo filter(split(a:000[0],'[''"]'), 'v:val !~ "^\\s*$"')
endfunction

...but it feels like there should be a better way.

2 Answers 2

4

The problem is that the syntax of quoting strings in order to include whitespace in them is somewhat foreign to Vim, particularly in a context such as user-defined commands, which typically take space-separated arguments.

Vim commands such as :args will take a list of filenames, but if you need to add whitespace to one of the filenames, you'll do so by escaping the whitespace character with a backslash. This is how <f-args> works, so if you want to pass it a string with whitespace, you can do so by escaping it with a backslash:

:MyCommandF a b\ c
['a', 'b c']

As an alternative, you can have a command that takes Vim expressions separated by commas, in which case you can use quotes around your strings, but then you need to always quote your strings, since unquoted words will be interpreted as variables. You can do so by using <args> directly in your command definition. For example:

:command! -nargs=* MyCommandA call MyFunc(<args>)
:MyCommandA 'a', 'b c'
['a', 'b c']

This is similar to how Vim native commands such as :echo work.

An advantage of the second approach (using <args>) is that you can pass the command arguments that are not strings (such as numbers, lists, dictionaries, etc.) It's also easier to use Vim variables and functions to determine the value of arguments (though when using an <f-args> command you can always use the :execute command to assemble a command string using variables and functions too.)

If you want a syntax that ressembles the shell syntax of using quoted strings to include whitespace while interpreting unquoted words as strings, then I'm afraid that using <q-args> and parsing the resulting argument string yourself is the only option...

3
  • 1
    Thanks, I think I'll stick <q-args> and parse the resulting argument string. I'm working on a custom command which is a wrapper for :grep optimized for searching specific paths for my project. As a result, I want my command to have shell like syntax. The command was working fine with <q-args> and then appending all of it unprocessed to :grep but then I started thinking about situations where I might want to alter a quoted regex arg. I should probably look at some similar plugins for reference now. Dec 1, 2021 at 12:14
  • 1
    @ChrisHeithoff I'd also consider whether you really want to wrap :grep or if you need some mappings or similar that setup something like :grep <cursor> my_special_args; I would be inclined to let :grep do the heavy lifting of argument parsing where possible. Heck, at that point, I might just do :command -nargs=+ Foo grep <args> my_special_args
    – D. Ben Knoble
    Dec 1, 2021 at 14:04
  • 2
    @D.BenKnoble That's a good thought. Yes, I'm adding my_special_args which is a space-separated list of paths. I wanted to be a little fancier and I temporarily set the quickfixwindow to modifiable so I can substitute really long absolute file names in the quickfixwindow with shorter abbreviated names. I also was thinking about a variation where a visual selection was the argument to grep so I would want to escape special characters from the selection that have special regex meaning. I'm looking through github.com/mhinz/vim-grepper now for ideas. Dec 1, 2021 at 14:47
1

In filbranden's answer he mentions that if you want to have arguments with quoting that works like the shell (space separated unless quoted) you'll have to parse the input string yourself.
I had to write this parsing manually so I thought I'd share:

function! ParseArgs(args)
  let args = []
  let inputCharList = split(a:args, '\zs')
  let lastChar = ''
  let inQuote = ""
  let currentArgChars = []

    let index = 0
  while index < len(inputCharList)
    let char = inputCharList[index]

    " ESCAPED
    if char == "\\"
      " skip to the next character, and take it as is.
      let index = index + 1
      let peek = inputCharList[index]
      call add(currentArgChars, peek)

    " SPACE
    elseif char == " "
      if inQuote != ""
        call add(currentArgChars, char)
      else
        call add(args, currentArgChars->join(""))
        let currentArgChars = []
      endif

    " QUOTE
    elseif char == '"' || char == "'"
      if inQuote != ""
        if inQuote == char " this is the closing char
          let inQuote = ""
        else " not the one we have opened, treat as simple character to parse
          call add(currentArgChars, char)
        endif
      else
        " not in a quote yet, so this is an opening quote
        let inQuote = char
      endif
    else

      " REGULAR CHAR
      call add(currentArgChars, char)
    endif

    let index = index + 1
  endwhile

  call add(args, currentArgChars->join("")) " End last group
  return args
endfunction

It works by simply splitting the input (as string) into a list of characters, and grouping them by space separation, unless quoted or escaped.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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