The Problem
I'm writing a lot of Ansible these days (not loving the language, but that's a rant for another day). An Ansible playbook is commonly spread across dozens, and possibly hundreds of files. Ansible's file structures are complex and annoying, and moving around in them in (Neo)Vim can be a pain. Today I wrote a minor extension for Vim that allows you to open a (Netrw|NerdTree) view on a role folder by typing a key combination on the playbook line that specifies the role. This is far from a complete solution: it does exactly what it says, but it turns out to be even less use than expected because there are so many other types of file linkages in Ansible. Consider it more a proof of concept, or a base for further work.
The Solution
Here's a piece of an Ansible playbook. If you're on either of the role: lines below, pressing "gf" will open the role folder:
- name: set up nginx
hosts: acctdev
roles:
- role: common
- { role: nginx, tags: sshkeys, env: dev }
Here's how I did it:
" file: ~/.vim/ftplugin/yaml.vim
function! AnsibleGoRole()
if getline('.') =~ 'role:'
silent normal "xY
let @x=substitute(@x, '.*\(\<role\>:\_s*\)\(\<\w\+\>\).*', '\2', '')
let roledir=fnamemodify("roles/" . @x . "/.", ":p")
silent execute ':tabnew '. roledir
else
echom "Couldn't find a role to go to."
endif
endfunction
nnoremap <buffer> <silent> gf :call AnsibleGoRole()<CR>
The Pieces
if getline('.') =~ 'role:'
- here, we check to see if the line actually has a reference to a "role:" in it: otherwise, let them know a role hasn't been detected.silent normal "xY
- let's start with"xY
, which means "select register 'x', Yank this line into it."silent normal
means "run it in normal mode, but don't echo any messages." This becomes important because this command would normally echo "1 line yanked" and then, when another action further along in the function echoes another message, Vim sees you have two lines of messages and insists you hit a key to continue. Very annoying.let @x=substitute(@x, '.*\(\<role\>:\_s*\)\(\<\w\+\>\).*', '\2', '')
- this was by far the worst line to sort out: I spent a lot of time reading about Vim regexes and typing:"xY
followed by:let @x=substitute(@x, 'role:', '', '')
, then:"xY
and:let @x=substitute(@x, '.*\<role\>:_s*', '', '')
etc. until I got it right. Who knew_s*
was whitespace in Vim?let roledir=fnamemodify("roles/" . @x . "/.", ":p")
- a simpler call to fnamemodify would be:echo fnamemodify("bob", ":p")
. If you run that as an ex command, you'll find it takes the directory of the current file and appends "bob" on the end. So this line assigns a value to roledir of the form /full/path/to/file/roles/<role_name>/.silent execute ':tabnew '. roledir
- now we're just (silently - no echoed messages) opening a new tab on the roledir folder - if you prefer buffers to tabs, substitute :e for :tabnewnnoremap <buffer> <silent> gf :call AnsibleGoRole()<CR>
- assign our new function to a key combo. It'll only be available in normal mode, don't allow remapping (never, ever allow remapping), only assign it for the current buffer, and be silent (it liked to complain that I was executing against a directory rather than a file: a valid complaint, but it was intentional).
The Caveats
- "gf" overwrites an existing Vim mapping. Type
:help gf
to see: it's meant to be "goto file" but doesn't work in Ansible, so this ... partly ... fixes it - the regex could be better
- this only works in top-level playbooks: as soon as you're in a task file, it's no use
- role: declarations can be spread across multiple lines: this would immediately cease to work
- I wrote it today: it's barely tested
- I wrote it to work with my own Ansible workflow, it may be totally unsuitable for you
- it should check for existence of the folder - and potentially, if it doesn't exist, offer to create it
- 2016-07-09: I almost immediately found a piece of code that causes it to fail:
- name: install common packages
hosts: router
remote_user: root
roles:
- openwire
- this is the problem with Ansible code from a Vim perspective: it's flexible enough that parsing it requires masses of exceptions and back-checks, which makes it hard to write
Next
Next up: opening a template: file. For example:
- name: add site configuration
template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf mode=0644
notify: nginx restart
In this form, it shouldn't be too difficult. We have both template: and src= as pointers, and a confirmation in the form of the .j2 file extension. But what if the person had a lot of parameters, and spread them over several lines? Trickier than the one for roles.