0

I have an old .bat file that I need to run on my QNAP. I need to recursively create folders for each file (based on file name) then move the file to that folder.

Here's the old .bat:

@echo off 
for %%a in (*.*) do (md "%%~na" 2>nul
move "%%a" "%%~na"
)

1 Answers1

1

If I understand the .bat syntax correctly, this should be basically equivalent:

#!/bin/sh
for a in *
do
    [ ! -f "$a" ] && continue  # skip anything that is not a regular file
    dirname="${a%.*}"          # trim off the last suffix
    mkdir -p "$dirname" && mv -i "$a" "$dirname/"
done

Like the original, this script will process the files within the current directory only. If you need to process the current directory and all its pre-existing sub-directories (that's how I would understand "recursively") it gets a bit more complex, as the script would have to proceed in a depth-first manner to avoid trying to push files into infinitely deep sub-sub-sub... directories.

In that situation, a different solution would need to be developed, most likely using the find command to gather the filenames. The find command is recursive by default: it will automatically process the specified directory and all its sub-directories, unless you specifically tell it not to. So here's a recursive version that can handle an arbitrary number of files in any directory:

#!/bin/sh
# FIXME: needs more protection against unusual filename characters
find . -type f -depth | while read a
do
    dirname="${a%.*}"          # trim off the last suffix
    mkdir -p "$dirname" && mv -i "$a" "$dirname/"
done

It has a slight problem, though: if a filename includes a line-feed character, it might not process that filename correctly. But if your files only have well-behaved characters in their names, this should be enough.

telcoM
  • 229
  • 3
  • 12
  • The shell will have no issue with number of argument for the `for a in *` line since it's not calling any external utilities there. – Kusalananda May 20 '18 at 06:46
  • To be safe with filenames: `find . -type f -depth -exec sh -c 'for a do dirname="${a%.*}"; mkdir -p "$dirname" && mv -i "$a" "$dirname/"; done' sh {} +` – Kusalananda May 20 '18 at 06:49
  • The number of arguments is not an issue: the total combined length of the filenames in the directory may be. On a modern regular system it requires exceptional circumstances to exceed ARG_MAX, but note that we're dealing with a QNAP (effectively an embedded system) here. In a directory with a large number of files, the wildcard expansion may hit the maximum command line length limit. – telcoM May 20 '18 at 07:02
  • The command length limit is only an issue if the shell calls an external utility through `execve()`, which it will not do with `*` here. If you want to be perfectly correct you should say "combined length of the command line and current environment" since the environment variables are also counted towards that limit. But it's not an issue in this case as the command line used with external utilities will be short. – Kusalananda May 20 '18 at 07:17
  • Thanks for clarification: in the past, I've been burned with something like that in a busybox or similar embedded enviroment, and getting the dang thing working was more important than understanding the entirety of the problem, so it looks like I was left with a bit of unnecessary paranoia there... – telcoM May 20 '18 at 07:25
  • Well, your basic paranoid instinct is correct in telling you to watch out when using `*` (but it's not always a problem), and defensive programming is good. – Kusalananda May 20 '18 at 07:33