ctag support for Terraform

Anyone working in the land of systems and infrastructure automation these days will no doubt have heard of Terraform. At work, we use it for pretty much everything, and being a long time vim user and fan, I wanted to work a little bit smarter as I spend a lot of time writing Terraform, Ansible, Shell scripts, Makefiles and Markdown documentation. Having never used ctags before, but knowing what they were, I decided to see if and how I could start using them to my advantage.

Here's what I came up with for the ctags configuration:

» cat ~/.ctags
--recurse
--exclude=.git
--exclude=.terraform
--regex-make=/^([^# \t]*):/\1/t,target/
--langdef=ansible
--langmap=ansible:.yml.yaml
--regex-ansible=/^[ \t]*-[ \t]*name:[ \t]*(.+)/\1/t,tasks/
--regex-ansible=/^[ \t]*-[ \t]*hosts:[ \t]*(.+)/\1/h,hosts/
--langdef=terraform
--langmap=terraform:.tf.tfvars
--regex-terraform=/^resource "(.+)" "(.+)"/\1 \2/r,resources/
--regex-terraform=/^variable "(.+)"/\1/v,variables/
--regex-terraform=/^module "(.+)"/\1/m,modules/
--regex-terraform=/^output "(.+)"/\1/o,outputs/
--regex-terraform=/^([a-z0-9_]+) =/\1/f,tfvars/
--languages=ansible,python,sh,terraform,make
--langdef=markdown
--langmap=markdown:.md
--regex-markdown=/^#[ \t]+(.*)/\1/h,Heading_L1/
--regex-markdown=/^##[ \t]+(.*)/\1/i,Heading_L2/
--regex-markdown=/^###[ \t]+(.*)/\1/k,Heading_L3/
--regex-sh=/readonly (.*)=.*/\1/c,constants/

As you can probably see, setting this up just requires reading the documentation and working out the correct regex for each language.

Next, I just needed to run ctags in the root of my repo to generate the tags file. This is just a plain text file you can look at e.g.:

» grep ^aws_instance tags
aws_instance bastion       terraform/layers/support/modules/bastion/bastion.tf     /^resource "aws_instance" "bastion" {$/;"          r  
aws_instance cm-controlnode        terraform/layers/cloudera/modules/cm-controlnode/cm-controlnode.tf      /^resource "aws_instance" "cm-controlnode" {$/;"           r  
aws_instance cm-datanode           terraform/layers/cloudera/modules/cm-datanode/cm-datanode.tf    /^resource "aws_instance" "cm-datanode" {$/;"      r  
aws_instance cm-edgenode           terraform/layers/cloudera/modules/cm-edgenode/cm-edgenode.tf    /^resource "aws_instance" "cm-edgenode" {$/;"      r  

I then wanted to know how to use these, and a quick google pointed me at the majutsushi/tagbar plugin. After reading the docs and a some testing I came up with the following vim configuration:

" CtrlP
nnoremap <silent> <leader>o :CtrlP<CR>  
nnoremap <silent> <leader>t :CtrlPTag<cr>  
nnoremap <silent> <leader>b :CtrlPBuffer<cr>  
nnoremap <silent> <leader>l :CtrlPLine<cr>  
nnoremap <silent> <leader>b :TagbarToggle<CR>  
nnoremap <silent> ; :CtrlPBuffer<CR>

" ctags/tagbar
nnoremap <leader>f :ta<space>

" Auto open the TagBar when file is supported
autocmd FileType * nested :call tagbar#autoopen(0)

let g:tagbar_compact = 1

let g:tagbar_type_ansible = {  
    \ 'ctagstype' : 'ansible',
    \ 'kinds' : [
    \ 't:tasks',
    \ 'h:hosts'
    \ ],
    \ 'sort' : 0
    \ }

let g:tagbar_type_terraform = {  
    \ 'ctagstype' : 'terraform',
    \ 'kinds' : [
    \ 'r:resources',
    \ 'm:modules',
    \ 'o:outputs',
    \ 'v:variables',
    \ 'f:tfvars'
    \ ],
    \ 'sort' : 0
    \ }

let g:tagbar_type_make = {  
    \ 'kinds':[
    \ 'm:macros',
    \ 't:targets'
    \ ]
    \}

let g:tagbar_type_sh = {  
    \ 'kinds':[
    \ 'f:functions',
    \ 'c:constants'
    \ ]
    \}

let g:tagbar_type_markdown = {  
    \ 'ctagstype' : 'markdown',
    \ 'kinds' : [
    \ 'h:Heading_L1',
    \ 'i:Heading_L2',
    \ 'k:Heading_L3'
    \ ]
    \ }

With the above configuration, I can use the CtrlP plugin with <leader>t to fuzzy find tags, and <leader>b to toggle the tagbar on the right open and closed. With the configuration above, the tagbar will open when a supported file is opened. Also, I can use the ta vim command to jump to a tag. For example, to go to my bastion instance resource, I can use:

:ta aws_instance bast<tab>

or

<leader>f aws_instance bast<tab>

To fuzzy search the tags, I use:

<leader>t aws_inst bast

To find the bastion host instance resource.

The end result:

Terrform ctags in vim

Here's a quick demo:

asciicast

As an additional bonus, if you have the hashivim/vim-terraform plugin you can have automatically run terraform fmt on the file you are working on when you save it by adding the following to your ~/.vimrc:

let g:terraform_fmt_on_save = 1

I'm sure this isn't the final configuration I'll use, but as always, my dotfiles can be viewed on GitHub.

If you have any comments or suggestions on how I can improve this, please shout at me on twitter: @z0mbix