Is there a way to make mv create the directory to be moved to if it doesn't exist?
Asked Answered
H

21

402

So, if I'm in my home directory and I want to move foo.c to ~/bar/baz/foo.c , but those directories don't exist, is there some way to have those directories automatically created, so that you would only have to type

mv foo.c ~/bar/baz/ 

and everything would work out? It seems like you could alias mv to a simple bash script that would check if those directories existed and if not would call mkdir and then mv, but I thought I'd check to see if anyone had a better idea.

Harrier answered 13/2, 2009 at 21:15 Comment(1)
Related: #1530446Euniceeunuch
P
401

How about this one-liner (in bash):

mkdir --parents ./some/path/; mv yourfile.txt $_

Breaking that down:

mkdir --parents ./some/path
# if it doesn't work; try
mkdir -p ./some/path

creates the directory (including all intermediate directories), after which:

mv yourfile.txt $_

moves the file to that directory ($_ expands to the last argument passed to the previous shell command, ie: the newly created directory).

I am not sure how far this will work in other shells, but it might give you some ideas about what to look for.

Here is an example using this technique:

$ > ls
$ > touch yourfile.txt
$ > ls
yourfile.txt
$ > mkdir --parents ./some/path/; mv yourfile.txt $_
$ > ls -F
some/
$ > ls some/path/
yourfile.txt
Physique answered 13/2, 2009 at 22:22 Comment(6)
Why the obsession with redundant ./ everywhere? The args are not commands in PATH without the . directory...Ethnomusicology
Oddly, --parents is not available on macOS 10.13.2. You need to use -p.Overlap
Doesn't work if moving a file eg ./some/path/foo. In this case the command should be: mkdir -p ./some/path/foo; mv yourfile.txt $_:hSoupspoon
That is not an one-liner.Lester
mkdir -p /path/mydirectory && mv myfile $_ This is, and it's better, since it'll only move if the mkdir is valid.Misfit
@Misfit Indeed. Abdoul Seibou's answer has it and could do would more love.Esurient
S
83
mkdir -p `dirname /destination/moved_file_name.txt`  
mv /full/path/the/file.txt  /destination/moved_file_name.txt
Squalor answered 21/6, 2012 at 19:19 Comment(4)
I am the only one to realize this code makes no sense? You create recursively the absolute path to the existing file (which means this path already exist) and then move the file to a location (which in your example doesn't exist).Hindquarter
@user544262772 GNU coreutils' dirname doesn't require its argument to exist, it's pure string manipulation. Can't speak for other distributions. Nothing wrong with this code afaict.Eyecatching
This is the answer that is most easily automated, I think. for f in *.txt; do mkdir -p `dirname /destination/${f//regex/repl}`; mv "$f" "/destination/${f//regex/repl}; doneCult
@Hindquarter - note that BSD (and therefore Macos) also perform dirname as string manipulation and do not check for file/path existence.Misfit
D
31

Save as a script named mv.sh

#!/bin/bash
# mv.sh
dir="$2" # Include a / at the end to indicate directory (not filename)
tmp="$2"; tmp="${tmp: -1}"
[ "$tmp" != "/" ] && dir="$(dirname "$2")"
[ -a "$dir" ] ||
mkdir -p "$dir" &&
mv "$@"

Or put at the end of your ~/.bashrc file as a function that replaces the default mv on every new terminal. Using a function allows bash keep it memory, instead of having to read a script file every time.

function mvp ()
{
    dir="$2" # Include a / at the end to indicate directory (not filename)
    tmp="$2"; tmp="${tmp: -1}"
    [ "$tmp" != "/" ] && dir="$(dirname "$2")"
    [ -a "$dir" ] ||
    mkdir -p "$dir" &&
    mv "$@"
}

Example usage:

mv.sh file ~/Download/some/new/path/ # <-End with slash

These based on the submission of Chris Lutz.

Dowager answered 26/4, 2012 at 3:54 Comment(9)
This answers the question most accurately IMHO. The user would like an enhanced mv command. Especially useful for scripting, ie you don't necessarily want to run the checks and run mkdir -p anytime you need to use mv. But since I would want the default error behavior for mv, I changed the function name to mvp -- so that I know when I could be creating directories.Gissing
Seems like the best solution idea, butimplementation crashes a lot.Meantime
@UlisesLayera I modified the algorithm to make it more robust. It should not crash nowDowager
Could someone please explain the meaning of [ ! -a "$dir" ] I have conducted experiments with the right half being true and false, both evaluated to true.. ??? For other's sake, tmp="${tmp: -1}" appears to grab the last character of the file to make sure it's not a path (/)Hadden
@Hadden It should be doing "if $dir does not exist, then continue". Unfortunately, you are correct, there is a bug when using "! -a", and I don't know why. I have edited the code a bit, and should now work.Dowager
The function crashes my shell after spamming something about !=: Unary operator expectedDipteran
If anyone is getting the error [: too many arguments, convert all all single square brackets to double square brackets. I'm too lazy to find out why that's happening...Sizing
this is exactly what i was looking for!Yentai
Relying on a custom .sh script with poor readability is not ideal if one wants to understand what is happening under the hood, or is onboarding onto a projectZinck
B
16

You can use mkdir:

mkdir -p ~/bar/baz/ && \
mv foo.c ~/bar/baz/

A simple script to do it automatically (untested):

#!/bin/sh

# Grab the last argument (argument number $#)    
eval LAST_ARG=\$$#

# Strip the filename (if it exists) from the destination, getting the directory
DIR_NAME=`echo $2 | sed -e 's_/[^/]*$__'`

# Move to the directory, making the directory if necessary
mkdir -p "$DIR_NAME" || exit
mv "$@"
Brigitta answered 13/2, 2009 at 21:18 Comment(7)
When I run "$ dirname ~?bar/baz/", I get "/home/dmckee/bar", which is not what yo want here...Stereotomy
@dmckee, Ah, you are right. Any idea on how to solve this? If you input ~/bar/baz you either (a) want to copy to ~/bar/ and rename to baz, or (b) copy to ~/bar/baz/. What's the better tool for the job?Brigitta
@dmckee, I've used regexp/sed to come up with a solution. Does it work to your liking? =]Brigitta
Nice, except $2 is not the last argument unless $# = 2. I have a program, la, that prints its last argument. I used to use it in a version of the cp command (to add the current directory if the last argument wasn't a directory). Even modern shells support $1..$9 only; a Perl script may be better.Zen
@Leffler, Not true -- I know bash supports more than 9 arguments (using ${123} is one method). I don't know Perl, so feel free to make an answer yourself. =]Brigitta
And even if you're using a plain vanilla /bin/sh, you can loop over 'store=${store} " " $1; shift;' until only one argument remains... Though at that point we've lost any semblance of elegance the script might have had.Stereotomy
dmckee, I was going to try shift, but then found a more elegent solution. See update.Brigitta
H
13

The simplest way to do it is:

mkdir -p [path/to/directory] && mv [filename] $_

The nice part about this is it will only move the file if mkdir succeeds.


Explanation:

Let's suppose I downloaded some PDF files located in my directory (~/download ). I want to move all of them into a directory that doesn't exist (let's say my_PDF).

I'll type the following command (making sure my current working directory is ~/download):

mkdir my_PDF && mv *.pdf $_

You can add -p option to mkdir if you want to create subdirectories just like this: (supposed I want to create a subdirectory named python):

mkdir -p my_PDF/python && mv *.pdf $_
Hanover answered 13/2, 2009 at 21:16 Comment(1)
The && condition, making it succeed only if the dir exists, is the crucial point. This is way better (also actually a oneliner) than the accepted answer which just separates the two commands by a ;. I consider this an upgrade. Abdoul Seibou, thanks for posting this way back in 2017.Esurient
V
13

It sounds like the answer is no :). I don't really want to create an alias or func just to do this, often because it's one-off and I'm already in the middle of typing the mv command, but I found something that works well for that:

mv *.sh shell_files/also_with_subdir/ || mkdir -p $_

If mv fails (dir does not exist), it will make the directory (which is the last argument to the previous command, so $_ has it). So just run this command, then up to re-run it, and this time mv should succeed.

Vortical answered 14/3, 2016 at 19:12 Comment(2)
Ctrl + a is your friend. "I'm already in the middle of typing the mv command" so hit C-a and type.Misfit
It's a good one-liner, better than the accepted answer. I prefer inverting it and using &&, like so mkdir -p shell_files/also_with_subdir/ && mv *.sh $_. Abdoul Seibou's answer has it.Esurient
R
7

Making use of the tricks in "Getting the last argument passed to a shell script" we can make a simple shell function that should work no matter how many files you want to move:

# Bash only
mvdir() { mkdir -p "${@: -1}" && mv "$@"; }

# Other shells may need to search for the last argument
mvdir() { for last; do true; done; mkdir -p "$last" && mv "$@"; }

Use the command like this:

mvdir foo.c foo.h ~/some/new/folder/
Rennin answered 3/10, 2020 at 19:8 Comment(0)
O
5

rsync command can do the trick only if the last directory in the destination path doesn't exist, e.g. for the destination path of ~/bar/baz/ if bar exists but baz doesn't, then the following command can be used:

rsync -av --remove-source-files foo.c ~/bar/baz/

-a, --archive               archive mode; equals -rlptgoD (no -H,-A,-X)
-v, --verbose               increase verbosity
--remove-source-files   sender removes synchronized files (non-dir)

In this case baz directory will be created if it doesn't exist. But if both bar and baz don't exist rsync will fail:

sending incremental file list
rsync: mkdir "/root/bar/baz" failed: No such file or directory (2)
rsync error: error in file IO (code 11) at main.c(657) [Receiver=3.1.2]

So basically it should be safe to use rsync -av --remove-source-files as an alias for mv.

Osseous answered 27/2, 2019 at 6:58 Comment(0)
S
3

The following shell script, perhaps?

#!/bin/sh
if [[ -e $1 ]]
then
  if [[ ! -d $2 ]]
  then
    mkdir --parents $2
  fi
fi
mv $1 $2

That's the basic part. You might want to add in a bit to check for arguments, and you may want the behavior to change if the destination exists, or the source directory exists, or doesn't exist (i.e. don't overwrite something that doesn't exist).

Susurrous answered 13/2, 2009 at 21:21 Comment(11)
With your code, the move isn't performed if the directory does not exist!Brigitta
And you missed the "-p" flag to mkdir.Stereotomy
If a directory doesn't exist, why create it if you're just going to move it? (-p flag fixed)Susurrous
I mean, when you run the script and the directory $2 does not exist, it is created but the file is not copied.Brigitta
he gave a simple construct and added information on how it should be expanded. Why vote it down?!Justis
Closer. But why are you making the construction of the target directory conditional on the non-existence of the file to be moved?Stereotomy
@ypnos: Because so far it doesn't do what it is supposed to?Stereotomy
And I haven't fixed it myself for pedagogical reasons.Stereotomy
The logic here: No point in try the move unless $1 exists, then if the target does not exist, make it. Might want to add a test so we don't overwrite an existing regular file named '~/bar/baz'.Stereotomy
ok, so at first I didn't understand why Chris Lutz checks $1, since $2 is the directory to be created. But Chris Lutz is absolutely right, if $1 does not exist mv will raise an error and therefore one may also not want the destination directory to be created. In the second if-condition one may not only test for existance of type directory but for existance regardless of type. Since when it exists and it is a file makedir will cause an error even with the -p option (btw. which it will not do for an existing directory with -p option on archlinux). So I suggest to test for existance regardless ofBiddable
type. And finally @dmckee's last point: If one wants this function/script to behave like the standard mv apart from creating nested directories, than one shall not add a test whether we are copying a file or a directory. But that may be different depending on one's needs. So if you dmckee meant this as optional we both agree ;).Biddable
E
3

Code:

if [[ -e $1 && ! -e $2 ]]; then
   mkdir --parents --verbose -- "$(dirname -- "$2")"
fi
mv --verbose -- "$1" "$2"

Example:

arguments: "d1" "d2/sub"

mkdir: created directory 'd2'
renamed 'd1' -> 'd2/sub'
Embower answered 30/6, 2018 at 12:45 Comment(0)
T
2

Sillier, but working way:

mkdir -p $2
rmdir $2
mv $1 $2

Make the directory with mkdir -p including a temporary directory that is shares the destination file name, then remove that file name directory with a simple rmdir, then move your file to its new destination. I think answer using dirname is probably the best though.

Torres answered 1/7, 2014 at 11:46 Comment(2)
Yepp, kinda silly.:-) If $2 is a directory (as in the original question) then it fails miserably, as the last directory will be deleted, so 'mv' will fail. And if $2 already exists then it also tries to delete it.Vientiane
Thanks! This is exactly what I needed to accomplish: mv ./old ./subdir/new where subdir doesn't yet existLowther
A
2

This will move foo.c to the new directory baz with the parent directory bar.

mv foo.c `mkdir -p ~/bar/baz/ && echo $_`

The -p option to mkdir will create intermediate directories as required.
Without -p all directories in the path prefix must already exist.

Everything inside backticks `` is executed and the output is returned in-line as part of your command.
Since mkdir doesn't return anything, only the output of echo $_ will be added to the command.

$_ references the last argument to the previously executed command.
In this case, it will return the path to your new directory (~/bar/baz/) passed to the mkdir command.


I unzipped an archive without giving a destination and wanted to move all the files except `demo-app.zip` from my current directory to a new directory called `demo-app`.
The following line does the trick:
mv `ls -A | grep -v demo-app.zip` `mkdir -p demo-app && echo $_`

ls -A returns all file names including hidden files (except for the implicit . and ..).

The pipe symbol | is used to pipe the output of the ls command to grep (a command-line, plain-text search utility).
The -v flag directs grep to find and return all file names excluding demo-app.zip.
That list of files is added to our command-line as source arguments to the move command mv. The target argument is the path to the new directory passed to mkdir referenced using $_ and output using echo.

Age answered 20/5, 2020 at 22:24 Comment(0)
D
2

Based on a comment in another answer, here's my shell function.

# mvp = move + create parents
function mvp () {
    source="$1"
    target="$2"
    target_dir="$(dirname "$target")"
    mkdir --parents $target_dir; mv $source $target
}

Include this in .bashrc or similar so you can use it everywhere.

Demakis answered 27/5, 2020 at 3:42 Comment(0)
Z
2

i accomplished this with the install command on linux:

root@logstash:# myfile=bash_history.log.2021-02-04.gz ; install -v -p -D $myfile /tmp/a/b/$myfile

bash_history.log.2021-02-04.gz -> /tmp/a/b/bash_history.log.2021-02-04.gz

the only downside being the file permissions are changed:

root@logstash:# ls -lh /tmp/a/b/

-rwxr-xr-x 1 root root 914 Fev  4 09:11 bash_history.log.2021-02-04.gz

if you dont mind resetting the permission, you can use:

-g, --group=GROUP   set group ownership, instead of process' current group
-m, --mode=MODE     set permission mode (as in chmod), instead of rwxr-xr-x
-o, --owner=OWNER   set ownership (super-user only)
Zippy answered 4/2, 2021 at 16:2 Comment(0)
E
1
((cd src-path && tar --remove-files -cf - files-to-move) | ( cd dst-path && tar -xf -))
Electrodynamometer answered 17/4, 2019 at 7:43 Comment(0)
S
1

I frequently stumble upon this issue while bulk moving files to new subdirectories. Ideally, I want to do this:

mv * newdir/  

Most of the answers in this thread propose to mkdir and then mv, but this results in:

mkdir newdir && mv * newdir 
mv: cannot move 'newdir/' to a subdirectory of itself

The problem I face is slightly different in that I want to blanket move everything, and, if I create the new directory before moving then it also tries to move the new directory to itself. So, I work around this by using the parent directory:

mkdir ../newdir && mv * ../newdir && mv ../newdir .

Caveats: Does not work in the root folder (/).

Syblesybley answered 2/11, 2020 at 13:4 Comment(1)
This answer is more of a note to anyone stumbling upon a specific sub-case of the question. I have had to rethink this solution multiple times after arriving to this exact SO question, so I decided to post it here for future reference. Still, it's possible it doesn't belong here.Syblesybley
O
1

There's a lot of conflicting solutions around for this, here's what worked for us:

## ss_mv ##
function ss_mv {
    mkdir -p $(dirname "$2") && mv -f "$@"
}

This assumes commands in the following syntax:

ss_mv /var/www/myfile /var/www/newdir/myfile

In this way the directory path /var/www/newdir is extracted from the 2nd part of the command, and that new directory is then created (it's critical that you use the dirname tag to avoid myfile being added to the new directory being created).

Then we go ahead and mv on the entire string again by using the "$@" tag.

Overcrop answered 28/5, 2022 at 10:16 Comment(0)
G
1

I wrote a script that is a drop-in replacement for mv that should handle all the edge cases, including the cases where the destination is a file but in a non-existent directory or a non-existent directory itself, and the case that there are options to be passed to mv, and is compliant with POSIX sh.

#!/bin/sh
# USAGE:
# mkdmv [-if] source_file target_file
# mkdmv [-if] source_file... target_dir
mkdmv() {
    n=0 endofargs=
    for arg; do
        if [ -n "$endofargs" ] || [ "${arg#-}" = "$arg" ]; then
            # dest will be set to last non-option arg
            n=$((n+1)) dest=$arg
        elif [ "$arg" = '--' ]; then
            endofargs=1
        fi
    done
    # dest is a dir to be created
    # if there are multiple src files
    # or if target ends with "/"
    if [ "$n" -gt 2 ] || [ "${dest%/}" != "$dest" ]; then
        # append `.` to prevent `dirname` from returning parent dir
        dest="$dest/."
    fi
    mkdir -p -- "$(dirname -- "$dest")" &&
        mv "$@"
}
Guess answered 14/11, 2023 at 14:8 Comment(1)
very nice! Thanks for sharing this. One of the best answer. Most answers here just assume we are moving one file in a directory (tsk novices)... By the way you can use the name mv for the function so you don't have to rewire your brain and instead of mv inside the function to avoid infinite loop point to absolute path (e.g. /usr/bin/mv).Hindquarter
W
0

My one string solution:

test -d "/home/newdir/" || mkdir -p "/home/newdir/" && mv /home/test.txt /home/newdir/
Wageworker answered 23/8, 2017 at 15:33 Comment(0)
P
-1

You can even use brace extensions:

mkdir -p directory{1..3}/subdirectory{1..3}/subsubdirectory{1..2}
  • which creates 3 directories (directory1, directory2, directory3),
    • and in each one of them two subdirectories (subdirectory1, subdirectory2),
      • and in each of them two subsubdirectories (subsubdirectory1 and subsubdirectory2).

You have to use bash 3.0 or newer.

Pericardium answered 1/4, 2011 at 5:11 Comment(1)
Interesting, but doesn't answer the OP's question.Gab
S
-1
$what=/path/to/file;
$dest=/dest/path;

mkdir -p "$(dirname "$dest")";
mv "$what" "$dest"
Shannan answered 30/6, 2016 at 1:24 Comment(1)
to make it standalone sh script, just replace $dest and $src with $1 and $2Shannan

© 2022 - 2024 — McMap. All rights reserved.