3

To flatten a directory structure, I can do this:

find . -type f -exec sh -c 'mv "{}" "./`basename "{}"`"'  \;

I want to store the following in my profile as $FLATTEN

-exec sh -c 'mv "{}" "./`basename "{}"`"'  \;

so that later I can just execute find . $FLATTEN

I'm having trouble storing the variable because it gets interpreted too early. I want it to be stored as a string literal and interpreted only in usage on the shell, not when sourced.

4 Answers4

9

If using GNU mv, you should rather do:

find . -type f -exec mv -t . {} +

With other mvs:

find . -type f -exec sh -c 'exec mv "$@" .' sh {} +

You should never embed {} in the sh code. That's a command injection vulnerability as the names of the files are interpreted as shell code (try with a file called `reboot` for instance).

Good point for quoting the command substitution, but because you used the archaic form (`...` as opposed to $(...)), you'd need to escape the inner double quotes or it won't work in sh implementations based on the Bourne shell or AT&T ksh (where "`basename "foo bar"`" would actually be treated as "`basename " (with an unmatched ` which is accepted in those shells) concatenated with foo and then bar"`").

Also, when you do:

mv foo/bar bar

If bar actually existed and was a directory, that would actually be a mv foo/bar bar/bar. mv -t . foo/bar or mv foo/bar . don't have that issue.

Now, to store those several arguments (-exec, sh, -c, exec mv "$@" ., sh, {}, +) into a variable, you'd need an array variable. Shells supporting arrays are (t)csh, ksh, bash, zsh, rc, es, yash, fish.

And to be able to use that variable as just $FLATTEN (as opposed to "${FLATTEN[@]}" in ksh/bash/yash or $FLATTEN:q in (t)csh), you'd need a shell with a sane array implementation: rc, es or fish. Also zsh here as it happens none of those arguments is empty.

In rc/es/zsh:

FLATTEN=(-exec sh -c 'exec mv "$@" .' sh '{}' +)

In fish:

set FLATTEN -exec sh -c 'exec mv "$@" .' sh '{}' +

Then you can use:

find . -type f $FLATTEN
4

It would be better to use a shell function for this, and to get the -exec right (don't put {} in a subshell):

flatten () {
     ( cd "${1:-.}" && 
       find . -type f -exec sh -c 'for n; do test -e "${n##*/}" || mv "$n" "${n##*/}"; done' sh {} + )
}

This also doesn't need to call the external utility basename and will not try to overwrite already existing things.

You would use this by just typing flatten and it will act on the current directory. Giving it a directory name will make it act on that (copying all files under that directory to the top of that directory).

Kusalananda
  • 333,661
3

How about a function?

flatten(){
  find "$@" -type f -exec sh -c 'mv -- "$0" "${0##*/}"' {} \;
}

Usage:

> flatten .

If you use zsh, there's the -g option of alias for that. It lets you define an alias which is inserted globally, not just at the command name.

dessert
  • 1,687
3

A function is probably the best way. Otherwise you need an array or eval:

find_array=(-exec sh -c 'mv "{}" "./`basename "{}"`"'  \;)
find . "${find_array[@]}"

or

FLATTEN="-exec sh -c 'mv \"{}\" \"./`basename \"{}\"`\"'  \;"
eval find . $FLATTEN
Hauke Laging
  • 90,279