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.

Stefan
  • 431
  • 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? – Harper - Reinstate Monica Dec 02 '19 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 Dec 02 '19 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. – Harper - Reinstate Monica Dec 02 '19 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 Dec 03 '19 at 05: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). – Giacomo Alzetta Dec 03 '19 at 11:38
  • @GiacomoAlzetta I'm aware of that. My intention is to move foo (the whole directory, not just its content) into a new directory. That new directory should be named "foo" as well. My example above (mkdir bar && mv foo bar && mv bar foo) already works as expected in that regard. – Stefan Dec 03 '19 at 12:47

9 Answers9

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).

Toby Speight
  • 8,678
Kusalananda
  • 333,661
  • Aside from the double copy, a different partition might also have insufficient space for the temporary file storage. – Chris Davies Dec 01 '19 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 Dec 01 '19 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 Dec 01 '19 at 18:04
  • 2
    @Stefan Yes, this is an issue. See my question on the topic: mktemp on macOS not honouring $TMPDIR – Kusalananda Dec 01 '19 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'
    
muru
  • 72,889
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
bxm
  • 4,855
  • 1
    Yes it has to be to handle hidden files: mv foo/* foo/.* foo/foo – Giacomo Alzetta Dec 03 '19 at 11:44
  • 1
    mv foo/* foo does what? It appears to be moving stuff to where they were originally located. – Kusalananda Dec 03 '19 at 13:08
  • 1
    @GiacomoAlzetta Better then to enable dotglob and nullglob (in bash) than trying to glob files that might not exist. – Kusalananda Dec 03 '19 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 Dec 03 '19 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.

Adam Trhon
  • 1,623
  • 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 – Matija Nalis Dec 02 '19 at 00:10
  • @MatijaNalis on my system (Arch Linux, coreutils 8.31-3) mv returns 1 to shell when moving the directory into itself. – Adam Trhon Dec 02 '19 at 08:37
  • 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 Dec 02 '19 at 09: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 Dec 02 '19 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. – Giacomo Alzetta Dec 03 '19 at 11:43
  • In addition to @terdons correct objection, it will also fail to move files that have the substring "foo", such as "buffoon" for a draft biography of the UK prime minister. – gerrit Dec 03 '19 at 13:28
  • @gerrit only the variant with !(foo). The grep variant has this handled. – Adam Trhon Dec 03 '19 at 15:10
  • For large directories this creates more metadata churn, removing each dirent from one directory and adding it to another. – Peter Cordes Dec 04 '19 at 13:23
  • 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. – Peter Cordes Dec 04 '19 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
Chris Davies
  • 116,213
  • 16
  • 160
  • 287
  • 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 Dec 01 '19 at 13:42
  • @Stefan if you had a directory that was unnamed how would you reference it in the filesystem? – Chris Davies Dec 01 '19 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 Dec 01 '19 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). –  Dec 01 '19 at 17:17
  • @mosvy remember dot files too – Chris Davies Dec 01 '19 at 17:25
  • 2
    @Stefan your find references (uses) a directory by its name, where its identifying criterion is the inode number. – Chris Davies Dec 01 '19 at 17:27
  • there's dotglob, extglob and all the bash zoo; I've assumed that the OP's Q was more of the general kind (eg. in C, perl, etc. you could use readdir directly, which would be faster than globbing as it would not imply sorting). –  Dec 01 '19 at 17:31
  • @mosvy if I'm not mistaken that would result in another / new inode number for the nested foo. I really want to move the directory itself, not its content. – Stefan Dec 01 '19 at 17:58
  • @roaima maybe my find example doesn't work. It was just to illustrate what I had in mind, i.e. create some anonymous directory, perform the operations and – when finished – name it. – Stefan Dec 01 '19 at 17:59
  • @Stefan why? what importance has the inode number of the directory? –  Dec 01 '19 at 18:20
  • @mosvy the directories are shown in the "recently opened" list of some GUI applications. It's not a critical requirement but preserving the inode numbers would be more convenient. – Stefan Dec 01 '19 at 19:32
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
masterl
  • 93
  • 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. – Peter Cordes Dec 04 '19 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
Tom
  • 158
  • 5
  • 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 Dec 04 '19 at 08: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 Dec 04 '19 at 09:01
  • @Stefan that's true, but it wasn't requested in the question. – Tom Dec 04 '19 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 Dec 04 '19 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!

  • 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 Dec 23 '19 at 18:01