From ‘one-off’ to ‘actually I need to do this again…’
Context
My often-talked-about video workflow has a bunch of useful features. For example, if I’ve uploaded a video to YouTube via the web interface (usually on a headless machine via xpra) I have a script which will grab the video id so that the metadata can be updated.
Unfortunately that script is rather simple and only scans back so far — around the past 100 videos or so. So today when I was updating my Mongolian Great Khan videos, of which there are 71, it missed a few.

There were less than two dozen video ids missing, which was enough that I could grab them from the video URLs rather than rewriting the script to be a bit smarter.
That said, I am going to have to do that anyway when I get to clearing out my Apex videos backlog, so it would have been time well-invested; but I wanted to get the Mongolia videos out so I could go out for a walk in the sun!
Vim Macros
This is something I’ve done on occasion before, and I had a macro recorded to strip out the id from a pasted URL. It went something like:
<80>kh/h^Mdt=<80>kD^[A"^[<80>a<80>kuA,<80>kd^[<80>a
Note that <80> is a specifier for a keyboard key, as far as I can tell. So it’s roughly:
“Hit <home>; jump to ‘h’ (in https); delete to =; hit another key…
to go from something like "id": "https://www.youtube.com/watch?v=dzJrSh_ByUw" to "id": "dzJrSh_ByUw".
Macros persist across sessions as they stored in the viminfo file. Unfortunately, I couldn’t remember to which register I had recorded the macro, so I had a look with :registers

"i highlightedYou can see some of the other macros I’ve been using. The eagle-eyed among you will also notice that the <80>s in that image on the "i line are in a different colour to other lines. That is because I accidentally hit qi (start recording new macro i) instead of @i (apply macro i). Whoops! I then wanted to recover the macro, and zeraphel in #vim gave me the tip of using let @i = (to assign to register i) or writing the text and "iy (into register i, yank the following text). But that doesn’t preserve the “this keyboard key” entries.
Hmm.
Writing Functions Instead
It was then suggested by zeraphel and romainl that I might consider writing a function instead, for things which are more than one-offs.
This is no tutorial on vimscript! I’ll leave that to Learn Vimscript the Hard Way, which is much better. This is a note of some of the features I used, as I ended up with a few tabs open while looking things up,

and this is more about how they are used and any gotchas than a reference guide.
Defining Functions
User functions need to be defined with a capital letter, because Reasons™.
Using Normal-mode Commands
Like you can use the command :normal (or :norm for short; insert-Cheers-reference-here), you can use that in vimscript. I [ab]used this to write macro-like functions, which though inelegant got the job done. For example norm A, to insert a comma at the end of the line; norm o"id": "TOREPLACE" to open a new line and insert a placeholder JSON key-value pair.
Checking For A String in Another String
You can get the position of a substring in another using stridx("string", "substring").
I used this as a sanity check:
function InsertYTID()
" Assumes YT URL is on clipboard (ie "+)
" Check for this:
let l:ytbegin = "https://www.youtube.com/watch?v="
let l:sane = stridx(@+, l:ytbegin)
stridx() returns -1 if the substring isn’t found within the string.
Clipboard Buffers
Some other things in the sanity check above:
l:prefix for local variables@+to access the contents of one of the two clipboard registers-@*accesses the one for selected text,@+for copy(+paste) clipboard.
A pitfall was that the vim I had installed from Arch was vim-minimal, which lacked X11 clipboard support. Installing the gvim package sorted that.
if syntax
vimscript has conditionals, though a potential pitfall is that -1 (and I assume other negative values) are truthy. Since that was the value I would get when the YouTube URL prefix was not present in the clipboard string, I tested for that explicitly:
if l:sane == -1
echom "You must have a YT url on the clipboard!"
elseif l:sane != -1
let ytid = ExtractYTID(@+)
which brings me to…
Function Arguments
To get at a function argument, you name it in the function definition (eg function MyFunction(argument1)) and get at it using a: prefix.
String Replacements / Substitutions
There is a substitution function, which I used to strip out the gubbins I didn’t need from the YT URL:
function ExtractYTID(url)
let out = substitute(a:url, "https://www.youtube.com/watch?v=", "", "")
return out
endfunction
it is of the form substitute({expr}, {pat}, {sub}, {flags}). See :help substitute:
substitute({expr}, {pat}, {sub}, {flags}) *substitute()*
The result is a String, which is a copy of {expr}, in which
the first match of {pat} is replaced with {sub}. This works
like the ":substitute" command (without any flags). But the
matching with {pat} is always done like the 'magic' option is
set and 'cpoptions' is empty (to make scripts portable).
'ignorecase' is still relevant. 'smartcase' is not used.
See |string-match| for how {pat} is used.
And a "~" in {sub} is not replaced with the previous {sub}.
Note that some codes in {sub} have a special meaning
|sub-replace-special|. For example, to replace something with
"\n" (two characters), use "\\\\n" or '\\n'.
When {pat} does not match in {expr}, {expr} is returned
unmodified.
When {flags} is "g", all matches of {pat} in {expr} are
replaced. Otherwise {flags} should be "".
Example: >
:let &path = substitute(&path, ",\\=[^,]*$", "", "")
This removes the last component of the 'path' option.
:echo substitute("testing", ".*", "\\U\\0", "")
results in "TESTING".
Lifted from this answer on SO.
Note that I didn’t have to assign the result to a variable, I could have returned directly: return substitute(...).
Calling Functions From Other Functions
Just use call:
function InsertYTID()
" Assumes YT URL is on clipboard (ie "+)
" Check for this:
let l:ytbegin = "https://www.youtube.com/watch?v="
let l:sane = stridx(@+, l:ytbegin)
if l:sane == -1
echom "You must have a YT url on the clipboard!"
elseif l:sane != -1
let ytid = ExtractYTID(@+)
call InsertIdToReplaceAfterScheduled()
"call YTInsertURLAndTrim()
" replace "" with the quote-wrapped YT id
s/TOREPLACE/\=ytid/
endif
endfunction
Timesaver: Map Function to Command
To save you (me) typing :call InsertYTID() repeatedly (modulo tab-completion), you can map a call to a command:
command InsertYTID call InsertYTID()
and then you can simply :InsertYTID, or whatever is enough to disambiguate it from another command. If you flipped the command around to be YTIDInsert, then :YTI may be sufficient!
