32

I have a directory foo with several files:

.
└── foo
    ├── a.txt
    └── b.txt

and I want to move it into a directory with the same name:

.
└── foo
    └── foo
        ├── a.txt
        └── b.txt

I'm currently creating a temporary directory bar, move foo into bar and rename bar to foo afterwards:

mkdir bar
mv foo bar
mv bar foo

But this feels a little cumbersome and I have to pick a name for bar that's not already taken.

Is there a more elegant or straight-forward way to achieve this? I'm on macOS if that matters.

6
  • How do you feel about links? That is, do other non-sym-links exist linking to original foo? Do those links now need to point to foo/foo? Commented Dec 2, 2019 at 18:56
  • @Harper-ReinstateMonica indeed, there are some GUI applications having a link to foo (e.g. "recently opened files"). To keep those working, foo itself has to be moved, rather than just its content. (otherwise those links would refer to the new parent directory which isn't desired)
    – Stefan
    Commented Dec 2, 2019 at 22:18
  • But are you sure these are plain links and not symlinks? Plain links increment the link count, symlinks do not. Plain links mean if the file is deleted, the file still lives in the other directory. Commented Dec 2, 2019 at 22:51
  • @Harper-ReinstateMonica those are not links in the file system. The applications store the file's unique ID which is based on its inode number.
    – Stefan
    Commented Dec 3, 2019 at 5:54
  • mkdir foo/foo; mv foo/* foo/foo prints an error but still results in your expected outcome (at least on Ubuntu). Now: if you are interested on how inodes "move" then the result is not what you want (the original foo directory is not the inner foo so if you access via inode or something you will not find the expected structure). Commented Dec 3, 2019 at 11:38

9 Answers 9

23

To safely create a temporary directory in the current directory, with a name that is not already taken, you can use mktemp -d like so:

tmpdir=$(mktemp -d "$PWD"/tmp.XXXXXXXX)   # using ./tmp.XXXXXXXX would work too

The mktemp -d command will create a directory at the given path, with the X-es at the end of the pathname replaced by random alphanumeric characters. It will return the pathname of the directory that was created, and we store this value in tmpdir.1

This tmpdir variable could then be used when following the same procedure that you are already doing, with bar replaced by "$tmpdir":

mv foo "$tmpdir"
mv "$tmpdir" foo
unset tmpdir

The unset tmpdir at the end just removes the variable.


1 Usually, one should be able to set the TMPDIR environment variable to a directory path where one wants to create temporary files or directories with mktemp, but the utility on macOS seems to work subtly differently with regards to this than the same utility on other BSD systems, and will create the directory in a totally different location. The above would however work on macOS. Using the slightly more convenient tmpdir=$(TMPDIR=$PWD mktemp -d) or even tmpdir=$(TMPDIR=. mktemp -d) would only be an issue on macOS if the default temporary directory was on another partition and the foo directory contained a lot of data (i.e. it would be slow).

4
  • Aside from the double copy, a different partition might also have insufficient space for the temporary file storage. Commented Dec 1, 2019 at 15:33
  • 1
    @roaima Yes, but hopefully it was clear enough that the command that I actually show will create a unique name in the current directory.
    – Kusalananda
    Commented Dec 1, 2019 at 16:29
  • 1
    Thanks for the example. I was playing with mktemp put couldn't find a way to create a temporary directory in the current working directory. Even though my TMPDIR is on the same partition, I don't want to move the files outside the current directory in case something goes wrong. Prefixing it with $PWD did the trick!
    – Stefan
    Commented Dec 1, 2019 at 18:04
  • 2
    @Stefan Yes, this is an issue. See my question on the topic: mktemp on macOS not honouring $TMPDIR
    – Kusalananda
    Commented Dec 1, 2019 at 18:06
10

On macOS, you can install the rename command (a Perl script) using Homebrew:

brew install rename

Then using the -p (a la mkdir) to have it make any necessary directories, and -A to add a prefix:

% mkdir -p foo/bar; touch foo/{a,b}.txt foo/bar/c.txt
% rename -p -A foo/ foo/*
% tree foo
foo
└── foo
    ├── a.txt
    ├── b.txt
    └── bar
        └── c.txt

Run with -n to show changes without renaming (dry-run):

% rename -p -A foo/ foo/* -n
'foo/a.txt' would be renamed to 'foo/foo/a.txt'
'foo/b.txt' would be renamed to 'foo/foo/b.txt'
'foo/bar' would be renamed to 'foo/foo/bar'

If you have dot files, so that a simple * won't pick them up, then use other methods with rename:

  • With bash, (mis)use GLOBIGNORE to get * to match dot files:

    $ GLOBIGNORE=.; rename -p -A foo/ foo/* -n
    'foo/.baz' would be renamed to 'foo/foo/.baz'
    'foo/a.txt' would be renamed to 'foo/foo/a.txt'
    'foo/b.txt' would be renamed to 'foo/foo/b.txt'
    'foo/bar' would be renamed to 'foo/foo/bar'
    
  • Or use find with -print0 and rename with -0:

    % find foo -mindepth 1 -maxdepth 1 -print0 | rename -0 -p -A foo/ -n
    Reading filenames from STDIN
    Splitting on NUL bytes
    'foo/b.txt' would be renamed to 'foo/foo/b.txt'
    'foo/a.txt' would be renamed to 'foo/foo/a.txt'
    'foo/bar' would be renamed to 'foo/foo/bar'
    'foo/.baz' would be renamed to 'foo/foo/.baz'
    
5
  • What does the -p do on macOS? I don't have a -p option for my rename. Same for the -A, actually. What do they do?
    – terdon
    Commented Dec 2, 2019 at 11:08
  • 1
    @terdon -p is like with mkdir - it creates missing directories. -a adds a suffix, -A adds a prefix. I assume your rename is one of the Perl renames on a Linux?
    – muru
    Commented Dec 2, 2019 at 11:37
  • Yes, it's perl-rename but has neither a -p nor a -A option. Thanks for the edit.
    – terdon
    Commented Dec 2, 2019 at 13:56
  • I didn't edit anything here. ¯\_(ツ)_/¯ Does your perl-rename report a version number?
    – muru
    Commented Dec 2, 2019 at 14:37
  • Duh, so you didn't! My bad, I had missed that you had already explained these somehow, sorry! I have perl-rename 1.9. I suspect macOS just brings in one of the other ones: What's with all the renames: prename, rename, file-rename?
    – terdon
    Commented Dec 2, 2019 at 14:55
10

As long as the contents are not sufficient to exceed the maximum parameter limits, (and you don't mind an "acceptable" error message) then it doesn't need to be any more complicated than this:

mkdir foo/foo
mv foo/* foo/foo

Amendment to handle hidden files:

mkdir foo/foo
mv foo/{.,}* foo/foo
4
  • 1
    Yes it has to be to handle hidden files: mv foo/* foo/.* foo/foo Commented Dec 3, 2019 at 11:44
  • 1
    mv foo/* foo does what? It appears to be moving stuff to where they were originally located.
    – Kusalananda
    Commented Dec 3, 2019 at 13:08
  • 1
    @GiacomoAlzetta Better then to enable dotglob and nullglob (in bash) than trying to glob files that might not exist.
    – Kusalananda
    Commented Dec 3, 2019 at 15:59
  • Sorry there was a typo missing the subdir off the target. Also added in handling for dot files as well. I concede it’s hacky, but the globs all exist — we just created subdirectory foo, and .. would match for dot files I think.
    – bxm
    Commented Dec 3, 2019 at 16:37
8

I suggest the other way around. Don't move the directory, but only its content:

.
└── foo
    ├── a.txt
    └── b.txt
mkdir foo/foo
.
└── foo
    ├── foo
    ├── a.txt
    └── b.txt
cd foo
mv $(ls | grep -v '^foo$') foo
cd -
.
└── foo
    └── foo
        ├── a.txt
        └── b.txt

If you have bash, you can also do

shopt -s extglob
cd foo
mv !(foo) foo
cd -

(as described here) to avoid running ls and grep.

The advantage of this solution is that it's really simple.

Disadvantages (as pointed out in the comments):

  • Cannot handle hidden files (ls -A fixes it, but not ls -a)
  • Cannot handle filenames containing spaces (can be fixed by quotes: mv "$(ls | grep -v '^foo$')" foo
  • Cannot handle filenames containing newlines and other special characters

Most disadvantages can be addressed by using some bash trick, but if one must handle crazy filenames, it's better to use more robust approach, as described in other answers.

9
  • 1
    You would need ls -A there (or ls -a and grep -v, if your ls does not hove -A) in order to move all the filenames starting with . . Then again, if you do have GNU coreutils, then mv will happily correctly handle trying to move directory into itself (with just a warning) when you try mv foo/* foo/foo Commented Dec 2, 2019 at 0:10
  • 1
    Rather than your grep / mv -combination, I'd suggest using find. e.g.: find -mindepth 1 -maxdepth 1 ! -name 'foo' -print0 | xargs -0 -I mv '{}' foo. You might otherwise risk errors on filenames with e.g. spaces.
    – FelixJN
    Commented Dec 2, 2019 at 9:37
  • 8
    This will miss any hidden files, as already pointed out, and will also fail on any file names with whitespace. Parsing ls is never a good idea.
    – terdon
    Commented Dec 2, 2019 at 11:11
  • 1
    mv foo/* foo/.* foo/foo works like a charm. It does print an error but still produces the expected result. It should handle spaces, newlines etc and hidden files too. Commented Dec 3, 2019 at 11:43
  • 1
    Yes, mv * foo/ will print an error message, but it will also move the other args. I just tried it on my own Arch Linux system. That's exactly what @MatijaNalis said: correctly handle it with just a warning. Commented Dec 4, 2019 at 13:25
6

You've pretty much nailed it already. You could pick a different name for the transient directory, such as the target name with the current date/time in nanoseconds and our PID as a composite suffix, but this still presupposes the directory doesn't already exist:

dir=foo                         # The directory we want to nest
now=$(date +'%s_%N')            # Seconds and nanoseconds
mkdir "$dir.$now.$$"            # Create a transient directory
mv -f "$dir" "$dir.$now.$$/"    # Move our directory inside transient directory
mv -f "$dir.$now.$$" "$dir"     # Rename the transient directory to the name with which we started

If you want a guaranteed robust solution, I would loop around the mkdir until it was successful

now=$(date +'%s_%N')
while ! mkdir -m700 "$dir.$now.$$" 2>/dev/null
do
    sleep 0.$$
    now=$(date +'%s_%N')
done
11
  • I was hoping for a way to create an "unnamed" directory then move my directory into it and assign a name afterwards. Like a nameless inode, but I guess that doesn't exists.
    – Stefan
    Commented Dec 1, 2019 at 13:42
  • @Stefan if you had a directory that was unnamed how would you reference it in the filesystem? Commented Dec 1, 2019 at 15:03
  • Via it's inode number, e.g. find . -inum 123456 -exec mv {} foo \; would rename the file with inode number 123456 to foo.
    – Stefan
    Commented Dec 1, 2019 at 16:46
  • 2
    @Stefan there's no way to access a file directly by its inode number; if that were possible, it would break completely break the security model. If creating another temporary dir in the parent dir of foo is complicated, you can restructure the foo dir in place: mkdir foo/foo; mv foo/* foo/foo (for the last part, you can ignore the error, or use extended globbing to skip the foo/foo subdir itself).
    – user313992
    Commented Dec 1, 2019 at 17:17
  • 2
    @Stefan your find references (uses) a directory by its name, where its identifying criterion is the inode number. Commented Dec 1, 2019 at 17:27
6
mkdir foo/foo && mv foo/!(foo) foo/foo

You need to cd into the directory where the source folder (foo) is.

Then run the command above. It will create a folder called of the same name and move the contents of the parent foo into the child foo directory (except the child directory, hence the ! designation).

If the child foo directory already exists you can ignore the first part and just run the mv command:

mv foo/!(foo) foo/foo

In MacOS you may need to set the extglob option:

shopt -s extglob
1
  • Explanation: In Bash, !(foo) is like * except it won't match foo. It's a glob expression. Does it match dotfiles, though? Getting that right is one downside to the move-all-the-files approach. Commented Dec 4, 2019 at 13:29
2

In addition to the suggestions above, you might want to checkout rsync. Before I even start, always remember to use the --dry-run option with rsync before running it without it. The --dry-run will tell you what would happen in real life.

rsync has many options that can not only help copy/move files, but also speed up the process. Here is one way to get your job done. The --remove-source-filesoption deletes files from the source after the move process (which is actually a copy followed by a delete). Notice that the rsync command first reads the contents of foo, creates the directory foo within the existing directory called foo, then starts the copy process. It ends with deleting the source files in foo. Caution: Pay particular attention to the slashes for the directory names.

There are other considerations as many have pointed above (like symlinks), but a man rsync will help you pick the options that you need. A note here: Since you have asked for a solution for files, this will work. This will not remove empty directories that will be left behind. I am not aware of a way to do so without an additional step, so if anyone can add to this, thanks in advance!

mkdir foo
cd foo
touch aaa bbb ccc ddd
cd ..
rsync -av --remove-source-files foo foo/

a = archive mode

v = verbose

2

Simply don't touch foo and you don't have issues with identical names. Instead, create a directory with the same name inside it and move everything there. Use the $_ trick for that and && to make it a one-liner:

cd foo && mkdir $_ && mv * $_

This will throw a harmless error (mv: rename foo to foo/foo: Invalid argument) that you can ignore.

Advantage over other answers: You only need to type the directory name once, so you can't make mistakes with typos.


you can mix this with other answers for even better effect:

  • mv {.,}* $_ will take care of hidden files (at the cost of more errors that you can also ignore)
  • mv ./!(foo) will avoid the error message, but only if you set shopt -s extglob
4
  • IMHO, in would be better to use the dotglob shell option and use that with the extglob variation (if there is a destination directory, you left it out).
    – Kusalananda
    Commented Dec 4, 2019 at 8:58
  • Another disadvantage is that it doesn't preserve foo's permissions or extended attributes (e.g. folder color or custom icon in macOS). Those would stick to the outer foo.
    – Stefan
    Commented Dec 4, 2019 at 9:01
  • @Stefan that's true, but it wasn't requested in the question.
    – Tom
    Commented Dec 4, 2019 at 18:29
  • Well, in the question I said that I want to move a directory foo into a directory with the same name. That implies that the original directory becomes the inner one.
    – Stefan
    Commented Dec 4, 2019 at 21:00
0

This is actually deceptively simple and I'm shocked nobody else has offered this answer yet, so here I go Using the following for example...

$ mkdir foo
$ mkdir bar
$ mkdir ./bar/foo
$ touch ./bar/foo/file1 && touch ./bar/foo/file2 && touch ./bar/foo/file3
$ ls ./
bar/ foo/
$ ls ./bar/foo
file1 file2 file3

Now comes the magic part

$ mv ./bar/foo ./foo/
$ ls ./foo/
foo/
$ ls ./foo/foo/
file1 file2 file3

Now why does this work? I can't give you the most precise answer as I'm just a neophyte - but I understand it comes down to how trailing / characters are treated on folders.

Look closely at that mv command

$ mv ./bar/foo ./foo/

Notice how the target folder of files is specified without a trailing /, while the destination sharing the target folder's name has been specified using a trailing /'. This is your magic ticket, and can bite you in the ass if you aren't paying attention.

I'm sorry I can't offer a more elaborate answer than that at present, but perhaps someone more fluently knowledgeable will take the liberty of editing my answer.

In any event, this would seem to be the most simple and straightforward solution, with a single use of the ls command and no creation of intermediary folders.

Hope it works for you, cheers!

1
  • Disclaimer: I personally do not have access to an OSX system to cross check this on. I'm making a wild assumption that the shell environment on Mac behaves within this context the same as I've encountered in bash and zsh. If it doesn't work the same for you, the difference in shell environments may be why. Perhaps someone else with the right hardware can test this and comment about how it went for them.
    – PowerLuser
    Commented Dec 23, 2019 at 18:01

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .