8

I have a list of folders which have dates for names. The dates are in the format YYYYMMDD (e.g. 20150129). Within these folders are text documents which are related to that specific date.

I would like to restructure them in a folder hierarchy going from year to month to date, and to move the text documents into the corresponding 'date' folder lower down in the hierarchy.

In other words I would like the 'root' folder to be named after the year like 2015, and then create sub-folders named with months like 01, and then create further sub-folders named with dates like 29 which hold the corresponding text documents.

So the path would look like 2015/01/29/file.txt or 2015>01>29>file.txt.

I have taken a look at Automator and it seems that something like this is not possible although I could be wrong, so I would like to know...

  1. Is there some easy solution to this problem that any layman can understand, for example an Automator workflow, or does this require some understanding of terminal commands and regular expressions?

  2. How would one solve this problem provided there is in fact a solution?

  • To whomever voted to close this question as "too broad", why? I'm curious as to what's "too broad" about this question? – user3439894 Apr 29 '17 at 16:00
  • Are these YYYYMMDD folders all directly inside one master folder or are they spread out across a broader hierarchy? – nohillside Apr 29 '17 at 16:34
  • @patrix In my case they are all in the same directory or master folder – davidjnatarajan Apr 30 '17 at 8:30
8

Assuming all these YYYYMMDD folders are part of the same parent directory you could run

cd PARENT_DIRECTORY
for d in */; do
    [[ $d =~ [0-9]{8}/ ]] || continue
    mkdir -p -- "${d:0:4}/${d:4:2}"
    mv -- "$d" "${d:0:4}/${d:4:2}/${d:6:2}"
done
  • The for d in */; do loop reads all directory entries, the trailing / ensures that only directory names actually match
  • [[ $d =~ [0-9]{8}/ ]] tests whether the current entry consists of 8 digits, and continues with the next entry if not
  • ${d:0:4}/${d:4:2}/${d:6:2} uses parameter expansion within bash to create a string containing the new path
  • The -- in both mkdir and mv prevents problem in case the directory or file name starts with a -. This can't happen here but it's probably good practice anyway.

Thanks to @terdon and @user3439894 for ideas on how to improve the original script.

  • Thanks for the answer, this works perfectly! I feel this solution is better than the one provided by @grgarside because it's far quicker, especially when dealing with a massive corpus including thousands of text documents. – davidjnatarajan Apr 30 '17 at 15:47
8

You can use the following in Terminal. cd to the containing folder, then run the following:

find . -type f -exec bash -c \
  'F=$(sed -E "s#^\./([0-9]{4})([0-9]{2})([0-9]{2})#\1/\2/\3#" <<< $1);\
  mkdir -p -- $(dirname "$F");\
  mv -- "$1" "$F"' - {} \;

find . -type f obtains every file in the current directory recursively.
-exec bash -c opens a shell to run the following commands.
F=$(…) opens a subshell and uses sed on the file path to manipulate the path into the folders.
^\./([0-9]{4})([0-9]{2})([0-9]{2}) is a regex with three capture groups, as follows:
\1/\2/\3 is replacement, where each capture group (\1, etc) are separated by /.
mkdir -p -- $(dirname "$F") creates the directories to move the files into.
mv -- "$1" "$F" moves each file into its corresponding folder.

This takes the hierarchy on the left and converts it to the hierarchy on the right:

├── 20170201               └── 2017
│   └── abcdefghij             ├── 02
└── 20170302                   │   └── 01
    └── abcdefghij 2           │       └── abcdefghij
                               └── 03
                                   └── 02
                                       └── abcdefghij 2

If there are other files in the containing folder with a date as a name, they will be moved as if they are a folder. To prevent this, replace the second line with:

  'F=$(sed -E "s#^\./([0-9]{4})([0-9]{2})([0-9]{2})(?:/.+)#\1/\2/\3#" <<< $1);\

The (?:/.+) ensures that the path has a subsequent component, therefore ignoring anything without a child in the parent directory which are files.

You must log in to answer this question.

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