# This code is free software; you can redistribute it and/or modify it under the
# terms of the new BSD License.
#
# Copyright (c) 2009, Sebastian Staudt
require 'singleton'
require 'yaml'
require 'rubikon/action'
require 'rubikon/exceptions'
module Rubikon
version = YAML.load_file(File.join(File.dirname(__FILE__), '..', '..', 'VERSION.yml'))
VERSION = "#{version[:major]}.#{version[:minor]}.#{version[:patch]}"
# The main class of Rubikon. Let your own application class inherit from this
# one.
class Application
include Singleton
attr_reader :settings
# Initialize with default settings (see set for more detail)
#
# If you really need to override this in your application class, be sure to
# call +super+
def initialize
@actions = {}
@aliases = {}
@default = nil
@settings = {
:autorun => true,
:dashed_options => true,
:help_banner => "Usage: #{$0}",
:istream => $stdin,
:name => self.class.to_s,
:ostream => $stdout,
:raise_errors => false
}
end
# Define an Application Action
#
# +name+:: The name of the action. Used as an option parameter.
# +options+:: A Hash of options to be used on the created Action
# (default: {})
# +block+:: A block containing the code that should be executed when this
# Action is called, i.e. when the Application is called with
# the associated option parameter
def action(name, options = {}, &block)
raise "No block given" unless block_given?
key = name
key = "--#{key}" if @settings[:dashed_options]
@actions[key.to_sym] = Action.new(name, options, &block)
end
# Define an alias to an Action
#
# +name+:: The name of the alias
# +action+:: The name of the Action that should be aliased
#
# Example:
#
# action_alias :doit, :dosomething
def action_alias(name, action)
@aliases[name.to_sym] = action.to_sym
end
# Define the default Action of the Application
#
# +options+:: A Hash of options to be used on the created Action
# (default: {})
# +block+:: A block containing the code that should be executed when this
# Action is called, i.e. when no option is given to the
# Application
def default(options = {}, &block)
@default = Action.new(:default, options, &block)
end
# Prompts the user for input
#
# If +prompt+ is not empty this will display a prompt using
# prompt.to_s.
#
# +prompt+:: A String or other Object responding to +to_s+ used for
# displaying a prompt to the user (default: '')
#
# Example:
#
# action 'interactive' do
# # Display a prompt "Please type something: "
# user_provided_value = input 'Please type something'
#
# # Do something with the data
# ...
# end
def input(prompt = '')
unless prompt.to_s.empty?
ostream << "#{prompt}: "
end
@settings[:istream].gets[0..-2]
end
# Convenience method for accessing the user-defined output stream
#
# Use this if you want to work directly with the output stream
#
# Example:
#
# ostream.flush
def ostream
@settings[:ostream]
end
# Output text using +IO#<<+ of the output stream
#
# +text+:: The text to write into the output stream
def put(text)
ostream << text
ostream.flush
end
# Output a character using +IO#putc+ of the output stream
#
# +char+:: The character to write into the output stream
def putc(char)
ostream.putc char
end
# Output a line of text using +IO#puts+ of the output stream
#
# +text+:: The text to write into the output stream
def puts(text)
ostream.puts text
end
# Run this application
#
# +args+:: The command line arguments that should be given to the
# application as options
#
# Calling this method explicitly is not required when you want to create a
# simple application (having one main class inheriting from
# Rubikon::Application). But it's useful for testing or if you want to have
# some sort of sub-applications.
def run(args = ARGV)
begin
assign_aliases unless @aliases.empty?
action_results = []
if !@default.nil? and args.empty?
action_results << @default.run
else
parse_options(args).each do |action, args|
action_results << @actions[action].run(*args)
end
end
rescue
if @settings[:raise_errors]
raise $!
else
puts "Error:\n #{$!.message}"
puts " #{$!.backtrace.join("\n ")}" if $DEBUG
exit 1
end
end
action_results
end
# Sets an application setting
#
# +setting+:: The name of the setting to change, will be symbolized first.
# +value+:: The value the setting should be changed to
#
# Available settings
# +autorun+:: If true, let the application run as soon as its class
# is defined
# +dashed_options+:: If true, each option is prepended with a double-dash
# (--)
# +help_banner+:: Defines a banner for the help message (unused)
# +istream+:: Defines an input stream to use
# +name+:: Defines the name of the application
# +ostream+:: Defines an output stream to use
# +raise_errors+:: If true, raise errors, otherwise fail gracefully
#
# Example:
#
# set :name, 'My App'
# set :autorun, false
def set(setting, value)
@settings[setting.to_sym] = value
end
# Displays a throbber while the given block is executed
#
# Example:
#
# action 'slow' do
# throbber do
# # Add some long running code here
# ...
# end
# end
def throbber(&block)
spinner = '-\|/'
current_ostream = ostream
@settings[:ostream] = StringIO.new
code_thread = Thread.new { block.call }
throbber_thread = Thread.new do
i = 0
current_ostream.putc 32
while code_thread.alive?
current_ostream.putc 8
current_ostream.putc spinner[i]
current_ostream.flush
i = (i + 1) % 4
sleep 0.25
end
current_ostream.putc 8
end
code_thread.join
throbber_thread.join
current_ostream << ostream.string
@settings[:ostream] = current_ostream
end
private
# Returns whether this application should be ran automatically
def self.autorun?
instance.settings[:autorun] || false
end
# Enables autorun functionality using Kernel#at_exit
#
# +subclass+:: The subclass inheriting from Application. This is the user's
# application.
#
# This is called automatically when subclassing Application.
def self.inherited(subclass)
Singleton.__init__(subclass)
at_exit { subclass.run if subclass.autorun? }
end
# This is used for convinience. Method calls on the class itself are
# relayed to the singleton instance.
#
# +method_name+:: The name of the method being called
# +args+:: Any arguments that are given to the method
# +block+:: A block that may be given to the method
#
# This is called automatically when calling methods on the class.
def self.method_missing(method_name, *args, &block)
instance.send(method_name, *args, &block)
end
# Relay putc to the instance method
#
# This is used to hide Kernel#putc so that the Application's
# output IO object is used for printing text
#
# +text+:: The text to write into the output stream
def self.putc(text)
instance.putc text
end
# Relay puts to the instance method
#
# This is used to hide Kernel#puts so that the Application's
# output IO object is used for printing text
#
# +text+:: The text to write into the output stream
def self.puts(text)
instance.puts text
end
# Assigns aliases to the actions that have been defined using action_alias
#
# Clears the aliases Hash afterwards
def assign_aliases
@aliases.each do |key, action|
if @settings[:dashed_options]
action = "--#{action}".to_sym
key = "--#{key}".to_sym
end
unless @actions.key? key
@actions[key] = @actions[action]
else
warn "There's already an action called \"#{key}\"."
end
end
@aliases = {}
end
# Parses the options used when starting the application
#
# +options+:: An Array of Strings that should be used as application
# options. Usually +ARGV+ is used for this.
def parse_options(options)
actions_to_call = {}
last_action = nil
options.each do |option|
option_sym = option.to_sym
if @actions.keys.include? option_sym
actions_to_call[option_sym] = []
last_action = option_sym
elsif last_action.nil? || (option.is_a?(String) && @settings[:dashed_options] && option[0..1] == '--')
raise UnknownOptionError.new(option)
else
actions_to_call[last_action] << option
end
end
actions_to_call
end
end
end