0

I have access to only busybox.

I have a folder, like this:

prueba/$: ls
./uno
./dos
./tres

Each two subdirectories (uno, dos) have many files (with spaces in their names and non-ASCII characters) and I want to move them over to the other subdirectory (tres).

So I did this (escaping characters just because, making sure I don't do redundancy when copying and using yes just to replace any file already in directory [tres] I have had issues with mv -f before):

prueba/$: busybox find . -type f -not \( -path . -o -path ./tres \) -exec sh -c ' yes | mv "$1" ./tres ' sh {} \+

But it moves just the first file it finds and then exits.

But if I run:

prueba/$: busybox find . -type f -not \( -path . -o -path ./tres \) -exec sh -c ' yes | mv "$1" ./tres ' sh {} \;

I got what I expected. All files are moved from subdirectories "uno" and "dos" onto subdirecty "tres".

What is the internal mechanism that is activated when ; [implicit loop] and + [implicit batch processing] are in place?

What's wrong with the first command line (the one with +)?

The following is out of the question:

mv -f ./uno/* ./dos/* ./tres/

I'm doing some automated work that will require me to just run a one-liner and be done with the job. Plus, is a boring solution.

Edit:

The reason behind find . -exec sh -c 'CMD "$1"' sh {} \;

Is it possible to use `find -exec sh -c` safely?

1
  • Thank you. What will be the way to go? Appending + or using $@ as a variable?
    – abacox
    Commented Sep 12, 2021 at 7:11

1 Answer 1

2

Change it to:

find uno/ dos/ -type f -exec sh -c 'exec mv -f "$@" tres/' sh {} +

The problems with your code:

  • find . -not -path ./tres does exclude ./tres, but not ./tres/any-file-in-it
  • -exec sh 'mv "$1" tres' sh {} + calls sh with as many files as possible as arguments, but you're only moving the first ($1). "$@" expands to all the positional arguments as separate arguments, as if you had written "$1" "$2" "$3" ... "$last".
  • Note the ! as standard alternative to non-standard -not
  • Note that + is not a special character in any shell. It doesn't need to be quoted.
  • Note the -f option of mv to avoid prompting in the first place so you don't need yes. If you still wanted to use yes, you could just do yes | find ... -exec sh -c 'exec mv ..., as sh and mv will inherit find's stdin and read the ys from there.
  • With the mv implementation from GNU coreutils and recent versions of busybox, you can avoid the sh invocation by using find ... -exec mv -ft tres {} +.

If you wanted to move the files from any directory (recursively) except those in ./tres and excluding files in the current directory, you'd need:

LC_ALL=C find . -path ./tres -prune -o \
                -path './*/*' -type f -exec sh -c 'exec mv -f "$@" tres/' sh {} +

It's the -prune above that tells find not to look for files inside ./tres, and for other files, -path './*/*' (in combination with LC_ALL=C so * matches any string of bytes, not just characters) restricts to files whose path contains at least one / after the leading ./, so excluding files in the current directory.

With zsh:

set -o extendedglob # best in ~/.zshrc
mv -f -- (^tres)/**/*(D.) tres/

Though beware that if there are symlinks to directories in the current directory, they will be followed. It could also fail if your system's limit on the number and size of arguments is reached (see zargs to work around it).

Contrary to find, zsh globs do sort the list of files, so if there are several files with the same name, which one will end up in ./tres will be more deterministic (that'll be the one whose path sorts last though beware uno comes after dos as the order is lexical¹; all the other ones will be lost)


¹ you can add the n glob qualifier to get a numerical order, but that's only for sequences of digits (for dir2 to come before dir10 for instance), uno will still sort after dos)

7
  • Thank you for the heads up in regards simlinks and files already in ./tres I did not take that into account. I tried your suggestion find . -path ./tres -prune -o -path './*/*' just to check how the command works. For now is just printing ./tres. How do I implement ! if (let's say) I want to exclude a file in ./dos and how exactly are options being implement by find, I do not fully understand which directory is the one being affected by each option -path ./tres -prune -o -path './*/*' I'm specially confused by -path './*/*' what's that for?
    – abacox
    Commented Sep 12, 2021 at 7:49
  • 1
    @abacox. Don't just remove the -exec to test. You need to replace it with -print. See also `find` with multiple `-name` and `-exec` executes only the last matches of `-name` Commented Sep 12, 2021 at 7:51
  • Did it, with or without -print there is no output, actually. I'm getting a result only if find . -path ./tres -prune -print is entered (same as before -print doesn't make a difference).
    – abacox
    Commented Sep 12, 2021 at 7:56
  • @abacox, you mean LC_ALL=C busybox find . -path ./tres -prune -o -path './*/*' -type f -print outputs nothing? Then that would mean there is no regular files in any subdirectory of . except in ./tres, presumably because they've been moved there already. Commented Sep 12, 2021 at 7:59
  • When I test my test enviroment is the same, so ./tres was with nothing and the other folders with dummy files. I tought LC_ALL=C was already explicit in my sh shell (I'm in a very bare bones system). Interesting, why LC_ALL=C determines if the output gets printed? (Now I got output, but it's weird. With LC_ALL=C if -print is present I do get the expected result but without -print, ./tres is included).
    – abacox
    Commented Sep 12, 2021 at 8:04

You must log in to answer this question.

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