require 'etc'
require 'optparse'
require 'gli/dsl'
require 'pathname'
module GLI
# A means to define and parse a command line interface that works as
# Git's does, in that you specify global options, a command name, command
# specific options, and then command arguments.
module App
include DSL
include AppSupport
# Loads ruby files in the load path that start with
# +path+, which are presumed to be commands for your executable.
# This is useful for decomposing your bin file into different classes, but
# can also be used as a plugin mechanism, allowing users to provide additional
# commands for your app at runtime. All that being said, it's basically
# a glorified +require+.
#
# path:: a path from which to load .rb
files that, presumably, contain commands. If this is an absolute path,
# any files in that path are loaded. If not, it is interpretted as relative to somewhere
# in the LOAD_PATH
.
#
# == Example:
#
# # loads *.rb from your app's install - great for decomposing your bin file
# commands_from "my_app/commands"
#
# # loads *.rb files from the user's home dir - great and an extension/plugin mechanism
# commands_from File.join(ENV["HOME"],".my_app","plugins")
def commands_from(path)
if Pathname.new(path).absolute? and File.exists?(path)
load_commands(path)
else
$LOAD_PATH.each do |load_path|
commands_path = File.join(load_path,path)
load_commands(commands_path)
end
end
end
# Describe the overall application/programm. This should be a one-sentence summary
# of what your program does that will appear in the help output.
#
# +description+:: A String of the short description of your program's purpose
def program_desc(description=nil)
if description
@program_desc = description
end
@program_desc
end
# Provide a longer description of the program. This can be as long as needed, and use double-newlines
# for paragraphs. This will show up in the help output.
#
# description:: A String for the description
def program_long_desc(description=nil)
if description
@program_long_desc = description
end
@program_long_desc
end
# Provide a flag to choose whether to hide or not from the help the undescribed commands.
# By default the undescribed commands will be shown in the help.
#
# hide:: A Bool for hide the undescribed commands
def hide_commands_without_desc(hide=nil)
unless hide.nil?
@hide_commands_without_desc = hide
end
@hide_commands_without_desc || false
end
# Use this if the following command should not have the pre block executed.
# By default, the pre block is executed before each command and can result in
# aborting the call. Using this will avoid that behavior for the following command
def skips_pre
@skips_pre = true
end
# Use this if the following command should not have the post block executed.
# By default, the post block is executed after each command.
# Using this will avoid that behavior for the following command
def skips_post
@skips_post = true
end
# Use this if the following command should not have the around block executed.
# By default, the around block is executed, but for commands that might not want the
# setup to happen, this can be handy
def skips_around
@skips_around = true
end
# Sets that this app uses a config file as well as the name of the config file.
#
# +filename+:: A String representing the path to the file to use for the config file. If it's an absolute
# path, this is treated as the path to the file. If it's *not*, it's treated as relative to the user's home
# directory as produced by File.expand_path('~')
.
def config_file(filename)
if filename =~ /^\//
@config_file = filename
else
@config_file = File.join(File.expand_path(ENV['HOME']),filename)
end
commands[:initconfig] = InitConfig.new(@config_file,commands,flags,switches)
@commands_declaration_order << commands[:initconfig]
@config_file
end
# Define a block to run after command line arguments are parsed
# but before any command is run. If this block raises an exception
# the command specified will not be executed.
# The block will receive the global-options,command,options, and arguments
# If this block evaluates to true, the program will proceed; otherwise
# the program will end immediately and exit nonzero
def pre(&a_proc)
@pre_block = a_proc
end
# Define a block to run after the command was executed, only
# if there was not an error.
# The block will receive the global-options,command,options, and arguments
def post(&a_proc)
@post_block = a_proc
end
# This inverts the pre/post concept. This is useful when you have a global shared resource that is governed by a block
# instead of separate open/close methods. The block you pass here will be given four parameters:
#
# global options:: the parsed global options
# command:: The GLI::Command that the user is going to invoke
# options:: the command specific options
# args:: unparsed command-line args
# code:: a block that you must +call+ to execute the command.
#
# #help_now! and #exit_now! work as expected; you can abort the command call by simply not calling it.
#
# You can declare as many #around blocks as you want. They will be called in the order in which they are defined.
#
# Note that if you declare #around blocks, #pre and #post blocks will still work. The #pre is called first, followed by
# the around, followed by the #post.
#
# Call #skips_around before a command that should not have this hook fired
def around(&a_proc)
@around_blocks ||= []
@around_blocks << a_proc
end
# Define a block to run if an error occurs.
# The block will receive any Exception that was caught.
# It should evaluate to false to avoid the built-in error handling (which basically just
# prints out a message). GLI uses a variety of exceptions that you can use to find out what
# errors might've occurred during command-line parsing:
# * GLI::CustomExit
# * GLI::UnknownCommandArgument
# * GLI::UnknownGlobalArgument
# * GLI::UnknownCommand
# * GLI::BadCommandLine
def on_error(&a_proc)
@error_block = a_proc
end
# Indicate the version of your application
#
# +version+:: String containing the version of your application.
def version(version)
@version = version
desc 'Display the program version'
switch :version, :negatable => false
end
# By default, GLI mutates the argument passed to it. This is
# consistent with +OptionParser+, but be less than ideal. Since
# that value, for scaffolded apps, is +ARGV+, you might want to
# refer to the entire command-line via +ARGV+ and thus not want it mutated.
def preserve_argv(preserve=true)
@preserve_argv = preserve
end
# Call this with +true+ will cause the +global_options+ and
# +options+ passed to your code to be wrapped in
# Options, which is a subclass of +OpenStruct+ that adds
# [] and []= methods.
#
# +use_openstruct+:: a Boolean indicating if we should use OpenStruct instead of Hashes
def use_openstruct(use_openstruct)
@use_openstruct = use_openstruct
end
# Configure a type conversion not already provided by the underlying OptionParser.
# This works more or less like the OptionParser version.
#
# object:: the class (or whatever) that triggers the type conversion
# block:: the block that will be given the string argument and is expected
# to return the converted value
#
# Example
#
# accept(Hash) do |value|
# result = {}
# value.split(/,/) do |pair|
# k,v = pair.split(/:/)
# result[k] = v
# end
# result
# end
#
# flag :properties, :type => Hash
def accept(object,&block)
accepts[object] = block
end
# Simpler means of exiting with a custom exit code. This will
# raise a CustomExit with the given message and exit code, which will ultimatley
# cause your application to exit with the given exit_code as its exit status
# Use #help_now! if you want to show the help in addition to the error message
#
# message:: message to show the user
# exit_code:: exit code to exit as, defaults to 1
def exit_now!(message,exit_code=1)
raise CustomExit.new(message,exit_code)
end
# Exit now, showing the user help for the command they executed. Use #exit_now! to just show the error message
#
# message:: message to indicate how the user has messed up the CLI invocation or nil to just simply show help
def help_now!(message=nil)
exception = OptionParser::ParseError.new(message)
class << exception
def exit_code; 64; end
end
raise exception
end
# Control how commands and options are sorted in help output. By default, they are sorted alphabetically.
#
# sort_type:: How you want help commands/options sorted:
# +:manually+:: help commands/options are ordered in the order declared.
# +:alpha+:: sort alphabetically (default)
def sort_help(sort_type)
@help_sort_type = sort_type
end
# Set how help text is wrapped.
#
# wrap_type:: Symbol indicating how you'd like text wrapped:
# +:to_terminal+:: Wrap text based on the width of the terminal (default)
# +:verbatim+:: Format text exactly as it was given to the various methods. This is useful if your output has
# formatted output, e.g. ascii tables and you don't want it messed with.
# +:one_line+:: Do not wrap text at all. This will bring all help content onto one line, removing any newlines
# +:tty_only+:: Wrap like +:to_terminal+ if this output is going to a TTY, otherwise don't wrap (like +:one_line+)
def wrap_help_text(wrap_type)
@help_text_wrap_type = wrap_type
end
# Control how the SYNOPSIS is formatted.
#
# format:: one of:
# +:full+:: the default, show subcommand options and flags inline
# +:terminal+:: if :full would be wider than the terminal, use :compact
# +:compact+:: use a simpler and shorter SYNOPSIS. Useful if your app has a lot of options and showing them in the SYNOPSIS makes things more confusing
def synopsis_format(format)
@synopsis_format_type = format
end
def program_name(override=nil) #:nodoc:
warn "#program_name has been deprecated"
end
# Sets a default command to run when none is specified on the command line. Note that
# if you use this, you won't be able to pass arguments, flags, or switches
# to the command when run in default mode. All flags and switches are treated
# as global, and any argument will be interpretted as the command name and likely
# fail.
#
# +command+:: Command as a Symbol to run as default
def default_command(command)
@default_command = command.to_sym
end
# How to handle subcommand options. In general, you want to set this to +:normal+, which
# treats each subcommand as establishing its own namespace for options. This is what
# the scaffolding should generate, but it is *not* what GLI 2.5.x and lower apps had as a default.
# To maintain backwards compatibility, the default is +:legacy+, which is that all subcommands of
# a particular command share a namespace for options, making it impossible for two subcommands
# to have options of the same name.
def subcommand_option_handling(handling_strategy)
@subcommand_option_handling_strategy = handling_strategy
end
# How to handle argument validation.
#
# handling_strategy:: One of:
# +:loose+:: no argument validation. Use of `arg` or `arg_name` is for documentation purposes only. (Default)
# +:strict+:: arguments are validated according to their specification. +action+ blocks may assume
# the value of `arguments` matches the specification provided in `arg`. Note that to use
# this strategy, you must also be sure that +subcommand_option_handling+ is set.
def arguments(handling_strategy)
@argument_handling_strategy = handling_strategy
end
private
def load_commands(path)
if File.exists? path
Dir.entries(path).sort.each do |entry|
file = File.join(path,entry)
if file =~ /\.rb$/
require file
end
end
end
end
end
end