Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Expanded behavior for tilt #128

Closed
wants to merge 15 commits into from

2 participants

@thinkerbot

I propose an extension to the tilt exe, as outlined in the usage information. I think this will make tilt much more capable because you could render a whole series of templates from an input dir to an output dir.

I plan on building this behavior somewhere. If you like this proposal, then I'll write it into tilt itself. Otherwise, I'll make a separate fork or project.

Thanks! Appreciate the great library!

Use Case

mkdir -p inputs outputs
echo "obj: milk" > attributes.yml
echo "Got <%= obj %>" > inputs/file.txt.erb
tilt --attrs attributes.yml --input-dir inputs --output-dir outputs --files inputs/*

The result would be 'outputs/file.txt' with content 'Got milk'.

@thinkerbot

Ok, so the extension is done. An example using it to build a static site is here: https://gist.github.com/1734959

@judofyr
Collaborator

I think a separate project is a better approach.

@judofyr judofyr closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
Showing with 534 additions and 46 deletions.
  1. +180 −46 bin/tilt
  2. +354 −0 test/tilt_exe_test.rb
View
226 bin/tilt
@@ -1,23 +1,43 @@
#!/usr/bin/env ruby
-require 'ostruct'
+begin
+require 'fileutils'
require 'optparse'
require 'tilt'
usage = <<USAGE
-Usage: tilt <options> <file>
-Process template <file> and write output to stdout. With no <file> or
-when <file> is '-', read template from stdin and use the --type option
-to determine the template's type.
+Usage: tilt <options> <files...>
-Options
- -l, --list List template engines + file patterns and exit
- -t, --type=<pattern> Use this template engine; required if no <file>
- -y, --layout=<file> Use <file> as a layout template
+Process templates <file> and write output to stdout. With no <files> or when
+<file> is '-', read template from stdin and use the --type option to determine
+the template's type.
- -D<name>=<value> Define variable <name> as <value>
- -o, --vars=<ruby> Evaluate <ruby> to Hash and use for variables
+Output can be written to files (rather than printing to stdout) using the
+--files option. In that case the output file is the same as the input file
+with the template extension removed. For input files not relative to pwd, the
+output file is the basename of the input file. For example:
- -h, --help Show this help message
+ $pwd/path/to/template.txt.erb => path/to/template.txt
+ /not/pwd/template.txt.erb => template.txt
+
+Adjust the output file locations with --input-dir and --output-dir, which
+describe the base dir for relative paths, and the output directory. The output
+files are printed to stdout. With no <files> or when a <file> is '-', read
+files from stdin.
+
+Options:
+ -l, --list List template engines + file patterns and exit
+ -t, --type=<pattern> Use this template engine; required if no <file>
+ -y, --layout=<file> Use <file> as a layout template
+
+ -f, --files Output to files rather than stdout
+ -i, --input-dir=<dir> Use <dir> to determine relative paths for --files
+ -o, --output-dir=<dir> Use <dir> as the output dir for --files
+
+ -a, --attrs=<file> Load file as YAML and use for variables
+ -D<name>=<value> Define variable <name> as <value>
+ --vars=<ruby> Evaluate <ruby> to Hash and use for variables
+
+ -h, --help Show this help message
Convert markdown to HTML:
$ tilt foo.markdown > foo.html
@@ -26,20 +46,120 @@ Process ERB template:
$ echo "Answer: <%= 2 + 2 %>" | tilt -t erb
Answer: 4
+Process to output file:
+ $ echo "Answer: <%= 2 + 2 %>" > foo.txt.erb
+ $ tilt --files foo.txt.erb
+ foo.txt
+ $ cat foo.txt
+ Answer: 4
+
Define variables:
$ echo "Answer: <%= 2 + n %>" | tilt -t erb --vars="{:n=>40}"
Answer: 42
$ echo "Answer: <%= 2 + n.to_i %>" | tilt -t erb -Dn=40
Answer: 42
+
+To debug:
+
+ $ RUBYOPT=-d tilt ...
+
USAGE
-script_name = File.basename($0)
-pattern = nil
-layout = nil
-locals = {}
+class TiltExe
+ attr_accessor :locals
+ attr_accessor :input_dir
+ attr_accessor :output_dir
+ attr_accessor :layout
+ attr_accessor :pattern
+
+ def initialize
+ @locals = {}
+ @input_dir = Dir.pwd
+ @output_dir = Dir.pwd
+ @layout = nil
+ @pattern = nil
+ end
+
+ def merge(hash, source="locals")
+ unless hash.is_a?(Hash)
+ raise "#{source} must be a Hash, not #{hash.inspect}"
+ end
+
+ hash.each_pair { |key, value| locals[key.to_sym] = value }
+ end
+
+ def engine_for(file)
+ unless file == '-' || File.file?(file)
+ raise "not a file: #{file.inspect}"
+ end
+
+ epattern = pattern ? pattern : file
+ engine = Tilt[epattern]
+
+ if engine.nil?
+ raise "template engine not found for: #{epattern.inspect}"
+ end
+
+ engine
+ end
+
+ # The part of file relative to dir, or nil if file is not relative to dir.
+ def relative_path(file, dir)
+ if file.index(dir) == 0
+ file[dir.length + 1, file.length - dir.length]
+ else
+ nil
+ end
+ end
+
+ def output_file(file)
+ outfile = File.expand_path(file).chomp File.extname(file)
+ outfile = relative_path(outfile, input_dir) || File.basename(outfile)
+ outfile = File.expand_path(outfile, output_dir)
+ outdir = File.dirname(outfile)
+
+ unless File.exists?(outdir)
+ FileUtils.mkdir_p outdir
+ end
+
+ outfile
+ end
+
+ def render_to_file(file)
+ outfile = output_file(file)
+ File.open(outfile, "w") {|io| io << render(file) }
+ relative_path(outfile, Dir.pwd) || outfile
+ end
+
+ def render(file)
+ template = engine_for(file).new(file) {
+ if file == '-'
+ $stdin.read
+ else
+ File.read(file)
+ end
+ }
+
+ output = template.render(Object.new, locals)
+ output = layout.render(Object.new, locals) { output } if layout
+ output
+ end
+end
+
+exe = TiltExe.new
+render_to_stdout = true
ARGV.options do |o|
- o.program_name = script_name
+ o.program_name = File.basename($0)
+
+ # load attributes as YAML
+ o.on("-a", "--attrs=FILE") do |file|
+ require 'yaml'
+ unless File.file?(file)
+ raise "not a file: #{file.inspect}"
+ end
+ exe.merge YAML.load_file(file), "attrs"
+ end
# list all available template engines
o.on("-l", "--list") do
@@ -56,57 +176,71 @@ ARGV.options do |o|
exit
end
+ # render to files, not stdout
+ o.on("-f", "--files") do
+ render_to_stdout = false
+ end
+
+ # set input dir for --files
+ o.on("-i", "--input-dir=DIR") do |dir|
+ exe.input_dir = File.expand_path(dir)
+ end
+
+ # set output dir for --files
+ o.on("-o", "--output-dir=DIR") do |dir|
+ exe.output_dir = File.expand_path(dir)
+ end
+
# the template type / pattern
o.on("-t", "--type=PATTERN", String) do |val|
- abort "unknown template type: #{val}" if Tilt[val].nil?
- pattern = val
+ exe.pattern = val
end
# pass template output into the specified layout template
o.on("-y", "--layout=FILE", String) do |file|
paths = [file, "~/.tilt/#{file}", "/etc/tilt/#{file}"]
- layout = paths.
- map { |p| File.expand_path(p) }.
- find { |p| File.exist?(p) }
- abort "no such layout: #{file}" if layout.nil?
+ layout = paths.map { |p| File.expand_path(p) }.find { |p| File.exist?(p) }
+ exe.layout = exe.engine_for(layout || file).new(layout)
end
# define a local variable
o.on("-D", "--define=PAIR", String) do |pair|
key, value = pair.split(/[=:]/, 2)
- locals[key.to_sym] = value
+ exe.merge(key => value)
end
# define local variables using a Ruby hash
o.on("--vars=RUBY") do |ruby|
- hash = eval(ruby)
- abort "vars must be a Hash, not #{hash.inspect}" if !hash.is_a?(Hash)
- hash.each { |key, value| locals[key.to_sym] = value }
+ exe.merge eval(ruby), "vars"
end
o.on_tail("-h", "--help") { puts usage; exit }
- o.parse!
-end
+end.parse!
-file = ARGV.first || '-'
-pattern = file if pattern.nil?
-abort "template type not given. see: #{$0} --help" if ['-', ''].include?(pattern)
+if ARGV.empty?
+ ARGV.unshift('-')
+end
-engine = Tilt[pattern]
-abort "template engine not found for: #{pattern}" if engine.nil?
+ARGV.each do |file|
+ case
+ when render_to_stdout
+ $stdout.write exe.render(file)
+ when file == '-'
+ while infile = $stdin.gets
+ infile.strip!
-template =
- engine.new(file) {
- if file == '-'
- $stdin.read
- else
- File.read(file)
+ unless infile.empty?
+ $stdout.puts exe.render_to_file(infile)
+ end
end
- }
-output = template.render(self, locals)
-
-# process layout
-output = Tilt.new(layout).render(self, locals) { output } if layout
+ else
+ $stdout.puts exe.render_to_file(file)
+ end
+end
-$stdout.write(output)
+rescue
+ raise if $DEBUG
+ $stderr.puts "#{$!.message} (see '#{File.basename($0)} --help')"
+ exit 1
+end
View
354 test/tilt_exe_test.rb
@@ -0,0 +1,354 @@
+require 'contest'
+require 'fileutils'
+
+# Test Strategy:
+#
+# These tests are setup to create a directory for each test method, named
+# like 'PROJECT_DIR/tmp/METHOD_NAME'. It is one of many ways to make a
+# little sandbox for the tests, so that any created files easily accessible
+# to the developer. By default they will be removed in the teardown method.
+# Set the environment variable KEEP_OUTPUTS=true to prevent their removal.
+#
+class TiltExeTest < Test::Unit::TestCase
+ PROJECT_DIR = File.expand_path("../..", __FILE__)
+ TMP_DIR = File.join(PROJECT_DIR, "tmp")
+ TILT = "ruby -I'#{PROJECT_DIR}/lib' '#{PROJECT_DIR}/bin/tilt'"
+
+ # A test-specific directory
+ attr_accessor :method_dir
+
+ # An accessor for the output of sh
+ attr_accessor :output
+
+ def setup
+ super
+ @pwd = Dir.pwd
+ @method_dir = File.join(TMP_DIR, method_name)
+ @output = nil
+
+ FileUtils.rm_r(method_dir) if File.exists?(method_dir)
+ FileUtils.mkdir_p(method_dir)
+ end
+
+ def teardown
+ Dir.chdir(@pwd)
+ unless ENV['KEEP_OUTPUTS'] == "true"
+ FileUtils.rm_r(method_dir) if File.exists?(method_dir)
+ FileUtils.rm_r(TMP_DIR) if Dir["#{TMP_DIR}/*"].empty?
+ end
+ super
+ end
+
+ unless instance_methods.include?('method_name')
+ # MiniTest uses __name__ instead of method_name
+ def method_name
+ __name__
+ end
+ end
+
+ # Returns the command to execute the tilt exe.
+ def tilt
+ TILT
+ end
+
+ # Execute the shell command and assert the exit status. Sets output.
+ def sh(cmd, expected_status=0)
+ @output = `#{cmd}`
+ assert_equal expected_status, $?.exitstatus, "$ #{cmd}\n#{@output}"
+ end
+
+ # The path to a file under the method dir.
+ def path(file)
+ File.expand_path(file, method_dir)
+ end
+
+ # Create a file relative to the method dir with the specified content.
+ # Creates directories as needed.
+ def prepare(file, content=nil)
+ file = File.expand_path(file, method_dir)
+ dir = File.dirname(file)
+
+ unless File.exists?(dir)
+ FileUtils.mkdir_p(dir)
+ end
+
+ File.open(file, "w") {|io| io << content.to_s }
+ file
+ end
+
+ #
+ # -l, --list
+ #
+
+ %w{-l --list}.each do |opt|
+ test "#{opt} prints template engines" do
+ sh %{#{tilt} #{opt}}
+ assert_match(/String\s+str/, output)
+ end
+ end
+
+ #
+ # -t, --type=<pattern>
+ #
+
+ %w{-t --type}.each do |opt|
+ test "#{opt} sets template engine for template read via stdin" do
+ sh %{echo "Answer: <%= 2 + 2 %>2" | #{tilt} #{opt} erb}
+ assert_equal "Answer: 42\n", output
+ end
+
+ test "#{opt} prints error message on unknown type" do
+ template = prepare 'template.erb', "<%= 2 + 2 %>2"
+ sh %{#{tilt} -t 'unknown' '#{template}' 2>&1}, 1
+ assert_equal "template engine not found for: \"unknown\" (see 'tilt --help')\n", output
+ end
+ end
+
+ #
+ # -y, --layout=<file>
+ #
+
+ %w{-y --layout}.each do |opt|
+ test "#{opt} renders into template" do
+ template = prepare 'template.erb', "<%= 2 + 2 %>2"
+ layout = prepare 'layout.erb', "Answer: <%= yield %>\n"
+
+ sh %{#{tilt} #{opt} '#{layout}' '#{template}'}
+ assert_equal "Answer: 42\n", output
+ end
+
+ test "#{opt} prints error message for non-file" do
+ template = prepare 'template.erb', "<%= 2 + 2 %>2"
+ assert_equal false, File.exists?('not_a_file')
+
+ sh %{#{tilt} #{opt} not_a_file '#{template}' 2>&1}, 1
+ assert_equal "not a file: \"not_a_file\" (see 'tilt --help')\n", output
+ end
+ end
+
+ #
+ # -f, --files
+ #
+
+ %w{-f --files}.each do |opt|
+ test "#{opt} outputs file named as relative path to input file minus extname" do
+ Dir.chdir(method_dir)
+ template = prepare 'path/to/template.txt.erb', "Answer: <%= 2 + 2 %>2\n"
+
+ sh %{#{tilt} #{opt} '#{template}'}
+ assert_equal "path/to/template.txt\n", output
+ assert_equal "Answer: 42\n", File.read('path/to/template.txt')
+ end
+
+ test "#{opt} uses input file basename for files not relative to self" do
+ FileUtils.mkdir_p path('a')
+ FileUtils.mkdir_p path('b')
+
+ template = prepare 'a/path/to/template.txt.erb', "Answer: <%= 2 + 2 %>2\n"
+ Dir.chdir path('b')
+
+ sh %{#{tilt} #{opt} '#{template}'}
+ assert_equal "template.txt\n", output
+ assert_equal "Answer: 42\n", File.read(path("b/template.txt"))
+ end
+
+ test "#{opt} reads files from stdin on -" do
+ Dir.chdir(method_dir)
+ a = prepare 'a.erb', "A<%= 2 - 1 %>"
+ b = prepare 'b.erb', "B<%= 1 + 1 %>"
+
+ sh %{ls *.erb | #{tilt} #{opt} -}
+ assert_equal "a\nb\n", output
+ assert_equal "A1", File.read('a')
+ assert_equal "B2", File.read('b')
+ end
+
+ test "#{opt} handles multiple files" do
+ Dir.chdir(method_dir)
+ a = prepare 'a.erb', "A<%= 2 - 1 %>"
+ b = prepare 'b.erb', "B<%= 1 + 1 %>"
+ c = prepare 'c.erb', "C<%= 2 + 1 %>"
+
+ sh %{ls c.erb | #{tilt} #{opt} '#{a}' '#{b}' '-'}
+ assert_equal "a\nb\nc\n", output
+ assert_equal "A1", File.read('a')
+ assert_equal "B2", File.read('b')
+ assert_equal "C3", File.read('c')
+ end
+ end
+
+ #
+ # -o, --output-dir=<dir>
+ #
+
+ %w{-o --output-dir}.each do |opt|
+ test "#{opt} sets the output dir for -f" do
+ Dir.chdir(method_dir)
+ template = prepare 'path/to/template.txt.erb', "Answer: <%= 2 + 2 %>2\n"
+
+ sh %{#{tilt} #{opt} output -f '#{template}'}
+ assert_equal "output/path/to/template.txt\n", output
+ assert_equal "Answer: 42\n", File.read('output/path/to/template.txt')
+ end
+ end
+
+ #
+ # -i, --input-dir=<dir>
+ #
+
+ %w{-i --input-dir}.each do |opt|
+ test "#{opt} sets base dir for relative paths" do
+ FileUtils.mkdir_p path('a')
+ FileUtils.mkdir_p path('b')
+
+ template = prepare 'a/path/to/template.txt.erb', "Answer: <%= 2 + 2 %>2\n"
+ Dir.chdir path('b')
+
+ sh %{#{tilt} #{opt} '#{path('a')}' -f '#{template}'}
+ assert_equal "path/to/template.txt\n", output
+ assert_equal "Answer: 42\n", File.read(path("b/path/to/template.txt"))
+ end
+ end
+
+ #
+ # -a, --attrs=<file>
+ #
+
+ %w{-a --attrs}.each do |opt|
+ test "#{opt} loads a YAML file for variables" do
+ attrs = prepare('attrs.yml', "answer: 42")
+
+ sh %{echo "Answer: <%= answer %>" | #{tilt} -t erb #{opt} '#{attrs}'}
+ assert_equal "Answer: 42\n", output
+ end
+
+ test "#{opt} prints error for non-file" do
+ template = prepare 'template.erb', "<%= 2 + 2 %>2"
+ assert_equal false, File.exists?('not_a_file')
+
+ sh %{#{tilt} #{opt} not_a_file '#{template}' 2>&1}, 1
+ assert_equal "not a file: \"not_a_file\" (see 'tilt --help')\n", output
+ end
+
+ test "#{opt} prints error if YAML does not load to a hash" do
+ template = prepare 'template.erb', "<%= 2 + 2 %>2"
+ attrs = prepare('attrs.yml', "42")
+
+ sh %{#{tilt} #{opt} '#{attrs}' '#{template}' 2>&1}, 1
+ assert_equal "attrs must be a Hash, not 42 (see 'tilt --help')\n", output
+ end
+ end
+
+ #
+ # -D<name>=<value>
+ #
+
+ test "-D defines a variable" do
+ sh %{echo "Answer: <%= 2 + n.to_i %>" | #{tilt} -t erb -Dn=40}
+ assert_equal "Answer: 42\n", output
+ end
+
+ #
+ # --vars=<ruby>
+ #
+
+ test "--vars evaluates ruby to variables" do
+ sh %{echo "Answer: <%= 2 + n %>" | #{tilt} -t erb --vars "{:n=>40}"}
+ assert_equal "Answer: 42\n", output
+ end
+
+ test "--vars prints error if ruby does not load to a hash" do
+ template = prepare 'template.erb', "<%= 2 + 2 %>2"
+ attrs = prepare('attrs.yml', "42")
+
+ sh %{#{tilt} --vars '42' '#{template}' 2>&1}, 1
+ assert_equal "vars must be a Hash, not 42 (see 'tilt --help')\n", output
+ end
+
+ #
+ # -h, --help
+ #
+
+ %w{-h --help}.each do |opt|
+ test "#{opt} prints help" do
+ sh %{#{tilt} #{opt}}
+ assert_match(/Usage: tilt/, output)
+ end
+ end
+
+ #
+ # tilt test
+ #
+
+ test "tilt renders input files to stdout" do
+ template = prepare 'template.erb', "Answer: <%= 2 + 2 %>2\n"
+
+ sh %{#{tilt} '#{template}'}
+ assert_equal "Answer: 42\n", output
+ end
+
+ test "tilt reads from stdin on -" do
+ sh %{echo "Answer: <%= 2 + 2 %>2" | #{tilt} -t erb -}
+ assert_equal "Answer: 42\n", output
+ end
+
+ test "tilt renders multiple files" do
+ a = prepare 'a.erb', "A<%= 2 - 1 %>"
+ b = prepare 'b.erb', "B<%= 1 + 1 %>"
+
+ sh %{echo "C<%= 2 + 1 %>" | #{tilt} -t erb '#{a}' '#{b}' -}
+ assert_equal "A1B2C3\n", output
+ end
+
+ test "tilt prints error message for non-file" do
+ assert_equal false, File.exists?('not_a_file')
+ sh %{#{tilt} not_a_file 2>&1}, 1
+ assert_equal "not a file: \"not_a_file\" (see 'tilt --help')\n", output
+
+ sh %{#{tilt} '#{Dir.pwd}' 2>&1}, 1
+ assert_equal "not a file: #{Dir.pwd.inspect} (see 'tilt --help')\n", output
+ end
+
+ test "tilt prints error message on unknown engine" do
+ file = prepare 'unknown.engine'
+ sh %{#{tilt} '#{file}' 2>&1}, 1
+ assert_equal "template engine not found for: #{file.inspect} (see 'tilt --help')\n", output
+ end
+
+ #
+ # documentation tests
+ #
+
+ test "documentation examples" do
+ Dir.chdir(method_dir)
+ # Process ERB template:
+ # $ echo "Answer: <%= 2 + 2 %>" | tilt -t erb
+ # Answer: 4
+ sh %{echo "Answer: <%= 2 + 2 %>" | #{tilt} -t erb}
+ assert_equal "Answer: 4\n", output
+
+ # Process to output file:
+ # $ echo "Answer: <%= 2 + 2 %>" > foo.txt.erb
+ # $ tilt --files foo.txt.erb
+ # foo.txt
+ # $ cat foo.txt
+ # Answer: 4
+ sh %{echo "Answer: <%= 2 + 2 %>" > foo.txt.erb}
+ sh %{#{tilt} --files foo.txt.erb}
+ assert_equal "foo.txt\n", output
+
+ sh %{cat foo.txt}
+ assert_equal "Answer: 4\n", output
+
+ # Define variables:
+ # $ echo "Answer: <%= 2 + n %>" | tilt -t erb --vars="{:n=>40}"
+ # Answer: 42
+ # $ echo "Answer: <%= 2 + n.to_i %>" | tilt -t erb -Dn=40
+ # Answer: 42
+ sh %{echo "Answer: <%= 2 + n %>" | #{tilt} -t erb --vars="{:n=>40}"}
+ assert_equal "Answer: 42\n", output
+
+ sh %{echo "Answer: <%= 2 + n.to_i %>" | #{tilt} -t erb -Dn=40}
+ assert_equal "Answer: 42\n", output
+ end
+end
Something went wrong with that request. Please try again.