1

I know how to find all directories with a given name:

find . -name unnecessary_dir_level

and how to move files to a parent dir:

mv * ..

how do I combine things - is

find . -name unnecessary_dir_level -exec mv {}/* {}/.. \;

going to do the job without causing chaos? or what will? I'm not keen on trying this without advice (and the directories are too big to back up in a reasonable amount of time).

simone
  • 111
  • 1
    What's the plan for name collisions? I.e., what are you planning to do when there is a file called myfile both in the directory that you want to move the contents from, and in the directory that you want to move the contents to. Also "too big to back up" is something I've never seen so far, ever. – Kusalananda Jan 31 '22 at 17:47
  • re. collision: mv -u, plus the parent folder only contains the folder to be moved from - that's why it's unnecessary; re too big - yes, you're right. the full sentence is "too big for the space I've got left on the device and the time it would take to transfer it over the network". – simone Jan 31 '22 at 17:52
  • Like many tasks, this is an exercise in iteration. First, you need to craft a script (perhaps a bash function) that can correctly process one such directory, given the path to that directory. Once you have a solid procedure for how to process one directory, you can then simply call that function N more times, once for each of the N remaining directories. Don't worry about how to do something 1,000 times; instead, solve the question of how to do it once. Then repeat that as many times as needed. – Jim L. Jan 31 '22 at 18:01

3 Answers3

2

With zsh and a mv implementation with support for a -n (no clobber) option, you could do:

for dir (**/unnecessary_dir_level(ND/od)) () {
  (( ! $# )) || mv -n -- $@ $dir:h/ && rmdir -- $dir
} $dir/*(ND)

Where:

  • for var (values) cmd is the short (and more familiar among programming languages) version of the for loop.
  • **/: a glob operator that means any level of subdirectories
  • N glob qualifier: enables nullglob for that glob (don't complain if there's no match)
  • D glob qualifier: enables dotglob for that glob (include hidden files)
  • / glob qualifier: restrict to files of type directory.
  • od glob qualifier: order by depth (leaves before the branch they're on)
  • () { body; } args: anonymous function with its args.
  • here args being $dir/*(ND): all the files including hidden ones in $dir
  • the body running mv on those files if there are any and then rmdir on the $dir that should now be empty.
  • $dir:h, the head of $dir (its dirname, like in csh).

Note that mv * .. is wrong on two accounts:

  • it's missing the option delimiter: mv -- * ..
  • you're missing the hidden files: mv -- *(D) .. in zsh, ((shopt -s nullglob failglob; exec mv -- * ..) in bash)
  • also: you could end up losing data if there's a file with the same name in the parent.
find . -name unnecessary_dir_level -exec mv {}/* {}/.. \;

Can't work as the {}/* glob is expanded by the shell before calling mv. It would only be expanded to something if there was a directory called {} in the current directory, and then move the wrong files.


You could do something similar with find and bash with:

find . -depth -name unnecessary_dir_level -type d -exec \
  bash -O nullglob -O dotglob -c '
    for dir do
      set -- "$dir"/*
      (( ! $# )) || mv -n -- "$@" "${dir%/*}" && rmdir -- "$dir"
    done' bash {} +
1

With bash version >= 4.0:

shopt -s globstar nullglob dotglob
for f in **/name/; do echo mv "$f"* "$f"..; done

If output looks okay, remove echo.

From man bash:

globstar: If set, the pattern ** used in a pathname expansion context will match all files and zero or more directories and subdirectories. If the pattern is followed by a /, only directories and subdirectories match.

nullglob: If set, bash allows patterns which match no files [...] to expand to a null string, rather than themselves.

dotglob: If set, bash includes filenames beginning with a `.' in the results of pathname expansion.

Cyrus
  • 12,309
  • You need bash 5.0 or newer for **/ to work properly in 4.x, **/name would also find the names that are in directories pointed to by symlinks in the current directory. – Stéphane Chazelas Feb 01 '22 at 08:15
  • In any case, even with 5+, **/name/ will include the symlinks called name that point to a file of type directory. You'd need to exclude them with [ -L "${f%/}" ] && continue... – Stéphane Chazelas Feb 01 '22 at 08:17
  • You'd also need to process the list depth-first. For instance, if you have a a/name/b/name dir if you move a/name/b to a/b, then when it comes to process a/name/b/name, it will no longer be there. Reversing the expansion of the **/name/ glob would do it, but it's painful to do in bash, so you might as well use find that fixes all those problems as in the approach in my answer. – Stéphane Chazelas Feb 01 '22 at 08:19
  • @StéphaneChazelas: Thanks for all your comments. – Cyrus Feb 01 '22 at 17:33
0

You can use for loop to move each found directory's content one level above. For example, I have temp file in abc directory.

tmp/abc/
└── temp

The following command will put temp in tmp directory:

for i in ` find . -name abc ` ; do mv $i/* $i/.. ; done