Create a hierarchy of folders for 2006

by Robert Daeley

Related link: http://www.onlamp.com/pub/a/onlamp/2005/12/15/organizing_files.html



Karl Vogel, on sister site ONLamp.com, recently published an article: "Organizing Files" which you should read. It's a very cool review of what systems didn't work, what almost worked, and what GTD-inspired solution wound up working for him.

In the midst of the article, Karl mentions creating a "notebook" directory containing individual year folders, each of which holds a series of folders in a "mmdd" format, one per day. There would be a ~/notebook/2005/1221 for today's date, 2005-12-21, for example

It struck me that it would be handy to automate the process of creating that hierarchy. For my purposes, however, I'd rather have a yyyy/mm/dd format to cut down on the number of folders at any given level.

So, just for the heck of it, I wrote a shell script to create a year's worth of folders, one per day. You can download createfolders.txt (rename it to createfolders.sh and make it executable), or check it out below.

Disclaimer: I make no claims to being a shell script ninja, so I'm sure there are other, more efficient methods that will also tuck you into bed at night and read you a bedtime story. TMTOWTDI. ;) Also, I've tested this using bash on my OS X 10.4.3 system. That means it may travel back in time and kill some ancestor of yours, preventing your birth and creating a rift in spacetime. You have been warned.


#!/bin/sh
# createfolders.sh
# create a date-based folder hierarchy for an entire year
# by Robert Daeley
# quick and dirty -- no error checking
# -------------------------------------------------------------
# Array for number of days in each month
# change the 28 manually for leap years
numdays=( 0 31 28 31 30 31 30 31 31 30 31 30 31 )
# change targetpath to desired location
targetpath=/Users/rdaeley/notebook/2006
# -------------------------------------------------------------
# first, a function to create a number of folders
createfolders ()
{
g=1
# this $1 refers to the value passed to this function
while [ "$g" -le "$1" ]
do
# if the month or day is 1-9, we add a leading zero
if [ "$g" -lt 10 ]
then
mkdir "0$g"
else
mkdir "$g"
fi
let "g += 1"
done
}
# -------------------------------------------------------------
# now we get started
cd "$targetpath"
m=1
# We loop through the 12 months and create a folder for each
# and, along the way, create the appropriate number of days.
# First, the months...
createfolders "12"
# ...then we run through each month and create the day folders.
while [ "$m" -le 12 ]
do
if [ "$m" -lt 10 ]
then
cd "$targetpath/0$m"
else
cd "$targetpath/$m"
fi
let "thismonth = ${numdays[$m]}"
createfolders "$thismonth"
let "m += 1"
done
exit 0

11 Comments

jmenard
2005-12-21 16:04:10
Ruby version
Just for fun, here's a Ruby version. I hope this survives text formatting. There's no way for me to preview this.


#!/usr/bin/env ruby
#
# usage: createfolders.rb [year]
#
# Creates a years' worth of folders. If year is not specified, the current
# year is used.


require 'date'
require 'ftools'


year = (ARGV[0] || Time.new.year).to_i
start_date = Date.new(year, 1, 1)
end_date = (start_date >> 12) - 1


start_date.step(end_date, 1) { | d |
File.makedirs("#{d.year}/#{'%02d' % d.mon}/#{'%02d' % d.mday}")
}

aristotle
2005-12-21 16:48:37
Re:
mkdir has this very handy -p switch, which makes it create any missing parent directories, so if you say mkdir -p foo/bar/baz and foo does not exist, it will create foo and foo/bar for you.


And for counting in shell scripts, the tool of choice is usually seq.


So the following is how I’d solve this task:


#!/bin/bash


numdays=( 0 31 28 31 30 31 30 31 31 30 31 30 31 )


year=${1:-2005}


month_begin=${2:-1}
month_end=${2:-12}


seq $month_begin $month_end | while read month ; do
seq 1 ${numdays[$month]} | while read day ; do
mkdir -p $( printf "%04d/%02d/%02d" $year $month $day )
done
done


The ${VAR:-default} construct is a handy tool for parametrising scripts: it produces the value of $VAR, except when the variable is not set, in which case it returns the string "default". The above script uses $1 and $2 with some default values, so it can be called as mkdatedirs (in which case it will default to make a complete tree for the year 2005) or mkdatedirs 2006 (in which case it will make a tree for 2006 and default to it having all months) or mkdatedirs 2008 12 (in which case it will make a tree for only the December of 2008).


Actually, this script is a bit suboptimal – it’s slow because it spawns a new mkdir process up to 365 times. But mkdir can create any number of directories in one go (you can say things like mkdir foo bar baz and it will create foo, bar and baz in your current directory). Noone really cares for one-offs of course, but since this version is a bit more flexible already than a one-off already, and it takes the computer a noticable thinking moment to execute it, and because it’s so easy to improve, here’s a better way to do it for demonstration purposes:


#!/bin/bash


numdays=( 0 31 28 31 30 31 30 31 31 30 31 30 31 )


year=${1:-2005}


month_begin=${2:-1}
month_end=${2:-12}


seq $month_begin $month_end | while read month ; do
seq 1 ${numdays[$month]} | while read day ; do
printf "%04d/%02d/%02d\0" $year $month $day
done
done |
xargs -0 mkdir -p


Here, the loop is only responsible for generating the directory names. Now notice the pipe after the second done there – this means anything printed within the loop is piped to the following process. So the loop output is piped to xargs, which separates it at the NUL characters and feeds it as parameters to a single mkdir process. Since the shell has a builtin version of printf, this script has very little process spawning overhead; and indeed it runs instantly. (Wow, creating 378 directories takes no time at all.)


Hope there’s something interesting for the bag o’tricks in there. :-)

pascalbalthrop
2005-12-21 17:04:57
Re: seq
when i run the latter version of your script, i get this error.


seq: command not found


is seq not a builtin?

daeley
2005-12-21 18:17:43
Re:
Cool tips. Unfortunately, I don't believe seq is included with OS X. Most of the GNU utilities are there, but there are a handful missing.
aristotle
2005-12-21 19:15:43
Re:
But the former version works…?
aristotle
2005-12-21 19:36:28
Re:
Hmm. I am guessing that that’s OS Ⅹ’s FreeBSD heritage showing, in which case the weapon of choice would be called jot. That makes the code a little more complicated, since instead of a start and end with optional step (like seq), it wants a start and count, with optional end.


Here I’ll rewrite only the improved version of the script; it ends up looking like so:


#!/bin/bash


num_days=( 0 31 28 31 30 31 30 31 31 30 31 30 31 )


year=${1:-2005}


month_begin=${2:-1}


num_months=${2:+1} # if $2 has a value, then it's "1"
num_months=${num_months:-12} # if $num_months has no value, then it's "12"


jot $num_months $month_begin | while read month ; do
jot ${num_days[$month]} 1 | while read day ; do
printf "%04d/%02d/%02d\0" $year $month $day
done
done |
xargs -0 mkdir -p


Here I had to use the ${VAR:+instead} construct (note the plus instead of minus sign), which means “if $VAR is set, the value is "instead"; if $VAR is empty, the value is empty too.”


Hopefully that works; if not, then you’ll have to make do with the uglier in-script math.

hailstone
2005-12-21 23:01:26
Ruby version
Thanks for this Ruby version saved me the 5 minutes I was going to spend writing one.
dropbox
2005-12-22 11:28:46
Ruby version
whats's up with all these ruby guys? They use words like "pragmatic" all the time but when it comes to getting things done several of them want to recreate a perfectly good shellscript in ruby.. why?
Why do you want to spend five minutes of your time to write a script that simply creates a folder structure if there already is a script that does exactly the same thing?
"for the fun of it" i can understand, but i can't understand hailstone.
KarlVogel
2006-01-05 11:30:10
Re: folder hierarchy
Thanks for the kind words!


Here's the perl version, assuming OSx has perl:



#!/usr/bin/perl -w
use strict;
use Carp;
use Time::Local;


my ($day, $dir, $mon, $newyear);


# Default to this year
my $year = shift(@ARGV)
|| (localtime(time()))[5] + 1900;
croak "invalid year [$year]"
unless $year =~ /^\d\d\d\d$/;


# Get started under home directory
my $home = "$ENV{HOME}" || (getpwuid($<))[7];
chdir($home) or croak "$home: unable to cd";


$dir = "./notebook";
-d $dir
or mkdir($dir)
or croak "$dir: unable to create: $!";


$dir = sprintf("./notebook/%4.4d", $year);
-d $dir
or mkdir($dir)
or croak "$dir: unable to create: $!";


# Avoid DST problems by going to 4am
# on 1 Jan of the given year
my $now = timelocal(0, 0, 4, 1, 0, $year)
or croak "timelocal failed";
my $curmon = 0;


while (1) {
($day,$mon,$newyear) = localtime($now))[3,4,5];
$newyear += 1900;
last if $newyear > $year;


$mon++;
if ($mon > $curmon) {
$dir = sprintf("./notebook/%4.4d/%2.2d",
$year, $mon);
-d $dir
or mkdir($dir)
or croak "$dir: unable to create: $!";
$curmon = $mon;
}


$dir = sprintf("./notebook/%4.4d/%2.2d/%2.2d",
$year, $mon, $day);
-d $dir
or mkdir($dir)
or croak "$dir: unable to create: $!";
$now += 86400;
}


exit(0);

daeley
2006-01-05 20:51:52
Re: folder hierarchy
Awesome, thanks!
mgaslowitz
2006-01-12 10:23:07
create folders only when you need them
Instead of creating 365* folders all at once, let's just create them when we need them. For example, we could save to a specific directory every time, say /default, and attach a folder action to it that says:


every time a folder is added to /default
move the file to /yyyy/mm/dd


the script below will ensure that the directory exists, and can be run as a cron task or at the beginning of the folder action.


#!/bin/bash


year=$(date +%Y)


# assigns the variable "year" with the current year


month=$(date +%m)


# assigns the variable "month" with the current month


day=$(date +%e)


# assigns the variable "day" with the current day


mkdir -p /YourDirectory/$year/$month/$day/


# creates the directory for /yyyy/mm/dd/
# if /yyyy or /mm do not exist, -p will create them automatically