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
You 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!