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
  • 2
  • 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 {} +
Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
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,059
  • 3
  • 29
  • 53
  • You need bash 5.0 or newer for `**/` to work *properly* in 4.x, `**/name` would also find the `name`s 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
Baba Rocks
  • 131
  • 2
  • This will break if the file (or directory) has whitespace or newlines in the filename. – doneal24 Jan 31 '22 at 18:26
  • You may want to read [Why is looping over find's output bad practice?](//unix.stackexchange.com/q/321697) and [Security implications of forgetting to quote a variable in bash/POSIX shells](//unix.stackexchange.com/q/171346) – Stéphane Chazelas Jan 31 '22 at 18:54
  • You might also consider using the `$(...)` syntax instead of backticks. Look [here](https://unix.stackexchange.com/questions/126927/have-backticks-i-e-cmd-in-sh-shells-been-deprecated) for instance on the change. – doneal24 Jan 31 '22 at 20:54