Vim: Creating parent directories on save
Asked Answered
D

8

136

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?

Deceptive answered 27/11, 2010 at 16:54 Comment(3)
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.Tympanist
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... SorryBallistics
I guess I could combine both your suggestions and alias :w to mkdir -p %:h followed by the builting :writeDeceptive
G
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
Gough answered 27/11, 2010 at 22:8 Comment(15)
Thanks, seems much cleaner than what I guess-hacked :)Deceptive
call mkdir(expand('%:h'), 'p') might be more portable.Tune
@MariusGedminas I'd like to see the complete code with that change. Could you please post it as an answer / upload it somewhere?Grabble
@Grabble See my answer. It was edited a few hours after that comment.Gough
@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.Grabble
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().Training
@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.Gough
@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?Hagan
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 <module>', ' 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']Hagan
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.Gough
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.Beanstalk
@TriNguyen How did you use it? If “put it into the vimrc”, did you restart Vim afterwards?Gough
@Gough yes, I did both of those things.Beanstalk
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.Katiakatie
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.Infliction
D
25

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.

Deceptive answered 27/11, 2010 at 19:44 Comment(3)
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.Brazilin
Changing write to execute ':write' fixed it for me.Kienan
I had to replace the execute line with the following two to fix all issues: let mkdircommand="mkdir -p '" . expand("%:h") . "'" then execute 'call system(mkdircommand)'. This way the screen also doesn't flash to execute the mkdir command. And it should also work with directories with spacesSculley
K
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
Katiakatie answered 18/3, 2017 at 8:48 Comment(0)
M
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.

Maenad answered 31/7, 2011 at 1:30 Comment(0)
C
7

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
Control answered 6/2, 2023 at 19:17 Comment(0)
H
4

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

Hendricks answered 23/2, 2012 at 20:10 Comment(0)
G
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.
Grabble answered 26/9, 2012 at 15:57 Comment(8)
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.Gough
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.Gough
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).Gough
@Gough 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!Grabble
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.Gough
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 ….Gough
Me too. Still no difference. Different systems, I guess (I'm on a Mac)Grabble
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.Gough
G
0

You can install a plugin for this, such as https://github.com/DataWraith/auto_mkdir

To use vim's built-in plugin management, clone it like so:

git clone https://github.com/DataWraith/auto_mkdir ~/.vim/pack/vendor/start/auto_mkdir

and it'll be active the next time you open vim.

Gatlin answered 30/9, 2023 at 23:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.