3e314e533b1d

Fix the shellescape/expand issue.

Thanks @mrgrubb and @sedm0784.
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Sat, 16 Jun 2012 16:05:20 -0400
parents 7dcc3e9f4ec3
children ee45fef4cd6b
branches/tags (none)
files chapters/32.markdown

Changes

--- a/chapters/32.markdown	Sat Jun 16 15:33:39 2012 -0400
+++ b/chapters/32.markdown	Sat Jun 16 16:05:20 2012 -0400
@@ -113,9 +113,14 @@
     :nnoremap <leader>g :grep -R '<cWORD>' .<cr>
 
 Most shells treat single-quoted text as (almost) literal, so our mapping is much
-more robust now.  However there's still one more problem with the search term!
-Try the mapping on the word "that's".  It won't work, because the single quote
-inside the word interferes with the quotes in the grep command!
+more robust now.
+
+Escaping Shell Command Arguments
+--------------------------------
+
+However there's still one more problem with the search term.  Try the mapping on
+the word "that's".  It won't work, because the single quote inside the word
+interferes with the quotes in the grep command!
 
 To get around this we can use Vim's `shellescape` function.  Read `:help
 escape()` and `:help shellescape()` to see how it works (it's pretty simple).
@@ -133,7 +138,51 @@
     :::vim
     :nnoremap <leader>g :execute "grep -R " . shellescape("<cWORD>") . " ."<cr>
 
-And now our mapping won't break if the word we're searching for happens to
+Try it out by running it on a normal word like "foo".  It will work properly.
+Now try it out on a word with a quote in it, like "that's".  It will not work!
+What happened?
+
+The problem is that Vim performed the `shellescape()` call *before* it expanded
+out special strings like `<cWORD>` in the command line.  So Vim shell-escaped
+the literal string `"<cWORD>"` (which did nothing but add single quotes to it)
+and then concatenated it with the strings of our `grep` command.
+
+You can see this by running the following command:
+
+    :::vim
+    :echom shellescape("<cWORD>")
+
+Vim will output `'<cWORD>'`.  Note that those quotes are actually part of the
+string -- Vim has prepared it for use as a shell command argument.
+
+To fix this we'll use the `expand()` function to force the expansion of
+`<cWORD>` into the actual string *before* it gets passed to `shellescape`.
+
+Let's break this apart and see how it works, in steps.  Put your cursor over
+a word with q quote, like "that's", and run the following command:
+
+    :::vim
+    :echom expand("<cWORD>")
+
+Vim outputs `that's` because `expand("<cWORD>")` will return the current word
+under the cursor as a Vim string.  Now let's add `shellescape` back in:
+
+    :::vim
+    :echom shellescape(expand("<cWORD>"))
+
+This time Vim outputs `'that'\''s'`.  If this looks a little funny, you haven't
+had the pleasure of wrapping your brain around shell-quoting in all its insane
+glory.  For now, don't worry about it.  Just trust the Vim has taken the string
+from `expand` and escaped it properly.
+
+Now that we know how to get a fully-escaped version of the word under the
+cursor, it's time to concatenate it into our mapping!  Run the following
+command:
+
+    :::vim
+    :nnoremap <leader>g :execute "grep -R " . shellescape(expand("<cWORD>")) . " ."<cr>
+
+Try it out.  Our mapping won't break if the word we're searching for happens to
 contain strange characters.
 
 The process of starting with a trivial bit of Vimscript and transforming it
@@ -149,14 +198,14 @@
 this command:
 
     :::vim
-    :nnoremap <leader>g :execute "grep! -R " . shellescape("<cWORD>") . " ."<cr>
+    :nnoremap <leader>g :execute "grep! -R " . shellescape(expand("<cWORD>")) . " ."<cr>
 
 Try it out again and nothing will seem to happen.  Vim has filled the quickfix
 window with the results, but we haven't opened it yet.  Run the following
 command:
 
     :::vim
-    :nnoremap <leader>g :execute "grep! -R " . shellescape("<cWORD>") . " ."<cr>:copen<cr>
+    :nnoremap <leader>g :execute "grep! -R " . shellescape(expand("<cWORD>")) . " ."<cr>:copen<cr>
 
 Now try the mapping and you'll see that Vim automatically opens the quickfix
 window with the search results.  All we did was tack `:copen<cr>` onto the end
@@ -166,7 +215,7 @@
 searching.  Run the following command:
 
     :::vim
-    :nnoremap <leader>g :silent execute "grep! -R " . shellescape("<cWORD>") . " ."<cr>:copen<cr>
+    :nnoremap <leader>g :silent execute "grep! -R " . shellescape(expand("<cWORD>")) . " ."<cr>:copen<cr>
 
 We're done, so try it out and admire your hard work!  The `silent` command just
 runs the command that follows it while hiding any messages it would normally
@@ -185,6 +234,8 @@
 Set up mappings for `:cnext` and `:cprevious` to make it easier to quickly run
 through matches.
 
+Read `:help expand`.
+
 Read `:help copen`.
 
 Add a height to the `:copen` command in the mapping we created to make sure the