134

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
  • 6,488
  • 3
  • 27
  • 28
  • 14
    `mkdir -p %:h` is better because it works for nested non-existing paths, doesn’t raise an error when the path already exists, and `%:h` is the full path of the current file. However, I don’t know how to invoke this automatically. Normally, this is done with automcommands but the `BufWritePre` event doesn’t seem to work here. – Konrad Rudolph Nov 27 '10 at 17:11
  • Define a function which checks if the file exists and calls the builtin `write` and calls the system to `mkdir -p` on `dirname` otherwise, map it to `W`... I'm too lazy to search for the syntax and to post it as an answer... Sorry – khachik Nov 27 '10 at 17:16
  • 1
    I guess I could combine both your suggestions and alias `:w` to `mkdir -p %:h` followed by the builting `:write` – Damien Pollet Nov 27 '10 at 17:47

7 Answers7

97
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
ZyX
  • 52,536
  • 7
  • 114
  • 135
  • 1
    Thanks, seems much cleaner than what I guess-hacked :) – Damien Pollet Nov 28 '10 at 08:19
  • 11
    `call mkdir(expand('%:h'), 'p')` might be more portable. – Marius Gedminas Aug 21 '12 at 12:19
  • 1
    @MariusGedminas I'd like to see the complete code with that change. Could you please post it as an answer / upload it somewhere? – kikito Sep 26 '12 at 10:07
  • @kikito See my answer. It was edited a few hours after that comment. – ZyX Sep 26 '12 at 15:55
  • @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. – kikito Sep 28 '12 at 08:21
  • 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: https://groups.google.com/d/topic/vim_dev/rFT67RzKMfU/discussion. Make sure you strip any trailing slashes before passing to mkdir(). – Mu Mind Aug 21 '13 at 01:23
  • @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. – ZyX Aug 25 '13 at 14:03
  • @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 Dec 12 '14 at 04:23
  • Failed to execute "python /Users/cnewton/.vim/bundle/editorconfig-vim/plugin/editorconfig-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/editorconfig-core-py/main.py", line 8, in ', ' main()', ' File "/usr/local/opt/dotfiles/vim/bundle/editorconfig-vim/plugin/editorconfig-core-py/editorconfig/main.py", line 73, in main', ' print(str( e))', 'UnboundLocalError: local variable ''e'' referenced before assignment'] – iamnewton Dec 12 '14 at 04:23
  • 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`. – ZyX Dec 18 '14 at 21:48
  • 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. – Tri Nguyen Dec 08 '16 at 22:22
  • @TriNguyen How did you use it? If “put it into the vimrc”, did you restart Vim afterwards? – ZyX Dec 09 '16 at 16:11
  • @ZyX yes, I did both of those things. – Tri Nguyen Dec 13 '16 at 19:53
  • 1
    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](http://stackoverflow.com/a/42872275/5353461) for a solution. – Tom Hale Mar 18 '17 at 08:51
  • 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. – Stephen Crosby Oct 30 '18 at 16:51
24

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.

Damien Pollet
  • 6,488
  • 3
  • 27
  • 28
9

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
Tom Hale
  • 40,825
  • 36
  • 187
  • 242
7

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.

Asa Ayers
  • 4,854
  • 6
  • 40
  • 57
4

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.
kikito
  • 51,734
  • 32
  • 149
  • 189
  • 1
    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 Sep 28 '12 at 12:33
  • 2
    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 Sep 28 '12 at 12:38
  • 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). – ZyX Sep 28 '12 at 12:44
  • 1
    @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! – kikito Sep 28 '12 at 16:16
  • 1
    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 Sep 28 '12 at 16:27
  • `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 …`. – ZyX Sep 28 '12 at 16:42
  • Me too. Still no difference. Different systems, I guess (I'm on a Mac) – kikito Sep 28 '12 at 18:31
  • 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. – ZyX Sep 28 '12 at 21:48
4

I made :saveas! create the directory if missing: https://github.com/henrik/dotfiles/commit/54cc9474b345332cf54cf25b51ddb8a9bd00a0bb

Henrik N
  • 15,786
  • 5
  • 82
  • 131
2

I am failing to see why everyone tries complicated functions. This is enough to create parent folders

:!mkdir -p %:p:h
  • mkdir -p is the shell command to create folders with parents
  • %:p:h is the folder path with
    • % : path given when vim starts: vim foo/bar/file.ext
    • :p : gives full path: /home/user/foo/bar/file.ext
    • :h : removes file name from final string: /home/user/foo/bar
  • %:h also works and gives relative path: foo/bar
Yılmaz Durmaz
  • 2,374
  • 12
  • 26