Vim: Creating parent directories on save

31,207

Solution 1

augroup BWCCreateDir
    autocmd!
    autocmd BufWritePre * if expand("<afile>")!~#'^\w\+:/' && !isdirectory(expand("%:h")) | execute "silent! !mkdir -p ".shellescape(expand('%:h'), 1) | redraw! | endif
augroup END

Note the conditions: expand("<afile>")!~#'^\w\+:/' will prevent vim from creating directories for files like ftp://* and !isdirectory will prevent expensive mkdir call.

Update: sligtly better solution that also checks for non-empty buftype and uses mkdir():

function s:MkNonExDir(file, buf)
    if empty(getbufvar(a:buf, '&buftype')) && a:file!~#'\v^\w+\:\/'
        let dir=fnamemodify(a:file, ':h')
        if !isdirectory(dir)
            call mkdir(dir, 'p')
        endif
    endif
endfunction
augroup BWCCreateDir
    autocmd!
    autocmd BufWritePre * :call s:MkNonExDir(expand('<afile>'), +expand('<abuf>'))
augroup END

Solution 2

Based on the suggestions to my question, here's what I ended up with:

function WriteCreatingDirs()
    execute ':silent !mkdir -p %:h'
    write
endfunction
command W call WriteCreatingDirs()

This defines the :W command. Ideally, I'd like to have all of :w!, :wq, :wq!, :wall etc work the same, but I'm not sure if it's possible without basically reimplementing them all with custom functions.

Solution 3

This code will prompt you to create the directory with :w, or just do it with :w!:

augroup vimrc-auto-mkdir
  autocmd!
  autocmd BufWritePre * call s:auto_mkdir(expand('<afile>:p:h'), v:cmdbang)
  function! s:auto_mkdir(dir, force)
    if !isdirectory(a:dir)
          \   && (a:force
          \       || input("'" . a:dir . "' does not exist. Create? [y/N]") =~? '^y\%[es]$')
      call mkdir(iconv(a:dir, &encoding, &termencoding), 'p')
    endif
  endfunction
augroup END

Solution 4

I added this to my ~/.vimrc

cnoremap mk. !mkdir -p <c-r>=expand("%:h")<cr>/

If I need to create the directory I'm in I type :mk. and it replaces that with "!mkdir -p /path/to/my/file/" and allows me to review the command before I invoke it.

Solution 5

I think I managed to do this in three lines, combining what others are saying on this answer.

This seems to do the trick:

if has("autocmd")
  autocmd BufWritePre * :silent !mkdir -p %:p:h
end

It attempts to create the folder automatically when saving a buffer. If anything bad happens (i.e. permission issues) it will just shut up and let the file write fail.

If anyone sees any obvious flaws, please post a comment. I'm not very versed in vimscript.

EDIT: Notes thanks to ZyX

  • This will not work if your folders have spaces on them (apparently they are not properly escaped or something)
  • Or if you are doing pseudo files.
  • Or if you are sourcing your vimrc.
  • But son, it is short.
Share:
31,207
Damien Pollet
Author by

Damien Pollet

Associate professor, researcher in reengineering and programming languages. Frequently used languages: Smalltalk (Pharo), Ruby, LaTeX…

Updated on March 18, 2020

Comments

  • Damien Pollet
    Damien Pollet over 4 years

    If I invoke vim foo/bar/somefile but foo/bar don't already exist, Vim refuses to save.

    I know I could switch to a shell or do :!mkdir foo/bar from Vim but I'm lazy :) Is there a way to make Vim do that automatically when it saves the buffer?

  • Damien Pollet
    Damien Pollet over 13 years
    Thanks, seems much cleaner than what I guess-hacked :)
  • Marius Gedminas
    Marius Gedminas almost 12 years
    call mkdir(expand('%:h'), 'p') might be more portable.
  • kikito
    kikito almost 12 years
    @MariusGedminas I'd like to see the complete code with that change. Could you please post it as an answer / upload it somewhere?
  • ZyX
    ZyX almost 12 years
    @kikito See my answer. It was edited a few hours after that comment.
  • kikito
    kikito almost 12 years
    @Zyx thanks! I ended up doing something a bit shorter (my answer is currently the last one) but it seems to do the trick nicely.
  • ZyX
    ZyX almost 12 years
    Never use % in such scripts. Vim is not going to escape any special symbols: for example, if you are editing a file named /mnt/windows/Documents and Settings/User/_vimrc you will end up having four new directories: /mnt/windows/Documents, ./and, ./Settings and ./Settings/User. And, by the way, you don’t need :execute here.
  • ZyX
    ZyX almost 12 years
    There is system() function for completely silent shell calls, but you don’t need both :execute and %:p:h: :silent !mkdir -p %:p:h works exactly as what you have wrote (though you may need :redraw! at the end, in this case :execute comes handy), but it is better to use call system('mkdir -p '.shellescape(expand('%:p:h'))). Do use :execute '!command' shellescape(arg, 1) (with the second argument to shellescape) if you have to use bangs instead of system(). Do use bangs if escaped argument contains newlines.
  • ZyX
    ZyX almost 12 years
    And you don’t avoid other troubles I am avoiding in my first code snippet: launching shell one additional time after every vimrc sourcing (supposing you pull in vimrc updates by doing :source ~/.vimrc) (this is what augroup and autocmd! are for), scrapped view after launching shell commands (that is what redraw! is for), creating garbage directories in case of using pseudo-files (in first code snipped it is checked by only matching filename against a pattern, but in second one I also check &buftype) and useless shell call in case directory exists (isdirectory() condition).
  • kikito
    kikito almost 12 years
    @ZyX Thanks for your feedback. I don't want to solve problems I don't have. I never use special chars (i.e. spaces) on my folders, so %:p:h does just fine for me. I never source vimrc (I kill and reopen vim instead) and I don't even know what pseudofiles are. redraw! doesn't seem to do anything at all to me. But I like your suggestion of removing execute to make everything shorter. Cheers!
  • ZyX
    ZyX almost 12 years
    It does not matter whether or not you do have special characters, it is the thing you should care about. There are too much problems with % expansion to ever suggest using it to anybody. Pseudo files are used in a big bunch of plugins (e.g. fugitive or my aurum), thus they are worth caring about. Resourcing vimrc is also a common practice. You can have whatever you want in the vimrc, just do not suggest it as an answer. Using :silent! call mkdir(expand('%:p:h'), 'p') variant solves two of the points I mentioned and third I did not mention: !mkdir is not going to work on windows.
  • ZyX
    ZyX almost 12 years
    redraw! will make difference if you are using terminal vim, it is blanking the screen otherwise. OP almost definitely has terminal vim: command in the question is vim ….
  • kikito
    kikito almost 12 years
    Me too. Still no difference. Different systems, I guess (I'm on a Mac)
  • ZyX
    ZyX almost 12 years
    I would say this is something related to alternate screens and how terminal handles them: when you launch a shell command vim switches to the first of the screens (non-alternate), terminal shows it (blanking almost everything), vim runs command, then switches back to alternate screen. But, unfortunately, terminal does not remember the contents of the screen (unlike previous sentence, this is only my guess). Or remembers, in your case. I am on linux though.
  • Mu Mind
    Mu Mind almost 11 years
    Watch out! Vim has a bug with trailing slashes in mkdir('foo/', 'p') where it creates the directory and then errors out that it failed to create it: groups.google.com/d/topic/vim_dev/rFT67RzKMfU/discussion. Make sure you strip any trailing slashes before passing to mkdir().
  • ZyX
    ZyX almost 11 years
    @MuMind It is impossible to have trailing slashes after using fnamemodify(, ':h'): :h is “Head of the file name (the last component and any separators removed).” The only exception is / (root), but I doubt that isdirectory('/') may return anything but 1.
  • iamnewton
    iamnewton over 9 years
    @ZyX, using your solution works beautifully, but i get an error, which appears to be due to a plugin I have. Can you shed light on it so I can either open a bug with the creator or fix myself?
  • iamnewton
    iamnewton over 9 years
    Failed to execute "python /Users/cnewton/.vim/bundle/editorconfig-vim/plugin/editorcon‌​fig-core-py/main.py 'test/foo/bar/test.md'". Exit code: 1 Message: ['Traceback (most recent call last):', ' File "/Users/cnewton/.vim/bundle/editorconfig-vim/plugin/editorco‌​nfig-core-py/main.py‌​", line 8, in <module>', ' main()', ' File "/usr/local/opt/dotfiles/vim/bundle/editorconfig-vim/plugin/‌​editorconfig-core-py‌​/editorconfig/main.p‌​y", line 73, in main', ' print(str( e))', 'UnboundLocalError: local variable ''e'' referenced before assignment']
  • ZyX
    ZyX over 9 years
    You should direct this bug to editorconfig-vim bug tracker. I do not suggest using plugins like Vundle without actually understanding what they do: /Users/cnewton/.vim/bundle/… is clearly a package manager path where gives you enough information about the plugin in which error occurred. It looks like they have forgot to remove a debugging print or to add as e.
  • moebius_eye
    moebius_eye almost 8 years
    I've tried this same command and everytime I use :W, my screen becomes almost blank. I'll try and remove my previous options and give feedback.
  • Tri Nguyen
    Tri Nguyen over 7 years
    Not sure what I'm doing wrong, but using the code provided does not work for me (same E212 error). if anyone has any suggestion, would appreciate it.
  • ZyX
    ZyX over 7 years
    @TriNguyen How did you use it? If “put it into the vimrc”, did you restart Vim afterwards?
  • Tri Nguyen
    Tri Nguyen over 7 years
    @ZyX yes, I did both of those things.
  • Tom Hale
    Tom Hale over 7 years
    The issue with this is that this will always create the leading directories without asking, which may may be unwanted in the case of a typo when you know the directory should exist. See my answer for a solution.
  • Stephen Crosby
    Stephen Crosby over 5 years
    I'd recommend using function! instead of function so that your .vimrc file can be reloaded. I'd edit the answer to add it, but I don't have enough privileges on SO yet.
  • superhawk610
    superhawk610 about 3 years
    Changing write to execute ':write' fixed it for me.