Adding 'yadr' command for managing plugins (wip)

This commit is contained in:
yan
2011-12-07 20:34:25 -08:00
parent 0741bb0a71
commit acb81bb874
36 changed files with 3874 additions and 15 deletions

View File

@@ -0,0 +1,280 @@
git-style-binaries
==================
Ridiculously easy git-style binaries.
This gem uses [`trollop`](http://trollop.rubyforge.org/) for option parsing
## Installation
gem install jashmenn-git-style-binaries --source=http://gems.github.com
## Screencast
Checkout <a href="http://www.xcombinator.com/movies/git-style-binaries.mov">the new screencast!</a>
<a href="http://www.xcombinator.com/movies/git-style-binaries.mov"><img src="http://github.com/jashmenn/git-style-binaries/tree/master/doc/gsb-screencast.png?raw=true" width='880' height='784' border=0></a>
## Try it out
cd `gem env gemdir`/gems/jashmenn-git-style-binaries-0.1.4/test/fixtures
./wordpress -h
./wordpress help post
## Goal
Lets use the imaginary `wordpress` gem. Let's say we have three different
actions we want to specify:
* categories
* list
* post
Each command has its own binary in a directory structure like this:
bin/
|-- wordpress
|-- wordpress-categories
|-- wordpress-list
`-- wordpress-post
The goal is to be able to call commands in this manner:
wordpress -h # gives help summary of all commands
wordpress-list -h # gives long help of wordpress-list
wordpress list -h # ditto
echo "about me" | wordpress-post --title="new post" # posts a new post with that title
## Example code
Our `bin/wordpress` binary is called the *primary* . Our primary only needs to contain the following line:
#!/usr/bin/env ruby
require 'git-style-binary/command'
`git-style-binary` will automatically make this command the primary.
The `bin/wordpress-post` binary could contain the following:
#!/usr/bin/env ruby
require 'git-style-binary/command'
GitStyleBinary.command do
short_desc "create a blog post"
banner <<-EOS
Usage: #{command.full_name} #{all_options_string} {content|STDIN}
Posts content to a wordpress blog
EOS
opt :blog, "short name of the blog to use", :default => 'default'
opt :category, "tag/category. specify multiple times for multiple categories", :type => String, :multi => true
opt :title, "title for the post", :required => true, :type => String
opt :type, "type of the content [html|xhtml|text]", :default => 'html', :type => String
run do |command|
command.die :type, "type must be one of [html|xhtml|text]" unless command.opts[:type] =~ /^(x?html|text)$/i
puts "Subcommand name: #{command.name.inspect}"
puts "Options: #{command.opts.inspect}"
puts "Remaining arguments: #{command.argv.inspect}"
end
end
And so on with the other binaries.
## Running the binaries
Now if we run `wordpress -h` we get the following output:
NAME
wordpress
VERSION
0.0.1 (c) 2009 Nate Murray - local
SYNOPSIS
wordpress [--version] [--test-primary] [--help] [--verbose] COMMAND [ARGS]
SUBCOMMANDS
wordpress-categories
do something with categories
wordpress-help
get help for a specific command
wordpress-list
list blog postings
wordpress-post
create a blog post
See 'wordpress help COMMAND' for more information on a specific command.
OPTIONS
-v, --verbose
verbose
-t, --test-primary=<s>
test an option on the primary
-e, --version
Print version and exit
-h, --help
Show this message
Default **options**, **version string**, and **usage banner** are automatically selected for you.
The subcommands and their short descriptions are loaded automatically!
You can pass the `-h` flag to any one of the subcommands (with or without the
connecting `-`) or use the built-in `help` subcommand for the same effect. For instance:
$ wordpress help post
NAME
wordpress-post - create a blog post
VERSION
0.0.1 (c) 2009 Nate Murray - local
SYNOPSIS
wordpress-post [--type] [--version] [--test-primary] [--blog] [--help] [--verbose] [--category]
[--title] COMMAND [ARGS] {content|STDIN}
OPTIONS
-v, --verbose
verbose
-t, --test-primary=<s>
test an option on the primary
-b, --blog=<s>
short name of the blog to use (default: default)
-c, --category=<s>
tag/category. specify multiple times for multiple
categories
-i, --title=<s>
title for the post
-y, --type=<s>
type of the content [html|xhtml|text] (default: html)
-e, --version
Print version and exit
-h, --help
Show this message
For more examples, see the binaries in `test/fixtures/`.
## Primary options
Often you may *want* the primary to have its own set of options. Simply call `GitStyleBinary.primary` with a block like so:
#!/usr/bin/env ruby
require 'git-style-binary/command'
GitStyleBinary.primary do
version "#{command.full_name} 0.0.1 (c) 2009 Nate Murray - local"
opt :test_primary, "a primary string option", :type => String
run do |command|
puts "Primary Options: #{command.opts.inspect}"
end
end
Primary options are **inherited** by all subcommands. That means in this case
all subcommands will now get the `--test-primary` option available to them as
well as this new `version` string.
## Option parsing
Option parsing is done by [trollop](http://trollop.rubyforge.org/).
`git-style-binary` uses this more-or-less exactly. See the [trollop
documentation](http://trollop.rubyforge.org/) for information on how to setup
the options and flags.
## Callbacks
Callbacks are available on the primary and subcommands. Available callbacks currently
are before/after_run. These execute before the run block of the command parser and take
take one argument, which is the command itself
## The `run` block
To get the 'introspection' on the individual binaries every binary is `load`ed
on `primary help`. We need a way to get that information while not running
every command when calling `primary help`. To achieve that you need to put what
will be run in the `run` block.
`run` `yields` a `Command` object which contains a number of useful options
such as `name`, `full_name`, `opts`, and `argv`.
* `command.opts` is a hash of the options parsed
* `command.argv` is an array of the remaining arguments
## Features
* automatic colorization
* automatic paging
## To Learn more
Play with the examples in the `test/fixtures` directory.
## Credits
* `git-style-binary` was written by Nate Murray `<nate@natemurray.com>`
* `trollop` was written by [William Morgan](http://trollop.rubyforge.org/)
* Inspiration comes from Ari Lerner's [git-style-binaries](http://blog.xnot.org/2008/12/16/git-style-binaries/) for [PoolParty.rb](http://poolpartyrb.com)
* [`colorize.rb`](http://colorize.rubyforge.org) by Michal Kalbarczyk
* Automatic less paging by [Nathan Weizenbaum](http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby)
* Color inspiration from [Brian Henderson](http://xcombinator.com) teaching me how to get `man git` colors using `less` on MacOSX
## TODO
* automagic tab completion - Automatic for subcommands and options for any library that uses this
## Known Bugs/Problems
* Young
* A few places of really ugly code
* A feeling that this could be done in 1/2 lines of code
## Authors
By Nate Murray and Ari Lerner
## Copyright
The MIT License
Copyright (c) 2009 Nate Murray. See LICENSE for details.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,65 @@
require 'rubygems'
require 'rake'
begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "git-style-binaries"
gem.description = %Q{Ridiculously easy git-style binaries}
gem.summary =<<-EOF
Add git-style binaries to your project easily.
EOF
gem.email = "nate@natemurray.com"
gem.homepage = "http://github.com/jashmenn/git-style-binaries"
gem.authors = ["Nate Murray"]
gem.add_dependency 'trollop'
gem.add_dependency 'shoulda' # for running the tests
excludes = /(README\.html)/
gem.files = (FileList["[A-Z]*.*", "{bin,examples,generators,lib,rails,spec,test,vendor}/**/*", 'Rakefile', 'LICENSE*']).delete_if{|f| f =~ excludes}
gem.extra_rdoc_files = FileList["README*", "ChangeLog*", "LICENSE*"].delete_if{|f| f =~ excludes}
end
rescue LoadError
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
end
require 'rake/testtask'
Rake::TestTask.new(:test) do |test|
test.libs << 'lib' << 'test'
test.pattern = 'test/**/*_test.rb'
test.verbose = true
end
begin
require 'rcov/rcovtask'
Rcov::RcovTask.new do |test|
test.libs << 'test'
test.pattern = 'test/**/*_test.rb'
test.verbose = true
end
rescue LoadError
task :rcov do
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
end
end
task :default => :test
require 'rake/rdoctask'
require 'yaml'
Rake::RDocTask.new do |rdoc|
if File.exist?('VERSION.yml')
config = YAML.load(File.read('VERSION.yml'))
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
else
version = ""
end
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "git-style-binaries #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
task :bump => ['version:bump:patch', 'gemspec', 'build']

View File

@@ -0,0 +1,4 @@
---
:patch: 10
:major: 0
:minor: 1

View File

@@ -0,0 +1,198 @@
#
# Colorize String class extension.
#
class String
#
# Version string
#
COLORIZE_VERSION = '0.5.6'
#
# Colors Hash
#
COLORS = {
:black => 0,
:red => 1,
:green => 2,
:yellow => 3,
:blue => 4,
:magenta => 5,
:cyan => 6,
:white => 7,
:default => 9,
:light_black => 10,
:light_red => 11,
:light_green => 12,
:light_yellow => 13,
:light_blue => 14,
:light_magenta => 15,
:light_cyan => 16,
:light_white => 17
}
#
# Modes Hash
#
MODES = {
:default => 0, # Turn off all attributes
#:bright => 1, # Set bright mode
:underline => 4, # Set underline mode
:blink => 5, # Set blink mode
:swap => 7, # Exchange foreground and background colors
:hide => 8 # Hide text (foreground color would be the same as background)
}
protected
#
# Set color values in new string intance
#
def set_color_parameters( params )
if (params.instance_of?(Hash))
@color = params[:color]
@background = params[:background]
@mode = params[:mode]
@uncolorized = params[:uncolorized]
self
else
nil
end
end
public
#
# Change color of string
#
# Examples:
#
# puts "This is blue".colorize( :blue )
# puts "This is light blue".colorize( :light_blue )
# puts "This is also blue".colorize( :color => :blue )
# puts "This is blue with red background".colorize( :color => :light_blue, :background => :red )
# puts "This is blue with red background".colorize( :light_blue ).colorize( :background => :red )
# puts "This is blue text on red".blue.on_red
# puts "This is red on blue".colorize( :red ).on_blue
# puts "This is red on blue and underline".colorize( :red ).on_blue.underline
# puts "This is blue text on red".blue.on_red.blink
#
def colorize( params )
unless STDOUT.use_color
return self unless STDOUT.isatty
end
return self if ENV['NO_COLOR']
begin
require 'Win32/Console/ANSI' if RUBY_PLATFORM =~ /win32/
rescue LoadError
raise 'You must gem install win32console to use color on Windows'
end
color_parameters = {}
if (params.instance_of?(Hash))
color_parameters[:color] = COLORS[params[:color]]
color_parameters[:background] = COLORS[params[:background]]
color_parameters[:mode] = MODES[params[:mode]]
elsif (params.instance_of?(Symbol))
color_parameters[:color] = COLORS[params]
end
color_parameters[:color] ||= @color || 9
color_parameters[:background] ||= @background || 9
color_parameters[:mode] ||= @mode || 0
color_parameters[:uncolorized] ||= @uncolorized || self.dup
# calculate bright mode
color_parameters[:color] += 50 if color_parameters[:color] > 10
color_parameters[:background] += 50 if color_parameters[:background] > 10
return "\033[#{color_parameters[:mode]};#{color_parameters[:color]+30};#{color_parameters[:background]+40}m#{color_parameters[:uncolorized]}\033[0m".set_color_parameters( color_parameters )
end
#
# Return uncolorized string
#
def uncolorize
return @uncolorized || self
end
#
# Return true if sting is colorized
#
def colorized?
return !@uncolorized.nil?
end
#
# Make some color and on_color methods
#
COLORS.each_key do | key |
eval <<-"end_eval"
def #{key.to_s}
return self.colorize( :color => :#{key.to_s} )
end
def on_#{key.to_s}
return self.colorize( :background => :#{key.to_s} )
end
end_eval
end
#
# Methods for modes
#
MODES.each_key do | key |
eval <<-"end_eval"
def #{key.to_s}
return self.colorize( :mode => :#{key.to_s} )
end
end_eval
end
class << self
#
# Return array of available modes used by colorize method
#
def modes
keys = []
MODES.each_key do | key |
keys << key
end
keys
end
#
# Return array of available colors used by colorize method
#
def colors
keys = []
COLORS.each_key do | key |
keys << key
end
keys
end
#
# Display color matrix with color names.
#
def color_matrix( txt = "[X]" )
size = String.colors.length
String.colors.each do | color |
String.colors.each do | back |
print txt.colorize( :color => color, :background => back )
end
puts " < #{color}"
end
String.colors.reverse.each_with_index do | back, index |
puts "#{"|".rjust(txt.length)*(size-index)} < #{back}"
end
end
end
end

View File

@@ -0,0 +1,16 @@
class Object
def returning(value)
yield(value)
value
end unless Object.respond_to?(:returning)
end
class Symbol
def to_proc
Proc.new { |*args| args.shift.__send__(self, *args) }
end
end
class IO
attr_accessor :use_color
end

View File

@@ -0,0 +1,88 @@
$:.unshift(File.dirname(__FILE__))
require 'rubygems'
# Load the vendor gems
$:.unshift(File.dirname(__FILE__) + "/../vendor/gems")
%w(trollop).each do |library|
begin
require "#{library}/lib/#{library}"
rescue LoadError
begin
require 'trollop'
rescue LoadError
puts "There was an error loading #{library}. Try running 'gem install #{library}' to correct the problem"
end
end
end
require 'ext/core'
require 'ext/colorize'
require 'git-style-binary/autorunner'
Dir[File.dirname(__FILE__) + "/git-style-binary/helpers/*.rb"].each {|f| require f}
module GitStyleBinary
class << self
include Helpers::NameResolver
attr_accessor :current_command
attr_accessor :primary_command
attr_writer :known_commands
# If set to false GitStyleBinary will not automatically run at exit.
attr_writer :run
# Automatically run at exit?
def run?
@run ||= false
end
def parser
@p ||= Parser.new
end
def known_commands
@known_commands ||= {}
end
def load_primary
unless @loaded_primary
@loaded_primary = true
primary_file = File.join(binary_directory, basename)
load primary_file
if !GitStyleBinary.primary_command # you still dont have a primary load a default
GitStyleBinary.primary do
run do |command|
educate
end
end
end
end
end
def load_subcommand
unless @loaded_subcommand
@loaded_subcommand = true
cmd_file = GitStyleBinary.binary_filename_for(GitStyleBinary.current_command_name)
load cmd_file
end
end
def load_command_file(name, file)
self.name_of_command_being_loaded = name
load file
self.name_of_command_being_loaded = nil
end
# UGLY eek
attr_accessor :name_of_command_being_loaded
end
end
at_exit do
unless $! || GitStyleBinary.run?
command = GitStyleBinary::AutoRunner.run
exit 0
end
end

View File

@@ -0,0 +1,21 @@
require 'git-style-binary/parser'
module GitStyleBinary
class AutoRunner
def self.run(argv=ARGV)
r = new
r.run
end
def run
unless GitStyleBinary.run?
if !GitStyleBinary.current_command
GitStyleBinary.load_primary
end
GitStyleBinary.current_command.run
end
end
end
end

View File

@@ -0,0 +1,204 @@
require 'git-style-binary'
module GitStyleBinary
def self.command(&block)
returning Command.new(:constraints => [block]) do |c|
c.name ||= (GitStyleBinary.name_of_command_being_loaded || GitStyleBinary.current_command_name)
GitStyleBinary.known_commands[c.name] = c
if !GitStyleBinary.current_command || GitStyleBinary.current_command.is_primary?
GitStyleBinary.current_command = c
end
end
end
def self.primary(&block)
returning Primary.new(:constraints => [block]) do |c|
c.name ||= (GitStyleBinary.name_of_command_being_loaded || GitStyleBinary.current_command_name)
GitStyleBinary.known_commands[c.name] = c
GitStyleBinary.primary_command = c unless GitStyleBinary.primary_command
GitStyleBinary.current_command = c unless GitStyleBinary.current_command
end
end
class Command
class << self
def defaults
lambda do
name_desc "#{command.full_name}\#{command.short_desc ? ' - ' + command.short_desc : ''}" # eval jit
version_string = defined?(VERSION) ? VERSION : "0.0.1"
version "#{version_string} (c) #{Time.now.year}"
banner <<-EOS
#{"SYNOPSIS".colorize(:red)}
#{command.full_name.colorize(:light_blue)} #{all_options_string}
#{"SUBCOMMANDS".colorize(:red)}
\#{GitStyleBinary.pretty_known_subcommands.join("\n ")}
See '#{command.full_name} help COMMAND' for more information on a specific command.
EOS
opt :verbose, "verbose", :default => false
end
end
end
attr_reader :constraints
attr_reader :opts
attr_accessor :name
def initialize(o={})
o.each do |k,v|
eval "@#{k.to_s}= v"
end
end
def parser
@parser ||= begin
p = Parser.new
p.command = self
p
end
end
def constraints
@constraints ||= []
end
def run
GitStyleBinary.load_primary unless is_primary?
GitStyleBinary.load_subcommand if is_primary? && running_subcommand?
load_all_parser_constraints
@opts = process_args_with_subcmd
call_parser_run_block
self
end
def running_subcommand?
GitStyleBinary.valid_subcommand?(GitStyleBinary.current_command_name)
end
def load_all_parser_constraints
@loaded_all_parser_constraints ||= begin
load_parser_default_constraints
load_parser_primary_constraints
load_parser_local_constraints
true
end
end
def load_parser_default_constraints
parser.consume_all([self.class.defaults])
end
def load_parser_primary_constraints
parser.consume_all(GitStyleBinary.primary_command.constraints)
end
def load_parser_local_constraints
cur = GitStyleBinary.current_command # see, why isn't 'this' current_command?
unless self.is_primary? && cur == self
# TODO TODO - the key lies in this function. figure out when you hav emore engergy
# soo UGLY. see #process_parser! unify with that method
# parser.consume_all(constraints) rescue ArgumentError
parser.consume_all(cur.constraints)
end
end
def call_parser_run_block
runs = GitStyleBinary.current_command.parser.runs
parser.run_callbacks(:before_run, self)
parser.runs.last.call(self) # ... not too happy with this
parser.run_callbacks(:after_run, self)
end
def process_args_with_subcmd(args = ARGV, *a, &b)
cmd = GitStyleBinary.current_command_name
vals = process_args(args, *a, &b)
parser.leftovers.shift if parser.leftovers[0] == cmd
vals
end
# TOOooootally ugly! why? bc load_parser_local_constraints doesn't work
# when loading the indivdual commands because it depends on
# #current_command. This really sucks and is UGLY.
# the todo is to put in 'load_all_parser_constraints' and this works
def process_parser!
# load_all_parser_constraints
load_parser_default_constraints
load_parser_primary_constraints
# load_parser_local_constraints
parser.consume_all(constraints)
# hack
parser.consume {
opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"]
opt :help, "Show this message" unless @specs[:help] || @long["help"]
resolve_default_short_options
} # hack
end
def process_args(args = ARGV, *a, &b)
p = parser
begin
vals = p.parse args
args.clear
p.leftovers.each { |l| args << l }
vals # ugly todo
rescue Trollop::CommandlineError => e
$stderr.puts "Error: #{e.message}."
$stderr.puts "Try --help for help."
exit(-1)
rescue Trollop::HelpNeeded
p.educate
exit
rescue Trollop::VersionNeeded
puts p.version
exit
end
end
def is_primary?
false
end
def argv
parser.leftovers
end
def short_desc
parser.short_desc
end
def full_name
# ugly, should be is_primary?
GitStyleBinary.primary_name == name ? GitStyleBinary.primary_name : GitStyleBinary.primary_name + "-" + name
end
def die arg, msg=nil
p = parser # create local copy
Trollop.instance_eval { @p = p }
Trollop::die(arg, msg)
end
# Helper to return the option
def [](k)
opts[k]
end
end
class Primary < Command
def is_primary?
true
end
def primary
self
end
end
end

View File

@@ -0,0 +1,32 @@
module GitStyleBinary
module Commands
class Help
# not loving this syntax, but works for now
GitStyleBinary.command do
short_desc "get help for a specific command"
run do |command|
# this is slightly ugly b/c it has to muck around in the internals to
# get information about commands other than itself. This isn't a
# typical case
self.class.send :define_method, :educate_about_command do |name|
load_all_commands
if GitStyleBinary.known_commands.has_key?(name)
cmd = GitStyleBinary.known_commands[name]
cmd.process_parser!
cmd.parser.educate
else
puts "Unknown command '#{name}'"
end
end
if command.argv.size > 0
command.argv.first == "help" ? educate : educate_about_command(command.argv.first)
else
educate
end
end
end
end
end
end

View File

@@ -0,0 +1,78 @@
module GitStyleBinary
module Helpers
module NameResolver
def basename(filename=zero)
File.basename(filename).match(/(.*?)(\-|$)/).captures.first
end
alias_method :primary_name, :basename
# checks the bin directory for all files starting with +basename+ and
# returns an array of strings specifying the subcommands
def subcommand_names(filename=zero)
subfiles = Dir[File.join(binary_directory, basename + "-*")]
cmds = subfiles.collect{|file| File.basename(file).sub(/^#{basename}-/, '')}.sort
cmds += built_in_command_names
cmds.uniq
end
def binary_directory(filename=zero)
File.dirname(filename)
end
def built_in_commands_directory
File.dirname(__FILE__) + "/../commands"
end
def built_in_command_names
Dir[built_in_commands_directory + "/*.rb"].collect{|f| File.basename(f.sub(/\.rb$/,''))}
end
def list_subcommands(filename=zero)
subcommand_names(filename).join(", ")
end
# load first from users binary directory. then load built-in commands if
# available
def binary_filename_for(name)
user_file = File.join(binary_directory, "#{basename}-#{name}")
return user_file if File.exists?(user_file)
built_in = File.join(built_in_commands_directory, "#{name}.rb")
return built_in if File.exists?(built_in)
user_file
end
def current_command_name(filename=zero,argv=ARGV)
current = File.basename(zero)
first_arg = ARGV[0]
return first_arg if valid_subcommand?(first_arg)
return basename if basename == current
current.sub(/^#{basename}-/, '')
end
# returns the command name with the prefix if needed
def full_current_command_name(filename=zero,argv=ARGV)
cur = current_command_name(filename, argv)
subcmd = cur == basename(filename) ? false : true # is this a subcmd?
"%s%s%s" % [basename(filename), subcmd ? "-" : "", subcmd ? current_command_name(filename, argv) : ""]
end
def valid_subcommand?(name)
subcommand_names.include?(name)
end
def zero
$0
end
def pretty_known_subcommands(theme=:long)
GitStyleBinary.known_commands.collect do |k,cmd|
next if k == basename
cmd.process_parser!
("%-s%s%-10s" % [basename, '-', k]).colorize(:light_blue) + ("%s " % [theme == :long ? "\n" : ""]) + ("%s" % [cmd.short_desc]) + "\n"
end.compact.sort
end
end
end
end

View File

@@ -0,0 +1,37 @@
module GitStyleBinary
module Helpers
module Pager
# by Nathan Weizenbaum - http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby
def run_pager
return if RUBY_PLATFORM =~ /win32/
return unless STDOUT.tty?
STDOUT.use_color = true
read, write = IO.pipe
unless Kernel.fork # Child process
STDOUT.reopen(write)
STDERR.reopen(write) if STDERR.tty?
read.close
write.close
return
end
# Parent process, become pager
STDIN.reopen(read)
read.close
write.close
ENV['LESS'] = 'FSRX' # Don't page if the input is short enough
Kernel.select [STDIN] # Wait until we have input before we start the pager
pager = ENV['PAGER'] || 'less -erXF'
exec pager rescue exec "/bin/sh", "-c", pager
end
module_function :run_pager
end
end
end

View File

@@ -0,0 +1,223 @@
module GitStyleBinary
class Parser < Trollop::Parser
attr_reader :runs, :callbacks
attr_reader :short_desc
attr_accessor :command
def initialize *a, &b
super
@runs = []
setup_callbacks
end
def setup_callbacks
@callbacks = {}
%w(run).each do |event|
%w(before after).each do |time|
@callbacks["#{time}_#{event}".to_sym] = []
instance_eval "def #{time}_#{event}(&block);@callbacks[:#{time}_#{event}] << block;end"
end
end
end
def run_callbacks(at, from)
@callbacks[at].each {|c| c.call(from) }
end
def banner s=nil; @banner = s if s; @banner end
def short_desc s=nil; @short_desc = s if s; @short_desc end
def name_desc s=nil; @name_desc = s if s; @name_desc end
# Set the theme. Valid values are +:short+ or +:long+. Default +:long+
attr_writer :theme
def theme
@theme ||= :long
end
## Adds text to the help display.
def text s; @order << [:text, s] end
def spec_names
@specs.collect{|name, spec| spec[:long]}
end
# should probably be somewhere else
def load_all_commands
GitStyleBinary.subcommand_names.each do |name|
cmd_file = GitStyleBinary.binary_filename_for(name)
GitStyleBinary.load_command_file(name, cmd_file)
end
end
## Print the help message to 'stream'.
def educate(stream=$stdout)
load_all_commands
width # just calculate it now; otherwise we have to be careful not to
# call this unless the cursor's at the beginning of a line.
GitStyleBinary::Helpers::Pager.run_pager
self.send("educate_#{theme}", stream)
end
def educate_long(stream=$stdout)
left = {}
@specs.each do |name, spec|
left[name] =
((spec[:short] ? "-#{spec[:short]}, " : "") +
"--#{spec[:long]}" +
case spec[:type]
when :flag; ""
when :int; "=<i>"
when :ints; "=<i+>"
when :string; "=<s>"
when :strings; "=<s+>"
when :float; "=<f>"
when :floats; "=<f+>"
end).colorize(:red)
end
leftcol_width = left.values.map { |s| s.length }.max || 0
rightcol_start = leftcol_width + 6 # spaces
leftcol_start = 6
leftcol_spaces = " " * leftcol_start
unless @order.size > 0 && @order.first.first == :text
if @name_desc
stream.puts "NAME".colorize(:red)
stream.puts "#{leftcol_spaces}"+ colorize_known_words(eval(%Q["#{@name_desc}"])) + "\n"
stream.puts
end
if @version
stream.puts "VERSION".colorize(:red)
stream.puts "#{leftcol_spaces}#@version\n"
end
stream.puts
banner = colorize_known_words_array(wrap(eval(%Q["#{@banner}"]) + "\n", :prefix => leftcol_start)) if @banner # lazy banner
stream.puts banner
stream.puts
stream.puts "OPTIONS".colorize(:red)
else
stream.puts "#@banner\n" if @banner
end
@order.each do |what, opt|
if what == :text
stream.puts wrap(opt)
next
end
spec = @specs[opt]
stream.printf " %-#{leftcol_width}s\n", left[opt]
desc = spec[:desc] +
if spec[:default]
if spec[:desc] =~ /\.$/
" (Default: #{spec[:default]})"
else
" (default: #{spec[:default]})"
end
else
""
end
stream.puts wrap(" %s" % [desc], :prefix => leftcol_start, :width => width - rightcol_start - 1 )
stream.puts
stream.puts
end
end
def educate_short(stream=$stdout)
left = {}
@specs.each do |name, spec|
left[name] = "--#{spec[:long]}" +
(spec[:short] ? ", -#{spec[:short]}" : "") +
case spec[:type]
when :flag; ""
when :int; " <i>"
when :ints; " <i+>"
when :string; " <s>"
when :strings; " <s+>"
when :float; " <f>"
when :floats; " <f+>"
end
end
leftcol_width = left.values.map { |s| s.length }.max || 0
rightcol_start = leftcol_width + 6 # spaces
leftcol_start = 0
unless @order.size > 0 && @order.first.first == :text
stream.puts "#@version\n" if @version
stream.puts colorize_known_words_array(wrap(eval(%Q["#{@banner}"]) + "\n", :prefix => leftcol_start)) if @banner # jit banner
stream.puts "Options:"
else
stream.puts "#@banner\n" if @banner
end
@order.each do |what, opt|
if what == :text
stream.puts wrap(opt)
next
end
spec = @specs[opt]
stream.printf " %#{leftcol_width}s: ", left[opt]
desc = spec[:desc] +
if spec[:default]
if spec[:desc] =~ /\.$/
" (Default: #{spec[:default]})"
else
" (default: #{spec[:default]})"
end
else
""
end
stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
end
end
def colorize_known_words_array(txts)
txts.collect{|txt| colorize_known_words(txt)}
end
def colorize_known_words(txt)
txt = txt.gsub(/^([A-Z]+\s*)$/, '\1'.colorize(:red)) # all caps words on their own line
txt = txt.gsub(/\b(#{bin_name})\b/, '\1'.colorize(:light_blue)) # the current command name
txt = txt.gsub(/\[([^\s]+)\]/, "[".colorize(:magenta) + '\1'.colorize(:green) + "]".colorize(:magenta)) # synopsis options
end
def consume(&block)
cloaker(&block).bind(self).call
end
def consume_all(blocks)
blocks.each {|b| consume(&b)}
end
def bin_name
GitStyleBinary.full_current_command_name
end
def all_options_string
# '#{spec_names.collect(&:to_s).collect{|name| "[".colorize(:magenta) + "--" + name + "]".colorize(:magenta)}.join(" ")} COMMAND [ARGS]'
'#{spec_names.collect(&:to_s).collect{|name| "[" + "--" + name + "]"}.join(" ")} COMMAND [ARGS]'
end
def run(&block)
@runs << block
end
def action(name = :action, &block)
block.call(self) if block
end
end
end

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env ruby
$:.unshift(File.dirname(__FILE__) + "/../../lib")
VERSION="0.0.2" # just to test it
require 'git-style-binary/command'

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env ruby
$:.unshift(File.dirname(__FILE__) + "/../../lib")
require 'git-style-binary/command'
GitStyleBinary.command do
short_desc "download a flickr image"
banner <<-EOS
SYNOPSIS
#{command.full_name} #{all_options_string} url
Downloads an image from flickr
EOS
run do |command|
puts "would download: #{command.argv.inspect}"
end
end

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env ruby
$:.unshift(File.dirname(__FILE__) + "/../../lib")
require 'git-style-binary/command'
GitStyleBinary.primary do
version "0.0.1 (c) 2009 Nate Murray - local"
opt :test_primary, "test an option on the primary", :type => String
action do
@categories = ["sports", "news"]
end
before_run do |cmd|
puts "before_run command #{cmd}"
end
after_run do |cmd|
puts "after_run command #{cmd}"
end
run do |command|
puts "Primary Options: #{command.opts.inspect}"
end
end
# OR
# require 'git-style-binary/primary'
# command = GitStyleBinary::primary("wordpress") do
# version "#{$0} 0.0.1 (c) 2009 Nate Murray"
# banner <<-EOS
# usage: #{$0} #{all_options.collect(:&to_s).join(" ")} COMMAND [ARGS]
#
# The wordpress subcommands commands are:
# {subcommand_names.pretty_print}
#
# See 'wordpress help COMMAND' for more information on a specific command.
# EOS
# opt :verbose, "verbose", :default => false
# opt :dry, "dry run", :default => false
# opt :test_global, "a basic global string option", :type => String
# end

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env ruby
$:.unshift(File.dirname(__FILE__) + "/../../lib")
require 'git-style-binary/command'
GitStyleBinary.command do
short_desc "do something with categories"
banner <<-EOS
SYNOPSIS
#{command.full_name} #{all_options_string}
Does something with categories
EOS
run do |command|
puts "does something with categories"
puts @categories.join(" ")
end
end

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env ruby
$:.unshift(File.dirname(__FILE__) + "/../../lib")
require 'git-style-binary/command'
GitStyleBinary.command do
short_desc "list blog postings"
banner <<-EOS
SYNOPSIS
#{command.full_name} #{all_options_string}
Lists the posts on the blog
EOS
run do |command|
puts "listing blog posts"
end
end

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env ruby
$:.unshift(File.dirname(__FILE__) + "/../../lib")
require 'git-style-binary/command'
GitStyleBinary.command do
short_desc "create a blog post"
banner <<-EOS
SYNOPSIS
#{command.full_name} #{all_options_string} {content|STDIN}
EOS
opt :blog, "short name of the blog to use", :default => 'default'
opt :category, "tag/category. specify multiple times for multiple categories", :type => String, :multi => true
opt :title, "title for the post", :required => true, :type => String
opt :type, "type of the content [html|xhtml|text]", :default => 'html', :type => String
run do |command|
command.die :type, "type must be one of [html|xhtml|text]" unless command.opts[:type] =~ /^(x?html|text)$/i
puts "Subcommand name: #{command.name.inspect}"
puts "Options: #{command.opts.inspect}"
puts "Remaining arguments: #{command.argv.inspect}"
end
end

View File

@@ -0,0 +1,17 @@
require File.dirname(__FILE__) + "/../test_helper.rb"
require 'git-style-binary/command'
class CommandTest < Test::Unit::TestCase
context "cmd" do
setup do
@c = GitStyleBinary::Command.new
end
should "be able to easily work with constraints" do
assert_equal @c.constraints, []
@c.constraints << "foo"
assert_equal @c.constraints, ["foo"]
end
end
end

View File

@@ -0,0 +1,21 @@
require File.dirname(__FILE__) + "/test_helper.rb"
class GitStyleBinariesTest < Test::Unit::TestCase
context "parsing basenames" do
should "accurately parse basenames" do
assert_equal "wordpress", GitStyleBinary.basename("bin/wordpress")
assert_equal "wordpress", GitStyleBinary.basename("bin/wordpress-post")
assert_equal "wordpress", GitStyleBinary.basename("wordpress-post")
end
should "get the current command name" do
# doesn't really apply any more b/c it calls 'current' which is never the
# current when your running rake_test_loader.rb
#
# assert_equal "wordpress", GitStyleBinary.current_command_name("bin/wordpress", ["--help"])
# assert_equal "post", GitStyleBinary.current_command_name("bin/wordpress-post", ["--help"])
# assert_equal "post", GitStyleBinary.current_command_name("bin/wordpress post", ["--help"])
#assert_equal "post", GitStyleBinary.current_command_name("bin/wordpress post", [])
end
end
end

View File

@@ -0,0 +1,224 @@
require File.dirname(__FILE__) + "/test_helper.rb"
THIS_YEAR=Time.now.year # todo
class RunningBinariesTest < Test::Unit::TestCase
include RunsBinaryFixtures
context "when running primary" do
["wordpress -h", "wordpress help"].each do |format|
context "and getting help as a '#{format}'" do
setup { @stdout, @stderr = bin(format) }
should "have the command name and short description" do
unless format == "wordpress -h" # doesn't apply to wordpress -h
output_matches /NAME\n\s*wordpress\-help \- get help for a specific command/m
end
end
should "have a local (not default) version string" do
output_matches /0\.0\.1 \(c\) 2009 Nate Murray - local/
end
should "get a list of subcommands" do
output_matches /subcommands/mi
end
should "have subcommand short descriptions" do
output_matches /post\s*create a blog post/
output_matches /categories\s*do something with categories/
output_matches /help\s*get help for a specific command/
output_matches /list\s*list blog postings/
end
should "have a usage" do
output_matches /SYNOPSIS/i
output_matches /wordpress(\-help)? \[/
end
should "be able to ask for help about help"
end
end
context "and getting help as subcommand" do
# ["wordpress -h", "wordpress help"].each do |format|
["wordpress help"].each do |format|
context "'#{format}'" do
should "get help on subcommand post"
end
end
end
context "with no options" do
setup { @stdout, @stderr = bin("wordpress") }
should "output the options" do
output_matches /Primary Options:/
end
should "have the test_primary option" do
output_matches /test_primary=>nil/
end
end
should "be able to require 'primary' and run just fine"
end
context "when running with an action" do
# should be the same for both formats
["wordpress-categories", "wordpress categories"].each do |bin_format|
context "#{bin_format}" do
context "with action block" do
setup { @stdout, @stderr = bin("#{bin_format}") }
should "have the parsed action items in the help output" do
output_matches /sports news/m
end
end
end
end
end
context "callbacks" do
context "on a binary" do
setup { @stdout, @stderr = bin("wordpress") }
%w(before after).each do |time|
should "run the callback #{time}_run}" do
assert @stdout.match(/#{time}_run command/)
end
end
end
context "on help" do
setup { @stdout, @stderr = bin("wordpress -h") }
%w(before after).each do |time|
should "not run the callback #{time}_run" do
assert_nil @stdout.match(/#{time}_run command/)
end
end
end
end
context "when running the subcommand" do
# should be the same for both formats
["wordpress-post", "wordpress post"].each do |bin_format|
context "#{bin_format}" do
context "with no options" do
setup { @stdout, @stderr = bin("#{bin_format}") }
should "fail because title is required" do
output_matches /Error: option 'title' must be specified.\s*Try --help for help/m
end
end
context "with options" do
setup { @stdout, @stderr = bin("#{bin_format} --title='glendale'") }
should "be running the subcommand's run block" do
output_matches /Subcommand name/
end
should "have some default options" do
output_matches /version=>false/
output_matches /help=>false/
end
should "have some primary options" do
output_matches /test_primary=>nil/
end
should "have some local options" do
output_matches /title=>"glendale"/
output_matches /type=>"html"/
end
end
context "testing die statements" do
setup { @stdout, @stderr = bin("#{bin_format} --title='glendale' --type=yaml") }
should "die on invalid options" do
output_matches /argument \-\-type type must be one of \[html\|xhtml\|text\]/
end
end
end # end bin_format
end # end #each
end
["wordpress help post", "wordpress post -h"].each do |format|
context "when calling '#{format}'" do
setup { @stdout, @stderr = bin(format) }
should "have a description" do
output_matches /create a blog post/
end
should "have the proper usage line" do
output_matches /SYNOPSIS\n\s*wordpress\-post/m
output_matches /\[--title\]/
end
should "have option flags" do
output_matches /\-\-title(.*)<s>/
end
should "have primary option flags" do
output_matches /\-\-test-primary(.*)<s>/
end
should "have default option flags" do
output_matches /\-\-verbose/
end
should "have trollop default option flags" do
output_matches /\-e, \-\-version/
end
should "have the correct binary name and short description" do
output_matches /NAME\n\s*wordpress\-post \- create a blog post/m
end
should "have a the primaries version string" do
output_matches /0\.0\.1 \(c\) 2009 Nate Murray - local/
end
should "have options" do
output_matches /Options/i
output_matches /\-b, \-\-blog=<s>/
output_matches /short name of the blog to use/
output_matches /-i, \-\-title=<s>/
output_matches /title for the post/
end
end
end
context "when running a bare primary" do
["flickr -h", "flickr help"].each do |format|
context format do
setup { @stdout, @stderr = bin(format) }
should "have the name and short description" do
unless format == "flickr -h" # hmm
output_matches /NAME\n\s*flickr\-help \- get help for a specific command/m
end
end
should "have a local (not default) version string" do
output_matches /0\.0\.2 \(c\) #{Time.now.year}/
end
end
end
["flickr-download -h", "flickr download -h"].each do |format|
context format do
setup { @stdout, @stderr = bin(format) }
should "match on usage" do
output_matches /SYNOPSIS\n\s*flickr\-download/m
end
end
end
end
end

View File

@@ -0,0 +1,13 @@
class Test::Unit::TestCase
def output_should_match(regexp)
assert_match regexp, @stdout + @stderr
end
alias_method :output_matches, :output_should_match
def stdout_should_match(regexp)
assert_match regexp, @stdout
end
def stderr_should_match(regexp)
assert_match regexp, @stderr
end
end

View File

@@ -0,0 +1,28 @@
require 'rubygems'
require 'test/unit'
require 'shoulda'
begin require 'redgreen'; rescue LoadError; end
require 'open3'
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
$LOAD_PATH.unshift(File.dirname(__FILE__))
Dir[File.join(File.dirname(__FILE__), "shoulda_macros", "*.rb")].each {|f| require f}
ENV['NO_COLOR'] = "true"
require 'git-style-binary'
GitStyleBinary.run = true
class Test::Unit::TestCase
def fixtures_dir
File.join(File.dirname(__FILE__), "fixtures")
end
end
module RunsBinaryFixtures
# run the specified cmd returning the string values of [stdout,stderr]
def bin(cmd)
stdin, stdout, stderr = Open3.popen3("#{fixtures_dir}/#{cmd}")
[stdout.read, stderr.read]
end
end