I wrote a script this morning called “Planter.” It plants directory trees. I made a logo for it; not because it’s an official project or deserving of the extra effort, but because I had Photoshop open anyway.

Planter takes in simple, indented text files to define the structure of the directory tree it’s going to create. You pass it something like:

css
img
js
	libs
	mylibs

And it creates:

./css
./img
./js
./js/libs
./js/mylibs

You can nest directories as deeply as you like. You can pass the indented list to it on STDIN (piping from another command), or run planter.rb alone and it will open up your default editor and let you define the list on the fly. You can also use templates…

Create ~/.planter/ and add text files named “[template].tpl”, where “[template]” is the short name you’ll call it with. Say I have “~/.planter/client.tpl”, I can just run planter.rb client and it will read that template in and create the directory structure in whatever directory I’m in when I run it.

You can also use a very basic template variable system to add variable content. In your template, use %%X%% where X is an integer. The number corresponds to the arguments passed on the command line after the template name, so %%1%% is replaced with the first argument:

In the template:

client-%%1%%
	expenses
	contracts

On the command line:

planter.rb client "Mrs. Yourmom"

Creates:

./client-Mrs. Yourmom
./client-Mrs. Yourmom/expenses
./client-Mrs. Yourmom/contracts

Nifty. Here’s the script. It may evolve a bit from here, but it does everything I needed it to right now. Hope it’s useful for you, too. Feel free to fork the gist and play with it.

planter.rbraw
#!/usr/bin/ruby
=begin
Planter v1.3
Brett Terpstra 2013
ruby script to create a directory structure from indented data.

Three ways to use it:
- Pipe indented (tabs or 2 spaces) text to the script
  - e.g. `cat "mytemplate" | planter.rb
- Create template.tpl files in ~/.planter and call them by their base name
  - e.g. Create a text file in ~/.planter/site.tpl
  - `planter.rb site`
- Call planter.rb without input and it will open your $EDITOR to create the tree on the fly

You can put %%X%% variables into templates, where X is a number that corresponds to the index
of the argument passed when planter is called. e.g. `planter.rb client "Mr. Butterfinger"`
would replace %%1%% in client.tpl with "Mr. Butterfinger". Use %%X|default%% to make a variable
optional with default replacement.

If a line in the template matches a file or folder that exists in ~/.planter, that file/folder
will be copied to the destination folder.
=end
require 'yaml'
require 'tmpdir'
require 'fileutils'

def get_hierarchy(input,parent=".",dirs_to_create=[])
  input.each do |dirs|
    if dirs.kind_of? Hash
      dirs.each do |k,v|
          dirs_to_create.push(File.expand_path("#{parent}/#{k.strip}"))
          dirs_to_create = get_hierarchy(v,"#{parent}/#{k.strip}",dirs_to_create)
      end
    elsif dirs.kind_of? Array
      dirs_to_create = get_hierarchy(dirs,parent,dirs_to_create)
    elsif dirs.kind_of? String
      dirs_to_create.push(File.expand_path("#{parent}/#{dirs.strip}"))
    end
  end
  return dirs_to_create
end

def text_to_yaml(input, replacements = [])
  variables_count = input.scan(/%%\d+%%/).length
  if variables_count > replacements.length
    $stderr.puts('Mismatch variable/replacement counts!')
    $stderr.puts("Template has #{variables_count} required replacements, #{replacements.length} provided.")
    Process.exit 1
  end
  input.gsub!(/%%(\d+)(?:\|(.*?))?%%/) do |match|
    if replacements[$1.to_i - 1]
      replacements[$1.to_i - 1]
    elsif !$2.nil?
      $2
    else
      print "Invalid variable"
      Process.exit 1
    end
  end
  lines = input.split(/[\n\r]/)
  output = []
  prev_indent = 0
  lines.each_with_index do |line, i|
    indent = line.gsub(/  /,"\t").match(/(\t*).*$/)[1]
    if indent.length > prev_indent
      lines[i-1] = lines[i-1].chomp + ":"
    end
    prev_indent = indent.length
    lines[i] = indent.gsub(/\t/,'  ') + "- " + lines[i].strip # unless indent.length == 0
  end
  lines.delete_if {|line|
    line == ''
  }
  return "---\n" + lines.join("\n")
end

if STDIN.stat.size > 0
  data = STDIN.read
elsif ARGV.length > 0
  template = File.expand_path("~/.planter/#{ARGV[0].gsub(/\.tpl$/,'')}.tpl")
  ARGV.shift
  if File.exists? template
    File.open(template, 'r') do |infile|
      data = infile.read
    end
  else
    puts "Specified template not found in ~/.planter/*.tpl"
  end
else
  tmpfile = File.expand_path(Dir.tmpdir + "/planter.tmp")
  File.new(tmpfile, 'a+')

  # at_exit {FileUtils.rm(tmpfile) if File.exists?(tmpfile)}

  %x{$EDITOR "#{tmpfile}"}
  data = ""
  File.open(tmpfile, 'r') do |infile|
    data = infile.read
  end
end

data.strip!

yaml = YAML.load(text_to_yaml(data,ARGV))
dirs_to_create = get_hierarchy(yaml)

dirs_to_create.each do |dir|
  curr_dir = ENV['PWD']
  unless File.exists? dir
    $stderr.puts "Creating #{dir.sub(/^#{curr_dir}\//,'')}"
    if File.exists?(File.join(File.expand_path("~/.planter"),File.basename(dir)))
      FileUtils.cp_r(File.join(File.expand_path("~/.planter"),File.basename(dir)), dir)
    else
      Dir.mkdir(dir)
    end
  else
    $stderr.puts "Skipping #{dir.sub(/^#{curr_dir}\//,'')} (file exists)"
  end
end

[Photo credit: simphonic]