# -*- ruby -*- #encoding: utf-8 require 'loggability' require 'tty/prompt' require 'pastel' require 'gli' require 'arborist' unless defined?( Arborist ) require 'arborist/mixins' # The command-line interface to Arborist. module Arborist::CLI extend Arborist::MethodUtilities, Loggability, GLI::App # Write logs to Arborist's logger log_to :arborist # # GLI # # Set up global[:description] and options program_desc 'Arborist' # The command version version Arborist::VERSION # Use an OpenStruct for options instead of a Hash # use_openstruct( true ) # Subcommand options are independent of global[:ones] subcommand_option_handling :normal # Strict argument validation arguments :strict # Custom parameter types accept Array do |value| value.strip.split(/\s*,\s*/) end accept Pathname do |value| Pathname( value.strip ) end # Global options desc "Load the specified CONFIGFILE." arg_name :CONFIGFILE flag [:c, :config], type: Pathname desc 'Enable debugging output' switch [:d, :debug] desc 'Enable verbose output' switch [:v, :verbose] desc 'Set log level to LEVEL (one of %s)' % [Loggability::LOG_LEVELS.keys.join(', ')] arg_name :LEVEL flag [:l, :loglevel], must_match: Loggability::LOG_LEVELS.keys desc "Don't actually do anything, just show what would happen." switch [:n, 'dry-run'] desc "Enable ANSI colors." switch :color, default_value: true desc "Additional Ruby libs to require before doing anything." flag [:r, 'requires'], type: Array # # GLI Event callbacks # # Set up global options pre do |global, command, options, args| self.set_logging_level( global[:l] ) # Include a 'lib' directory if there is one $LOAD_PATH.unshift( 'lib' ) if File.directory?( 'lib' ) self.require_additional_libs( global[:r] ) if global[:r] self.load_config( global ) self.set_logging_level( global[:l] ) if global[:l] # again; override config file self.setup_output( global ) self.setup_pastel_aliases true end # Write the error to the log on exceptions. on_error do |exception| case exception when OptionParser::ParseError, GLI::CustomExit self.log.debug( exception ) else self.log.error( exception ) end exception.backtrace.each {|frame| self.log.debug(frame) } true end ## # Registered subcommand modules singleton_attr_accessor :subcommand_modules ### Overridden -- Add registered subcommands immediately before running. def self::run( * ) self.add_registered_subcommands super end ### Add the specified +mod+ule containing subcommands to the 'arborist' command. def self::register_subcommands( mod ) self.subcommand_modules ||= [] self.subcommand_modules.push( mod ) mod.extend( GLI::DSL, GLI::AppSupport, Loggability ) mod.log_to( :arborist ) end ### Add the commands from the registered subcommand modules. def self::add_registered_subcommands self.subcommand_modules ||= [] self.subcommand_modules.each do |mod| merged_commands = mod.commands.merge( self.commands ) self.commands.update( merged_commands ) command_objs = self.commands_declaration_order | self.commands.values self.commands_declaration_order.replace( command_objs ) end end ### Return the Pastel colorizer. ### def self::pastel @pastel ||= Pastel.new( enabled: $stdout.tty? && $COLOR ) end ### Return the TTY prompt used by the command to communicate with the ### user. def self::prompt @prompt ||= TTY::Prompt.new end ### Discard the existing HighLine prompt object if one existed. Mostly useful for ### testing. def self::reset_prompt @prompt = nil end ### Set the global logging +level+ if it's defined. def self::set_logging_level( level=nil ) if level Loggability.level = level.to_sym else Loggability.level = :fatal end end ### Load any additional Ruby libraries given with the -r global option. def self::require_additional_libs( requires) requires.each do |path| path = "arborist/#{path}" unless path.start_with?( 'arborist/' ) require( path ) end end ### Setup pastel color aliases ### def self::setup_pastel_aliases self.pastel.alias_color( :headline, :bold, :white, :on_black ) self.pastel.alias_color( :success, :bold, :green ) self.pastel.alias_color( :error, :bold, :red ) self.pastel.alias_color( :up, :green ) self.pastel.alias_color( :down, :red ) self.pastel.alias_color( :unknown, :dark, :yellow ) self.pastel.alias_color( :disabled, :dark, :white ) self.pastel.alias_color( :quieted, :dark, :green ) self.pastel.alias_color( :acked, :yellow ) self.pastel.alias_color( :warn, :bold, :magenta ) self.pastel.alias_color( :highlight, :bold, :yellow ) self.pastel.alias_color( :search_hit, :black, :on_white ) self.pastel.alias_color( :prompt, :cyan ) self.pastel.alias_color( :even_row, :bold ) self.pastel.alias_color( :odd_row, :reset ) end ### Load the config file using either arborist-base's config-loader if available, or ### fall back to DEFAULT_CONFIG_FILE def self::load_config( global={} ) Arborist.load_config( global[:c] ) # Set up the logging formatter Loggability.format_with( :color ) if $stdout.tty? end ### Set up the output levels and globals based on the associated +global+ options. def self::setup_output( global ) # Turn on Ruby debugging and/or verbosity if specified if global[:n] $DRYRUN = true Loggability.level = :warn else $DRYRUN = false end if global[:verbose] $VERBOSE = true Loggability.level = :info end if global[:debug] $DEBUG = true Loggability.level = :debug end $COLOR = global[ :color ] end # # GLI subcommands # # Convenience module for subcommand registration syntax sugar. module Subcommand ### Extension callback -- register the extending object as a subcommand. def self::extended( mod ) Arborist::CLI.log.debug "Registering subcommands from %p" % [ mod ] Arborist::CLI.register_subcommands( mod ) 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 ############### module_function ############### ### Exit with the specified +exit_code+ after printing the given +message+. def exit_now!( message, exit_code=1 ) raise GLI::CustomExit.new( message, exit_code ) end ### Exit with a helpful +message+ and display the usage. def help_now!( message=nil ) exception = OptionParser::ParseError.new( message ) def exception.exit_code; 64; end raise exception end ### Get the prompt (a TTY::Prompt object) def prompt return Arborist::CLI.prompt end ### Return the global Pastel object for convenient formatting, color, etc. def hl return Arborist::CLI.pastel end ### Return the specified +string+ in the 'headline' ANSI color. def headline_string( string ) return hl.headline( string ) end ### Return the specified +string+ in the 'highlight' ANSI color. def highlight_string( string ) return hl.highlight( string ) end ### Return the specified +string+ in the 'success' ANSI color. def success_string( string ) return hl.success( string ) end ### Return the specified +string+ in the 'error' ANSI color. def error_string( string ) return hl.error( string ) end ### Output a table with the given +header+ (an array) and +rows+ ### (an array of arrays). def display_table( header, rows ) table = TTY::Table.new( header, rows ) renderer = nil if hl.enabled? renderer = TTY::Table::Renderer::Unicode.new( table, multiline: true, padding: [0,1,0,1] ) renderer.border.style = :dim else renderer = TTY::Table::Renderer::ASCII.new( table, multiline: true, padding: [0,1,0,1] ) end puts renderer.render end ### Return the count of visible (i.e., non-control) characters in the given +string+. def visible_chars( string ) return string.to_s.gsub(/\e\[.*?m/, '').scan( /\P{Cntrl}/ ).size end ### In dry-run mode, output the description instead of running the provided block and ### return the +return_value+. ### If dry-run mode is not enabled, yield to the block. def unless_dryrun( description, return_value=true ) if $DRYRUN self.log.warn( "DRYRUN> #{description}" ) return return_value else return yield end end alias_method :unless_dry_run, :unless_dryrun end # module Subcommand ### Load commands from any files in the specified directory relative to LOAD_PATHs def self::commands_from( subdir ) Gem.find_latest_files( File.join(subdir, '*.rb') ).each do |rbfile| self.log.debug " loading %s..." % [ rbfile ] require( rbfile ) end end commands_from 'arborist/command' end # class Arborist::CLI