├── .tool-versions ├── test ├── apps │ ├── todo_legacy │ │ ├── Gemfile │ │ ├── lib │ │ │ └── todo │ │ │ │ ├── version.rb │ │ │ │ └── commands │ │ │ │ ├── create.rb │ │ │ │ ├── ls.rb │ │ │ │ └── list.rb │ │ ├── README.rdoc │ │ ├── todo.rdoc │ │ ├── test │ │ │ └── tc_nothing.rb │ │ ├── Rakefile │ │ ├── todo.gemspec │ │ └── bin │ │ │ └── todo │ ├── todo │ │ ├── Gemfile │ │ ├── lib │ │ │ └── todo │ │ │ │ ├── version.rb │ │ │ │ └── commands │ │ │ │ ├── ls.rb │ │ │ │ ├── create.rb │ │ │ │ ├── make.rb │ │ │ │ └── list.rb │ │ ├── README.rdoc │ │ ├── todo.rdoc │ │ ├── test │ │ │ └── tc_nothing.rb │ │ ├── Rakefile │ │ ├── todo.gemspec │ │ └── bin │ │ │ └── todo │ ├── README.md │ └── todo_plugins │ │ └── commands │ │ └── third.rb ├── unit │ ├── init_simplecov.rb │ ├── support │ │ ├── gli_test_config.yml │ │ └── fake_std_out.rb │ ├── test_helper.rb │ ├── compound_command_test.rb │ ├── verbatim_wrapper_test.rb │ ├── options_test.rb │ ├── switch_test.rb │ ├── command_finder_test.rb │ ├── flag_test.rb │ ├── terminal_test.rb │ ├── subcommands_test.rb │ ├── doc_test.rb │ └── subcommand_parsing_test.rb └── integration │ ├── scaffold_test.rb │ ├── test_helper.rb │ ├── gli_powered_app_test.rb │ └── gli_cli_test.rb ├── bin ├── setup ├── ci ├── gli └── rake ├── object-model.png ├── lib ├── gli │ ├── version.rb │ ├── argument.rb │ ├── commands │ │ ├── help_modules │ │ │ ├── compact_synopsis_formatter.rb │ │ │ ├── verbatim_wrapper.rb │ │ │ ├── one_line_wrapper.rb │ │ │ ├── terminal_synopsis_formatter.rb │ │ │ ├── list_formatter.rb │ │ │ ├── tty_only_wrapper.rb │ │ │ ├── help_completion_format.rb │ │ │ ├── arg_name_formatter.rb │ │ │ ├── text_wrapper.rb │ │ │ ├── options_formatter.rb │ │ │ ├── command_finder.rb │ │ │ ├── global_help_format.rb │ │ │ ├── command_help_format.rb │ │ │ └── full_synopsis_formatter.rb │ │ ├── compound_command.rb │ │ ├── initconfig.rb │ │ ├── rdoc_document_listener.rb │ │ ├── help.rb │ │ ├── doc.rb │ │ └── scaffold.rb │ ├── options.rb │ ├── option_parsing_result.rb │ ├── command_line_option.rb │ ├── switch.rb │ ├── command_line_token.rb │ ├── command_finder.rb │ ├── option_parser_factory.rb │ ├── gli_option_block_parser.rb │ ├── exceptions.rb │ ├── flag.rb │ ├── terminal.rb │ ├── command_support.rb │ ├── command.rb │ ├── app_support.rb │ └── gli_option_parser.rb └── gli.rb ├── Gemfile ├── .gitignore ├── gli.gemspec ├── CONTRIBUTING.md ├── .circleci └── config.yml ├── object-model.dot ├── gli.rdoc ├── exe └── gli ├── gli.cheat ├── Rakefile └── README.rdoc /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 2.7.1 2 | -------------------------------------------------------------------------------- /test/apps/todo_legacy/Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec 3 | -------------------------------------------------------------------------------- /test/apps/todo/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | bundle check || bundle install 6 | -------------------------------------------------------------------------------- /test/apps/todo/lib/todo/version.rb: -------------------------------------------------------------------------------- 1 | module Todo 2 | VERSION = '0.0.1' 3 | end 4 | -------------------------------------------------------------------------------- /object-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/gli/main/object-model.png -------------------------------------------------------------------------------- /test/apps/todo_legacy/lib/todo/version.rb: -------------------------------------------------------------------------------- 1 | module Todo 2 | VERSION = '0.0.1' 3 | end 4 | -------------------------------------------------------------------------------- /test/apps/todo/README.rdoc: -------------------------------------------------------------------------------- 1 | = todo 2 | 3 | Describe your project here 4 | 5 | :include:todo.rdoc 6 | 7 | -------------------------------------------------------------------------------- /test/apps/todo_legacy/README.rdoc: -------------------------------------------------------------------------------- 1 | = todo 2 | 3 | Describe your project here 4 | 5 | :include:todo.rdoc 6 | 7 | -------------------------------------------------------------------------------- /lib/gli/version.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | unless const_defined? :VERSION 3 | VERSION = '2.21.0' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/apps/README.md: -------------------------------------------------------------------------------- 1 | Special-built applications for use in tests that will hopefully reveal 2 | various features and edge cases 3 | -------------------------------------------------------------------------------- /test/apps/todo/todo.rdoc: -------------------------------------------------------------------------------- 1 | = todo 2 | 3 | Generate this with 4 | todo rdoc 5 | After you have described your command line interface -------------------------------------------------------------------------------- /test/apps/todo_legacy/todo.rdoc: -------------------------------------------------------------------------------- 1 | = todo 2 | 3 | Generate this with 4 | todo rdoc 5 | After you have described your command line interface -------------------------------------------------------------------------------- /test/apps/todo_plugins/commands/third.rb: -------------------------------------------------------------------------------- 1 | class App 2 | command :third do |c| c.action { |g,o,a| puts "third: #{a.join(',')}" } end 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem "simplecov", "~> 0.6.4", :platforms => :mri_19 6 | gem "psych", :platforms => :mri_19 7 | -------------------------------------------------------------------------------- /test/unit/init_simplecov.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'simplecov' 3 | SimpleCov.start do 4 | add_filter "/test" 5 | end 6 | rescue LoadError 7 | # Don't care 8 | end 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | html 3 | *.sw? 4 | pkg 5 | coverage 6 | .bundle 7 | tmp 8 | Gemfile.lock 9 | *.gem 10 | results.html 11 | .DS_Store 12 | Session.vim 13 | scaffold_test 14 | -------------------------------------------------------------------------------- /test/unit/support/gli_test_config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | commands: 3 | :command: 4 | :g: bar 5 | :s: true 6 | :other_command: 7 | :g: foobar 8 | :f: barfoo 9 | :f: foo 10 | :bleorgh: true 11 | :t: false 12 | -------------------------------------------------------------------------------- /test/apps/todo/test/tc_nothing.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | class TC_testNothing < Test::Unit::TestCase 4 | 5 | def setup 6 | end 7 | 8 | def teardown 9 | end 10 | 11 | def test_the_truth 12 | assert true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/apps/todo_legacy/test/tc_nothing.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | class TC_testNothing < Test::Unit::TestCase 4 | 5 | def setup 6 | end 7 | 8 | def teardown 9 | end 10 | 11 | def test_the_truth 12 | assert true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/unit/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | require "gli" 3 | 4 | module TestHelper 5 | class CLIApp 6 | include GLI::App 7 | 8 | def reset 9 | super 10 | @subcommand_option_handling_strategy = :normal 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/gli/argument.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | class Argument #:nodoc: 3 | 4 | attr_reader :name 5 | attr_reader :options 6 | 7 | def initialize(name,options = []) 8 | @name = name 9 | @options = options 10 | end 11 | 12 | def optional? 13 | @options.include? :optional 14 | end 15 | 16 | def multiple? 17 | @options.include? :multiple 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/gli/commands/help_modules/compact_synopsis_formatter.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | module Commands 3 | module HelpModules 4 | class CompactSynopsisFormatter < FullSynopsisFormatter 5 | 6 | protected 7 | 8 | def sub_options_doc(sub_options) 9 | if sub_options.empty? 10 | '' 11 | else 12 | '[subcommand options]' 13 | end 14 | end 15 | 16 | 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/gli/commands/help_modules/verbatim_wrapper.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | module Commands 3 | module HelpModules 4 | # Leaves text formatting exactly as it was received. Doesn't strip anything. 5 | class VerbatimWrapper 6 | # Args are ignored entirely; this keeps it consistent with the TextWrapper interface 7 | def initialize(width,indent) 8 | end 9 | 10 | def wrap(text) 11 | return String(text) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/apps/todo/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/clean' 2 | require 'rubygems' 3 | require 'rake/gempackagetask' 4 | require 'rdoc/task' 5 | 6 | Rake::RDocTask.new do |rd| 7 | rd.main = "README.rdoc" 8 | rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*") 9 | rd.title = 'Your application title' 10 | end 11 | 12 | spec = eval(File.read('todo.gemspec')) 13 | 14 | Rake::GemPackageTask.new(spec) do |pkg| 15 | end 16 | 17 | require 'rake/testtask' 18 | Rake::TestTask.new do |t| 19 | t.libs << "test" 20 | t.test_files = FileList['test/tc_*.rb'] 21 | end 22 | 23 | task :default => :test 24 | -------------------------------------------------------------------------------- /test/apps/todo_legacy/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/clean' 2 | require 'rubygems' 3 | require 'rake/gempackagetask' 4 | require 'rdoc/task' 5 | 6 | Rake::RDocTask.new do |rd| 7 | rd.main = "README.rdoc" 8 | rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*") 9 | rd.title = 'Your application title' 10 | end 11 | 12 | spec = eval(File.read('todo.gemspec')) 13 | 14 | Rake::GemPackageTask.new(spec) do |pkg| 15 | end 16 | 17 | require 'rake/testtask' 18 | Rake::TestTask.new do |t| 19 | t.libs << "test" 20 | t.test_files = FileList['test/tc_*.rb'] 21 | end 22 | 23 | task :default => :test 24 | -------------------------------------------------------------------------------- /test/unit/support/fake_std_out.rb: -------------------------------------------------------------------------------- 1 | class FakeStdOut 2 | attr_reader :strings 3 | 4 | def initialize 5 | @strings = [] 6 | end 7 | 8 | def puts(string=nil) 9 | @strings << string unless string.nil? 10 | end 11 | 12 | def write(x) 13 | puts(x) 14 | end 15 | 16 | def printf(*args) 17 | puts(Kernel.printf(*args)) 18 | end 19 | 20 | # Returns true if the regexp matches anything in the output 21 | def contained?(regexp) 22 | strings.find{ |x| x =~ regexp } 23 | end 24 | 25 | def flush; end 26 | 27 | def to_s 28 | @strings.join("\n") 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/gli/options.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module GLI 4 | # Subclass of +OpenStruct+ that provides hash-like methods for #[] and #[]=. Note that is is *not* a Hash. 5 | # By using GLI::App#use_openstruct, your options will be coerced into one of these. 6 | class Options < OpenStruct 7 | 8 | # Return the value of an attribute 9 | def[](k) 10 | self.send(k.to_sym) 11 | end 12 | 13 | # Set the value of an attribute 14 | def[]=(k, v) 15 | self.send("#{k.to_sym}=",v) 16 | end 17 | 18 | def map(&block) 19 | @table.map(&block) 20 | end 21 | end 22 | end 23 | 24 | -------------------------------------------------------------------------------- /lib/gli/option_parsing_result.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | class OptionParsingResult 3 | attr_accessor :global_options 4 | attr_accessor :command 5 | attr_accessor :command_options 6 | attr_accessor :arguments 7 | 8 | def convert_to_openstruct! 9 | @global_options = Options.new(@global_options) 10 | @command_options = Options.new(@command_options) 11 | self 12 | end 13 | 14 | # Allows us to splat this object into blocks and methods expecting parameters in this order 15 | def to_a 16 | [@global_options,@command,@command_options,@arguments] 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/unit/compound_command_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class CompoundCommandFinderTest < MiniTest::Test 4 | include TestHelper 5 | 6 | def test_exception_for_missing_commands 7 | @name = "new" 8 | @unknown_name = "create" 9 | @existing_command = OpenStruct.new(:name => @name) 10 | @base = OpenStruct.new( :commands => { @name => @existing_command }) 11 | 12 | @code = lambda { GLI::Commands::CompoundCommand.new(@base,{:foo => [@name,@unknown_name]}) } 13 | 14 | ex = assert_raises(RuntimeError,&@code) 15 | assert_match /#{Regexp.escape(@unknown_name)}/,ex.message 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/apps/todo_legacy/lib/todo/commands/create.rb: -------------------------------------------------------------------------------- 1 | desc "Create a new task or context" 2 | command [:create,:new] do |c| 3 | c.desc "Make a new task" 4 | c.arg_name 'task_name', :multiple 5 | c.command :tasks do |tasks| 6 | tasks.action do |global,options,args| 7 | puts "#{args}" 8 | end 9 | end 10 | 11 | c.desc "Make a new context" 12 | c.arg_name 'context_name', :optional 13 | c.command :contexts do |contexts| 14 | contexts.action do |global,options,args| 15 | puts "#{args}" 16 | end 17 | end 18 | 19 | c.default_desc "Makes a new task" 20 | c.action do 21 | puts "default action" 22 | end 23 | end 24 | 25 | -------------------------------------------------------------------------------- /lib/gli/commands/help_modules/one_line_wrapper.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | module Commands 3 | module HelpModules 4 | # Formats text in one line, stripping newlines and NOT wrapping 5 | class OneLineWrapper 6 | # Args are ignored entirely; this keeps it consistent with the TextWrapper interface 7 | def initialize(width,indent) 8 | end 9 | 10 | # Return a wrapped version of text, assuming that the first line has already been 11 | # indented by @indent characters. Resulting text does NOT have a newline in it. 12 | def wrap(text) 13 | return String(text).gsub(/\n+/,' ').strip 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/unit/verbatim_wrapper_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class TerminalTest < MiniTest::Test 4 | include TestHelper 5 | 6 | def test_handles_nil 7 | @wrapper = GLI::Commands::HelpModules::VerbatimWrapper.new(rand(100),rand(100)) 8 | @result = @wrapper.wrap(nil) 9 | assert_equal '',@result 10 | end 11 | 12 | def test_does_not_touch_input 13 | @wrapper = GLI::Commands::HelpModules::VerbatimWrapper.new(rand(100),rand(100)) 14 | @input = < Terminal.instance.size[0] } 13 | CompactSynopsisFormatter.new(@app,@flags_and_switches).synopses_for_command(command) 14 | 15 | else 16 | synopses 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /bin/ci: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | if [ -z $1 ]; then 5 | echo "[bin/ci] Running with default warnings" 6 | export RUBYOPT= 7 | else 8 | if [ $1 == 'warnings' ]; then 9 | echo "[bin/ci] Running with all warnings on" 10 | export RUBYOPT=-w 11 | else 12 | if [ $1 == 'none' ]; then 13 | echo "[bin/ci] Running with all warnings off" 14 | export RUBYOPT='-W0' 15 | else 16 | echo "[bin/ci] '$1' is not a supported option" 17 | echo "[bin/ci] usage: $0 # run with default warnings" 18 | echo "[bin/ci] usage: $0 warnings # run with all warnings" 19 | echo "[bin/ci] usage: $0 none # run with warnings disabled" 20 | exit 1 21 | fi 22 | fi 23 | fi 24 | 25 | echo "[bin/ci] Running unit tests" 26 | bin/rake test:unit 27 | 28 | echo "[bin/ci] Running integration tests" 29 | bin/rake test:integration 30 | -------------------------------------------------------------------------------- /lib/gli/commands/help_modules/list_formatter.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | module Commands 3 | module HelpModules 4 | # Given a list of two-element lists, formats on the terminal 5 | class ListFormatter 6 | def initialize(list,wrapper_class=TextWrapper) 7 | @list = list 8 | @wrapper_class = wrapper_class 9 | end 10 | 11 | # Output the list to the output_device 12 | def output(output_device) 13 | return if @list.empty? 14 | max_width = @list.map { |_| _[0].length }.max 15 | wrapper = @wrapper_class.new(Terminal.instance.size[0],4 + max_width + 3) 16 | @list.each do |(name,description)| 17 | output_device.printf(" %-#{max_width}s - %s\n",name,wrapper.wrap(String(description).strip)) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/gli/commands/help_modules/tty_only_wrapper.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | module Commands 3 | module HelpModules 4 | # Formats text in one line, stripping newlines and NOT wrapping 5 | class TTYOnlyWrapper 6 | # Args are ignored entirely; this keeps it consistent with the TextWrapper interface 7 | def initialize(width,indent) 8 | @proxy = if STDOUT.tty? 9 | TextWrapper.new(width,indent) 10 | else 11 | OneLineWrapper.new(width,indent) 12 | end 13 | end 14 | 15 | # Return a wrapped version of text, assuming that the first line has already been 16 | # indented by @indent characters. Resulting text does NOT have a newline in it. 17 | def wrap(text) 18 | @proxy.wrap(text) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/apps/todo/todo.gemspec: -------------------------------------------------------------------------------- 1 | # Ensure we require the local version and not one we might have installed already 2 | require File.join([File.dirname(__FILE__),'lib','todo','version.rb']) 3 | spec = Gem::Specification.new do |s| 4 | s.name = 'todo' 5 | s.version = Todo::VERSION 6 | s.author = 'Your Name Here' 7 | s.email = 'your@email.address.com' 8 | s.homepage = 'http://your.website.com' 9 | s.platform = Gem::Platform::RUBY 10 | s.summary = 'A description of your project' 11 | # Add your other files here if you make them 12 | s.files = %w( 13 | bin/todo 14 | ) 15 | s.require_paths << 'lib' 16 | s.extra_rdoc_files = ['README.rdoc','todo.rdoc'] 17 | s.rdoc_options << '--title' << 'todo' << '--main' << 'README.rdoc' << '-ri' 18 | s.bindir = 'bin' 19 | s.executables << 'todo' 20 | s.add_development_dependency('rake') 21 | s.add_development_dependency('rdoc') 22 | end 23 | -------------------------------------------------------------------------------- /test/apps/todo_legacy/todo.gemspec: -------------------------------------------------------------------------------- 1 | # Ensure we require the local version and not one we might have installed already 2 | require File.join([File.dirname(__FILE__),'lib','todo','version.rb']) 3 | spec = Gem::Specification.new do |s| 4 | s.name = 'todo' 5 | s.version = Todo::VERSION 6 | s.author = 'Your Name Here' 7 | s.email = 'your@email.address.com' 8 | s.homepage = 'http://your.website.com' 9 | s.platform = Gem::Platform::RUBY 10 | s.summary = 'A description of your project' 11 | # Add your other files here if you make them 12 | s.files = %w( 13 | bin/todo 14 | ) 15 | s.require_paths << 'lib' 16 | s.extra_rdoc_files = ['README.rdoc','todo.rdoc'] 17 | s.rdoc_options << '--title' << 'todo' << '--main' << 'README.rdoc' << '-ri' 18 | s.bindir = 'bin' 19 | s.executables << 'todo' 20 | s.add_development_dependency('rake') 21 | s.add_development_dependency('rdoc') 22 | end 23 | -------------------------------------------------------------------------------- /bin/gli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'gli' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("gli", "gli") 30 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rake' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rake", "rake") 30 | -------------------------------------------------------------------------------- /lib/gli/commands/help_modules/help_completion_format.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | module Commands 3 | module HelpModules 4 | class HelpCompletionFormat 5 | def initialize(app,command_finder,args) 6 | @app = app 7 | @command_finder = command_finder 8 | @command_finder.squelch_stderr = true 9 | @args = args 10 | end 11 | 12 | def format 13 | name = @args.shift 14 | 15 | base = @command_finder.find_command(name) 16 | base = @command_finder.last_found_command if base.nil? 17 | base = @app if base.nil? 18 | 19 | prefix_to_match = @command_finder.last_unknown_command 20 | 21 | base.commands.values.map { |command| 22 | [command.name,command.aliases] 23 | }.flatten.compact.map(&:to_s).sort.select { |command_name| 24 | prefix_to_match.nil? || command_name =~ /^#{prefix_to_match}/ 25 | }.join("\n") 26 | end 27 | 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/integration/scaffold_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | require "open3" 3 | 4 | class ScaffoldCommandTest < MiniTest::Test 5 | include TestHelper 6 | 7 | def test_scaffolded_app_has_reasonable_setup 8 | FileUtils.rm_rf "scaffold_test" 9 | run_gli("init scaffold_test") 10 | assert Dir.exist? "scaffold_test" 11 | FileUtils.chdir "scaffold_test" do 12 | run_command("bundle install", "", return_err_and_status: false, expect_failure: false) 13 | 14 | scaffold_lib = "lib:../lib" 15 | 16 | # help works 17 | out = run_command("bin/scaffold_test","--help", return_err_and_status: false, expect_failure: false, rubylib: scaffold_lib) 18 | assert_match /SYNOPSIS/,out 19 | assert_match /GLOBAL OPTIONS/,out 20 | assert_match /COMMANDS/,out 21 | 22 | # can run unit tests 23 | out = run_command("bundle exec ","rake test", return_err_and_status: false, expect_failure: false, rubylib: scaffold_lib) 24 | assert_match /0 failures/,out 25 | assert_match /0 errors/,out 26 | assert_match /0 skips/,out 27 | end 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /test/unit/options_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class OptiosnTest < MiniTest::Test 4 | include TestHelper 5 | 6 | def test_by_method 7 | o = GLI::Options.new 8 | o.name = 'verbose' 9 | assert_equal 'verbose', o.name 10 | assert_equal 'verbose', o[:name] 11 | assert_equal 'verbose', o['name'] 12 | end 13 | 14 | def test_by_string 15 | o = GLI::Options.new 16 | o['name'] = 'verbose' 17 | assert_equal 'verbose', o.name 18 | assert_equal 'verbose', o[:name] 19 | assert_equal 'verbose', o['name'] 20 | end 21 | 22 | def test_by_symbol 23 | o = GLI::Options.new 24 | o[:name] = 'verbose' 25 | assert_equal 'verbose', o.name 26 | assert_equal 'verbose', o[:name] 27 | assert_equal 'verbose', o['name'] 28 | end 29 | 30 | def test_map_defers_to_underlying_map 31 | o = GLI::Options.new 32 | o[:foo] = 'bar' 33 | o[:blah] = 'crud' 34 | 35 | result = Hash[o.map { |k,v| 36 | [k,v.upcase] 37 | }] 38 | assert_equal 2,result.size 39 | assert_equal "BAR",result[:foo] 40 | assert_equal "CRUD",result[:blah] 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lib/gli/command_line_option.rb: -------------------------------------------------------------------------------- 1 | require 'gli/command_line_token.rb' 2 | 3 | module GLI 4 | # An option, not a command or argument, on the command line 5 | class CommandLineOption < CommandLineToken #:nodoc: 6 | 7 | attr_accessor :default_value 8 | # Command to which this option "belongs", nil if it's a global option 9 | attr_accessor :associated_command 10 | 11 | # Creates a new option 12 | # 13 | # names - Array of symbols or strings representing the names of this switch 14 | # options - hash of options: 15 | # :desc - the short description 16 | # :long_desc - the long description 17 | # :default_value - the default value of this option 18 | def initialize(names,options = {}) 19 | super(names,options[:desc],options[:long_desc]) 20 | @default_value = options[:default_value] 21 | end 22 | 23 | def self.name_as_string(name,negatable=true) 24 | string = name.to_s 25 | if string.length == 1 26 | "-#{string}" 27 | elsif negatable 28 | "--[no-]#{string}" 29 | else 30 | "--#{string}" 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/gli.rb: -------------------------------------------------------------------------------- 1 | require 'gli/command_finder.rb' 2 | require 'gli/gli_option_block_parser.rb' 3 | require 'gli/option_parser_factory.rb' 4 | require 'gli/option_parsing_result.rb' 5 | require 'gli/gli_option_parser.rb' 6 | require 'gli/app_support.rb' 7 | require 'gli/app.rb' 8 | require 'gli/command_support.rb' 9 | require 'gli/command.rb' 10 | require 'gli/command_line_token.rb' 11 | require 'gli/command_line_option.rb' 12 | require 'gli/exceptions.rb' 13 | require 'gli/flag.rb' 14 | require 'gli/options.rb' 15 | require 'gli/switch.rb' 16 | require 'gli/argument.rb' 17 | require 'gli/dsl.rb' 18 | require 'gli/version.rb' 19 | require 'gli/commands/help' 20 | require 'gli/commands/compound_command' 21 | require 'gli/commands/initconfig' 22 | require 'gli/commands/rdoc_document_listener' 23 | require 'gli/commands/doc' 24 | 25 | module GLI 26 | include GLI::App 27 | 28 | def self.included(klass) 29 | warn "You should include GLI::App instead" 30 | end 31 | 32 | def self.run(*args) 33 | warn "GLI.run no longer works for GLI-2, you must just call `run(ARGV)' instead" 34 | warn "either fix your app, or use the latest GLI in the 1.x family" 35 | 1 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/gli/switch.rb: -------------------------------------------------------------------------------- 1 | require 'gli/command_line_option.rb' 2 | 3 | module GLI 4 | # Defines a command line switch 5 | class Switch < CommandLineOption #:nodoc: 6 | 7 | attr_accessor :default_value 8 | attr_reader :negatable 9 | 10 | # Creates a new switch 11 | # 12 | # names - Array of symbols or strings representing the names of this switch 13 | # options - hash of options: 14 | # :desc - the short description 15 | # :long_desc - the long description 16 | # :negatable - true or false if this switch is negatable; defaults to true 17 | # :default_value - default value if the switch is omitted 18 | def initialize(names,options = {}) 19 | super(names,options) 20 | @default_value = false if options[:default_value].nil? 21 | @negatable = options[:negatable].nil? ? true : options[:negatable] 22 | if @default_value != false && @negatable == false 23 | raise "A switch with default #{@default_value} that isn't negatable is useless" 24 | end 25 | end 26 | 27 | def arguments_for_option_parser 28 | all_forms_a 29 | end 30 | 31 | def negatable? 32 | @negatable 33 | end 34 | 35 | def required? 36 | false 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /gli.gemspec: -------------------------------------------------------------------------------- 1 | # Make sure we get the gli that's local 2 | require File.join([File.dirname(__FILE__),"lib","gli","version.rb"]) 3 | 4 | spec = Gem::Specification.new do |s| 5 | s.name = "gli" 6 | s.version = GLI::VERSION 7 | s.licenses = ["Apache-2.0"] 8 | s.author = "David Copeland" 9 | s.email = "davidcopeland@naildrivin5.com" 10 | s.homepage = "http://davetron5000.github.io/gli" 11 | s.platform = Gem::Platform::RUBY 12 | s.summary = "Build command-suite CLI apps that are awesome." 13 | s.description = "Build command-suite CLI apps that are awesome. Bootstrap your app, add commands, options and documentation while maintaining a well-tested idiomatic command-line app" 14 | 15 | s.files = `git ls-files`.split("\n") 16 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 17 | s.require_paths = ["lib"] 18 | 19 | s.extra_rdoc_files = ["README.rdoc", "gli.rdoc"] 20 | s.rdoc_options << "--title" << "Git Like Interface" << "--main" << "README.rdoc" 21 | 22 | s.bindir = "exe" 23 | s.executables = "gli" 24 | 25 | s.add_development_dependency("rake") 26 | s.add_development_dependency("rdoc") 27 | s.add_development_dependency("rainbow", "~> 1.1", "~> 1.1.1") 28 | s.add_development_dependency("sdoc") 29 | s.add_development_dependency("minitest") 30 | end 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributions are welcome as long as they are part of my vision for GLI (or can be treated as optional to the user). I am obsessive about backwards-compatibility, so you may need to default things to disable your features. Sorry, not ready to bump a major version any time soon. 2 | 3 | 1. Fork my Repository 4 | 2. Create a branch off of main 5 | 3. Make your changes: 6 | * Please include tests and watch out for reek and roodi; i.e. keep your code clean 7 | * If you make changes to the gli executable or the scaffolding, please update the cucumber features 8 | * Please rubydoc any new methods and update the rubydoc to methods you change in the following format: 9 | ```ruby 10 | # Short description 11 | # 12 | # Longer description if needed 13 | # 14 | # +args+:: get documented using this syntax 15 | # +args+:: please state the TYPE of every arg 16 | # 17 | # Returns goes here, please state the TYPE of what's returned, if anything 18 | ``` 19 | * Use # :nodoc: for methods that a _user_ of GLI should not call (but still please do document all methods) 20 | 4. Make sure your branch will merge with my gli-2 branch (or just rebase your branch from my gli-2 branch) 21 | 5. Create a pull request explaining your change 22 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | # See https://circleci.com/developer/orbs/orb/circleci/ruby 4 | ruby: circleci/ruby@1.4.0 5 | jobs: # keyword 6 | test: # my name for the job 7 | parameters: # keyword 8 | ruby-version: # my parameter name 9 | type: string # type is a keyword 10 | docker: # keyword 11 | - image: cimg/base:stable 12 | steps: # keyword 13 | - checkout # magic name 14 | - ruby/install: # ruby/ is from the orb name, install is a command in that orb 15 | version: << parameters.ruby-version >> # magic nonsense for param subst (version param to the command) 16 | - run: 17 | command: "bin/setup" 18 | - run: 19 | command: "bin/ci none" 20 | workflows: # keyword 21 | all-rubies: # my name for the workflow 22 | jobs: # keyword 23 | - test: # my name for the job 24 | matrix: # keyword 25 | parameters: # keyword 26 | # All rubies being maintained per this page: 27 | # https://www.ruby-lang.org/en/downloads/branches/ 28 | ruby-version: [ "2.5", "2.6", "2.7", "3.0" ] 29 | -------------------------------------------------------------------------------- /object-model.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | 3 | rankdir="BT" 4 | nodesep=0.5 5 | 6 | node[shape=record fontname=courier fontsize=18] 7 | edge[fontname=avenir fontsize=12] 8 | 9 | CommandLineToken [ label="{ CommandLineToken | #name\l | #description\l | #long_description\l | #aliases\l}"] 10 | CommandLineOption [ label="{ CommandLineOption | #default_value \l }"] 11 | DSL 12 | Command 13 | Flag [ label="{ Flag | #argument_name\l }"] 14 | Switch 15 | App 16 | TopLevel [ label="top level?" shape=diamond fontname=avenir fontsize=12] 17 | 18 | Command -> DSL [ arrowhead=oarrow label=" includes" minlen=3] 19 | Command -> CommandLineToken [ arrowhead=oarrow label="inherits"] 20 | CommandLineOption -> CommandLineToken [ arrowhead=oarrow label="inherits"] 21 | Flag -> CommandLineOption [ arrowhead=oarrow label="inherits"] 22 | Switch -> CommandLineOption [ arrowhead=oarrow label="inherits"] 23 | Command -> TopLevel [ arrowhead=none label="parent" style=dotted] 24 | TopLevel -> App [ arrowhead=odiamond label="YES" style=dotted ] 25 | TopLevel -> Command [ arrowhead=odiamond label="NO" style=dotted ] 26 | CommandLineOption -> Command [ arrowhead=odiamond style=dotted label="associated_command"] 27 | 28 | { rank=same; DSL; App } 29 | } 30 | -------------------------------------------------------------------------------- /test/apps/todo_legacy/lib/todo/commands/ls.rb: -------------------------------------------------------------------------------- 1 | # This is copied so I can have two slightly different versions of the same thing 2 | desc "LS things, such as tasks or contexts" 3 | long_desc %( 4 | List a whole lot of things that you might be keeping track of 5 | in your overall todo list. 6 | 7 | This is your go-to place or finding all of the things that you 8 | might have 9 | stored in 10 | your todo databases. 11 | ) 12 | command [:ls] do |c| 13 | c.desc "Show long form" 14 | c.switch [:l,:long] 15 | 16 | c.desc "List tasks" 17 | c.long_desc %( 18 | Lists all of your tasks that you have, in varying orders, and 19 | all that stuff. Yes, this is long, but I need a long description. 20 | ) 21 | c.command :tasks do |tasks| 22 | tasks.desc "blah blah crud x whatever" 23 | tasks.flag [:x] 24 | tasks.action do |global,options,args| 25 | puts "ls tasks: #{args.join(',')}" 26 | end 27 | end 28 | 29 | c.desc "List contexts" 30 | c.long_desc %( 31 | Lists all of your contexts, which are places you might be 32 | where you can do stuff and all that. 33 | ) 34 | c.command :contexts do |contexts| 35 | 36 | contexts.desc "Foobar" 37 | contexts.switch [:f,'foobar'] 38 | 39 | contexts.desc "Blah" 40 | contexts.switch [:b] 41 | 42 | contexts.action do |global,options,args| 43 | puts "ls contexts: #{args.join(',')}" 44 | end 45 | end 46 | end 47 | 48 | -------------------------------------------------------------------------------- /test/unit/switch_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class SwitchTest < MiniTest::Test 4 | include TestHelper 5 | 6 | def test_basics_simple 7 | switch_with_names(:filename) 8 | attributes_should_be_set 9 | assert_equal(:filename,@cli_option.name) 10 | assert_nil @cli_option.aliases 11 | end 12 | 13 | def test_basics_kinda_complex 14 | switch_with_names([:f]) 15 | attributes_should_be_set 16 | assert_equal(:f,@cli_option.name) 17 | assert_nil @cli_option.aliases 18 | end 19 | 20 | def test_basics_complex 21 | switch_with_names([:f,:file,:filename]) 22 | attributes_should_be_set 23 | assert_equal(:f,@cli_option.name) 24 | assert_equal([:file,:filename],@cli_option.aliases) 25 | assert_equal ["-f","--[no-]file","--[no-]filename"],@switch.arguments_for_option_parser 26 | end 27 | 28 | def test_includes_negatable 29 | assert_equal '-a',GLI::Switch.name_as_string('a') 30 | assert_equal '--[no-]foo',GLI::Switch.name_as_string('foo') 31 | end 32 | 33 | private 34 | 35 | def switch_with_names(names) 36 | @options = { 37 | :desc => 'Filename', 38 | :long_desc => 'The Filename', 39 | } 40 | @switch = GLI::Switch.new(names,@options) 41 | @cli_option = @switch 42 | end 43 | 44 | def attributes_should_be_set 45 | assert_equal(@options[:desc],@switch.description) 46 | assert_equal(@options[:long_desc],@switch.long_description) 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /test/apps/todo/lib/todo/commands/ls.rb: -------------------------------------------------------------------------------- 1 | class App 2 | # This is copied so I can have two slightly different versions of the same thing 3 | desc "LS things, such as tasks or contexts" 4 | long_desc %( 5 | List a whole lot of things that you might be keeping track of 6 | in your overall todo list. 7 | 8 | This is your go-to place or finding all of the things that you 9 | might have 10 | stored in 11 | your todo databases. 12 | ) 13 | command [:ls] do |c| 14 | c.desc "Show long form" 15 | c.switch [:l,:long] 16 | 17 | c.desc "List tasks" 18 | c.long_desc %( 19 | Lists all of your tasks that you have, in varying orders, and 20 | all that stuff. Yes, this is long, but I need a long description. 21 | ) 22 | c.command :tasks do |tasks| 23 | tasks.desc "blah blah crud x whatever" 24 | tasks.flag [:x] 25 | tasks.action do |global,options,args| 26 | puts "ls tasks: #{args.join(',')}" 27 | end 28 | end 29 | 30 | c.desc "List contexts" 31 | c.long_desc %( 32 | Lists all of your contexts, which are places you might be 33 | where you can do stuff and all that. 34 | ) 35 | c.command :contexts do |contexts| 36 | 37 | contexts.desc "Foobar" 38 | contexts.switch [:f,'foobar'] 39 | 40 | contexts.desc "Blah" 41 | contexts.switch [:b] 42 | 43 | contexts.action do |global,options,args| 44 | puts "ls contexts: #{args.join(',')}" 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/gli/commands/compound_command.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | module Commands 3 | # A command that calls other commands in order 4 | class CompoundCommand < Command 5 | # base:: object that respondes to +commands+ 6 | # configuration:: Array of arrays: index 0 is the array of names of this command and index 1 7 | # is the names of the compound commands. 8 | def initialize(base,configuration,options={}) 9 | name = configuration.keys.first 10 | super(options.merge(:names => [name])) 11 | 12 | command_names = configuration[name] 13 | 14 | check_for_unknown_commands!(base,command_names) 15 | 16 | @wrapped_commands = command_names.map { |name| self.class.find_command(base,name) } 17 | end 18 | 19 | def execute(global_options,options,arguments) #:nodoc: 20 | @wrapped_commands.each do |command| 21 | command.execute(global_options,options,arguments) 22 | end 23 | end 24 | 25 | private 26 | 27 | def check_for_unknown_commands!(base,command_names) 28 | known_commands = base.commands.keys.map(&:to_s) 29 | unknown_commands = command_names.map(&:to_s) - known_commands 30 | 31 | unless unknown_commands.empty? 32 | raise "Unknown commands #{unknown_commands.join(',')}" 33 | end 34 | end 35 | 36 | def self.find_command(base,name) 37 | base.commands.values.find { |command| command.name == name } 38 | end 39 | 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/apps/todo/lib/todo/commands/create.rb: -------------------------------------------------------------------------------- 1 | class App 2 | desc "Create a new task or context" 3 | command [:create,:new] do |c| 4 | c.desc "Make a new task" 5 | c.arg_name 'task_name', :multiple 6 | c.arg :should_ignore_this 7 | c.command :tasks do |tasks| 8 | tasks.action do |global,options,args| 9 | puts "#{args}" 10 | end 11 | end 12 | 13 | c.desc "Make a new context" 14 | c.arg :should_ignore_this 15 | c.arg_name 'context_name', :optional 16 | c.command :contexts do |contexts| 17 | contexts.action do |global,options,args| 18 | puts "#{args}" 19 | end 20 | end 21 | 22 | c.default_desc "Makes a new task" 23 | c.action do 24 | puts "default action" 25 | end 26 | 27 | c.arg "first" 28 | c.arg "second" 29 | c.arg "name", :optional 30 | c.command :"relation_1-1" do |remote| 31 | remote.action do |global,options,args| 32 | puts "relation: #{args}" 33 | end 34 | end 35 | 36 | c.arg "first", :multiple 37 | c.arg "second" 38 | c.arg "name", :optional 39 | c.command :"relation_n-1" do |remote| 40 | remote.action do |global,options,args| 41 | puts "relation: #{args}" 42 | end 43 | end 44 | 45 | c.arg "first" 46 | c.arg "second", :multiple 47 | c.arg "name", :optional 48 | c.command :"relation_1-n" do |remote| 49 | remote.action do |global,options,args| 50 | puts "relation: #{args}" 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/gli/commands/help_modules/arg_name_formatter.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | module Commands 3 | module HelpModules 4 | # Handles wrapping text 5 | class ArgNameFormatter 6 | def format(arguments_description,arguments_options,arguments) 7 | # Select which format to use: argname or arguments 8 | # Priority to old way: argname 9 | desc = format_argname(arguments_description, arguments_options) 10 | desc = format_arguments(arguments) if desc.strip == '' 11 | desc 12 | end 13 | 14 | def format_arguments(arguments) 15 | return '' if arguments.empty? 16 | desc = "" 17 | 18 | # Go through the arguments, building the description string 19 | arguments.each do |arg| 20 | arg_desc = "#{arg.name}" 21 | if arg.optional? 22 | arg_desc = "[#{arg_desc}]" 23 | end 24 | if arg.multiple? 25 | arg_desc = "#{arg_desc}..." 26 | end 27 | desc = desc + " " + arg_desc 28 | end 29 | 30 | desc 31 | end 32 | 33 | def format_argname(arguments_description,arguments_options) 34 | return '' if String(arguments_description).strip == '' 35 | desc = arguments_description 36 | if arguments_options.include? :optional 37 | desc = "[#{desc}]" 38 | end 39 | if arguments_options.include? :multiple 40 | desc = "#{desc}..." 41 | end 42 | " " + desc 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /gli.rdoc: -------------------------------------------------------------------------------- 1 | == gli - create scaffolding for a GLI-powered application 2 | 3 | v2.19.2 4 | 5 | === Global Options 6 | === -r|--root arg 7 | 8 | Root dir of project 9 | 10 | [Default Value] . 11 | This is the directory where the project''s directory will be made, so if you 12 | specify a project name ''foo'' and the root dir of ''.'', the directory 13 | ''./foo'' will be created' 14 | 15 | === --help 16 | Show this message 17 | 18 | 19 | 20 | === -n 21 | Dry run; dont change the disk 22 | 23 | 24 | 25 | === -v 26 | Be verbose 27 | 28 | 29 | 30 | === --version 31 | Display the program version 32 | 33 | 34 | 35 | === Commands 36 | ==== Command: help command 37 | Shows a list of commands or help for one command 38 | 39 | Gets help for the application or its commands. Can also list the commands in a way helpful to creating a bash-style completion function 40 | ===== Options 41 | ===== -c 42 | List commands one per line, to assist with shell completion 43 | 44 | 45 | 46 | ==== Command: init|scaffold project_name [command_name]... 47 | Create a new GLI-based project 48 | 49 | This will create a scaffold command line project that uses GLI 50 | for command line processing. Specifically, this will create 51 | an executable ready to go, as well as a lib and test directory, all 52 | inside the directory named for your project 53 | ===== Options 54 | ===== -e|--[no-]ext 55 | Create an ext dir 56 | 57 | 58 | 59 | ===== --[no-]force 60 | Overwrite/ignore existing files and directories 61 | 62 | 63 | 64 | ===== --notest 65 | Do not create a test or features dir 66 | 67 | 68 | 69 | ===== --[no-]rvmrc 70 | Create an .rvmrc based on your current RVM setup 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /test/integration/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | require "pathname" 3 | require "fileutils" 4 | 5 | # Copied from https://github.com/splattael/minitest-around 6 | # so as to avoid an explicit dependency 7 | Minitest::Test.class_eval do 8 | alias_method :run_without_around, :run 9 | def run(*args) 10 | if defined?(around) 11 | result = nil 12 | around { result = run_without_around(*args) } 13 | result 14 | else 15 | run_without_around(*args) 16 | end 17 | end 18 | end 19 | 20 | module TestHelper 21 | def around(&block) 22 | Bundler.with_original_env do 23 | root = Pathname(__FILE__).dirname / ".." / ".." 24 | FileUtils.chdir root do 25 | block.() 26 | end 27 | end 28 | end 29 | 30 | def run_gli(args="", return_err_and_status: false, expect_failure: false) 31 | run_command("bin/gli",args,return_err_and_status:return_err_and_status,expect_failure:expect_failure) 32 | end 33 | 34 | def run_command(command,args,return_err_and_status:,expect_failure:,rubylib:nil) 35 | command_line_invocation = "#{command} #{args}" 36 | env = {} 37 | if !rubylib.nil? 38 | env["RUBYLIB"] = rubylib 39 | end 40 | stdout_string, stderr_string, status = Open3.capture3(env,command_line_invocation) 41 | if expect_failure 42 | refute_equal 0,status.exitstatus,"Expected failure for '#{command_line_invocation}' but it succeeded" 43 | else 44 | assert_equal 0,status.exitstatus,"Expected success for '#{command_line_invocation}' but it failed:\n#{stdout_string}\n\n#{stderr_string}\n\n" 45 | end 46 | if return_err_and_status 47 | [ stdout_string, stderr_string, status ] 48 | else 49 | stdout_string 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/apps/todo_legacy/lib/todo/commands/list.rb: -------------------------------------------------------------------------------- 1 | desc "List things, such as tasks or contexts" 2 | long_desc %( 3 | List a whole lot of things that you might be keeping track of 4 | in your overall todo list. 5 | 6 | This is your go-to place or finding all of the things that you 7 | might have 8 | stored in 9 | your todo databases. 10 | ) 11 | command [:list] do |c| 12 | c.default_command :tasks 13 | 14 | c.desc "Show long form" 15 | c.switch [:l,:long] 16 | 17 | c.desc "List tasks" 18 | c.long_desc %( 19 | Lists all of your tasks that you have, in varying orders, and 20 | all that stuff. Yes, this is long, but I need a long description. 21 | ) 22 | c.command :tasks do |tasks| 23 | tasks.desc "blah blah crud x whatever" 24 | tasks.flag [:x], :must_match => Array 25 | 26 | tasks.flag :flag 27 | 28 | tasks.action do |global,options,args| 29 | puts options[:x].inspect 30 | puts "list tasks: #{args.join(',')}" 31 | end 32 | 33 | tasks.desc 'list open tasks' 34 | tasks.command :open do |open| 35 | open.action do |global,options,args| 36 | puts "tasks open" 37 | end 38 | end 39 | 40 | tasks.default_desc 'list all tasks' 41 | end 42 | 43 | c.desc "List contexts" 44 | c.long_desc %( 45 | Lists all of your contexts, which are places you might be 46 | where you can do stuff and all that. 47 | ) 48 | c.command :contexts do |contexts| 49 | 50 | contexts.desc "Foobar" 51 | contexts.switch [:f,'foobar'] 52 | 53 | contexts.desc "Blah" 54 | contexts.switch [:b] 55 | 56 | contexts.flag :otherflag 57 | 58 | contexts.action do |global,options,args| 59 | puts "list contexts: #{args.join(',')}" 60 | end 61 | end 62 | end 63 | 64 | -------------------------------------------------------------------------------- /test/apps/todo/lib/todo/commands/make.rb: -------------------------------------------------------------------------------- 1 | class App 2 | command [:make] do |c| 3 | c.desc "Show long form" 4 | c.flag [:l,:long] 5 | 6 | c.desc 'make a new task' 7 | c.command :task do |task| 8 | task.desc 'make the task a long task' 9 | task.flag [:l,:long] 10 | 11 | task.action do |g,o,a| 12 | puts 'new task' 13 | puts a.join(',') 14 | puts o[:long] 15 | end 16 | 17 | task.desc 'make a bug' 18 | task.arg :argument, [:multiple, :optional] 19 | task.command :bug do |bug| 20 | bug.desc 'make this bug in the legacy system' 21 | bug.flag [:l,:legacy] 22 | 23 | bug.action do |g,o,a| 24 | puts 'new task bug' 25 | puts a.join(',') 26 | # All this .to_s is to make sure 1.8.7/REE don't convert nil to the string "nil" 27 | puts o[:legacy].to_s 28 | puts o[:long].to_s 29 | puts o[:l].to_s 30 | puts o[GLI::Command::PARENT][:l].to_s 31 | puts o[GLI::Command::PARENT][:long].to_s 32 | puts o[GLI::Command::PARENT][:legacy].to_s 33 | puts o[GLI::Command::PARENT][GLI::Command::PARENT][:l].to_s 34 | puts o[GLI::Command::PARENT][GLI::Command::PARENT][:long].to_s 35 | puts o[GLI::Command::PARENT][GLI::Command::PARENT][:legacy].to_s 36 | end 37 | end 38 | end 39 | 40 | c.desc 'make a new context' 41 | c.command :context do |context| 42 | context.desc 'make the context a local context' 43 | context.flag [:l,:local] 44 | 45 | context.action do |g,o,a| 46 | puts 'new context' 47 | puts a.join(',') 48 | puts o[:local].to_s 49 | puts o[:long].to_s 50 | puts o[:l].to_s 51 | end 52 | end 53 | 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/unit/command_finder_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class CommandFinderTest < MiniTest::Test 4 | include TestHelper 5 | 6 | def setup 7 | @app = CLIApp.new 8 | [:status, :deployable, :some_command, :some_similar_command].each do |command| 9 | @app.commands[command] = GLI::Command.new(:names => command) 10 | end 11 | end 12 | 13 | def teardown 14 | end 15 | 16 | def test_unknown_command_name 17 | assert_raises(GLI::UnknownCommand) do 18 | GLI::CommandFinder.new(@app.commands, :default_command => :status).find_command(:unfindable_command) 19 | end 20 | end 21 | 22 | def test_no_command_name_without_default 23 | assert_raises(GLI::UnknownCommand) do 24 | GLI::CommandFinder.new(@app.commands).find_command(nil) 25 | end 26 | end 27 | 28 | def test_no_command_name_with_default 29 | actual = GLI::CommandFinder.new(@app.commands, :default_command => :status).find_command(nil) 30 | expected = @app.commands[:status] 31 | 32 | assert_equal(actual, expected) 33 | end 34 | 35 | def test_ambigous_command 36 | assert_raises(GLI::AmbiguousCommand) do 37 | GLI::CommandFinder.new(@app.commands, :default_command => :status).find_command(:some) 38 | end 39 | end 40 | 41 | def test_partial_name_with_autocorrect_enabled 42 | actual = GLI::CommandFinder.new(@app.commands, :default_command => :status).find_command(:deploy) 43 | expected = @app.commands[:deployable] 44 | 45 | assert_equal(actual, expected) 46 | end 47 | 48 | def test_partial_name_with_autocorrect_disabled 49 | assert_raises(GLI::UnknownCommand) do 50 | GLI::CommandFinder.new(@app.commands, :default_command => :status, :autocomplete => false) 51 | .find_command(:deploy) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/integration/gli_powered_app_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | require "open3" 3 | 4 | class GLIPoweredAppTest < MiniTest::Test 5 | include TestHelper 6 | 7 | def teardown 8 | FileUtils.rm_f("todo.rdoc") 9 | end 10 | 11 | def test_help_works 12 | out = run_app("help") 13 | assert_top_level_help(out) 14 | end 15 | 16 | def test_unknown_command_exits_nonzero 17 | out, err, status = run_app("asdfasdfasdf", expect_failure: true, return_err_and_status: true) 18 | assert_match /Unknown command 'asdfasdfasdf'/,err 19 | assert_equal 64, status.exitstatus 20 | assert_top_level_help(out) 21 | end 22 | 23 | def test_unknown_switch_exits_nonzero 24 | out, err, status = run_app("list --foo", expect_failure: true, return_err_and_status: true) 25 | assert_match /Unknown option \-\-foo/,err 26 | assert_equal 64, status.exitstatus 27 | assert_match /COMMAND OPTIONS/, out 28 | end 29 | 30 | def test_missing_args_exits_nonzero 31 | out, err, status = run_app("list", expect_failure: true, return_err_and_status: true) 32 | assert_match /required_flag is required, required_flag2 is required/,err 33 | assert_equal 64, status.exitstatus 34 | assert_match /COMMAND OPTIONS/, out 35 | end 36 | 37 | def test_doc_generation 38 | out, err, status = run_app("_doc", return_err_and_status: true) 39 | assert File.exists?("todo.rdoc") 40 | end 41 | 42 | private 43 | def assert_top_level_help(out) 44 | assert_match /SYNOPSIS/, out 45 | assert_match /GLOBAL OPTIONS/, out 46 | assert_match /COMMANDS/, out 47 | end 48 | 49 | def run_app(args="", return_err_and_status: false, expect_failure: false) 50 | run_command("test/apps/todo/bin/todo",args,return_err_and_status:return_err_and_status,expect_failure:expect_failure) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/apps/todo_legacy/bin/todo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # These would not be in a real GLI app; we do this so we can easily run this on the command line 4 | $: << File.expand_path(File.join(File.dirname(__FILE__),'..','..','..','lib')) 5 | $: << File.expand_path(File.join(File.dirname(__FILE__),'..','lib')) 6 | 7 | require 'gli' 8 | require 'todo/version' 9 | 10 | 11 | if ENV['GLI1_COMPATIBILITY'] 12 | include GLI 13 | else 14 | include GLI::App 15 | end 16 | 17 | sort_help (ENV['TODO_SORT_HELP'] || 'alpha').to_sym 18 | wrap_help_text (ENV['TODO_WRAP_HELP_TEXT'] || 'to_terminal').to_sym 19 | 20 | program_desc 'Manages tasks' 21 | program_long_desc "A test program that has a sophisticated UI that can be used to exercise a lot of GLI's power" 22 | 23 | config_file "gli_test_todo.rc" 24 | 25 | flag :flag 26 | switch :switch 27 | switch :otherswitch, :negatable => true 28 | 29 | version Todo::VERSION 30 | 31 | commands_from 'todo/commands' 32 | commands_from File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'todo_plugins', 'commands')) 33 | 34 | command :first do |c| c.action { |g,o,a| puts "first: #{a.join(',')}" } end 35 | command :second do |c| c.action { |g,o,a| puts "second: #{a.join(',')}" } end 36 | 37 | command :chained => [ :first, :second ] 38 | command [:chained2,:ch2] => [ :second, :first ] 39 | 40 | pre do |global,command,options,args| 41 | # Pre logic here 42 | # Return true to proceed; false to abort and not call the 43 | # chosen command 44 | # Use skips_pre before a command to skip this block 45 | # on that command only 46 | true 47 | end 48 | 49 | post do |global,command,options,args| 50 | # Post logic here 51 | # Use skips_post before a command to skip this 52 | # block on that command only 53 | end 54 | 55 | on_error do |exception| 56 | # Error logic here 57 | # return false to skip default error handling 58 | true 59 | end 60 | 61 | exit run(ARGV) 62 | -------------------------------------------------------------------------------- /lib/gli/commands/help_modules/text_wrapper.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | module Commands 3 | module HelpModules 4 | # Handles wrapping text 5 | class TextWrapper 6 | # Create a text_wrapper wrapping at the given width, 7 | # and indent. 8 | def initialize(width,indent) 9 | @width = width 10 | @indent = indent 11 | end 12 | 13 | # Return a wrapped version of text, assuming that the first line has already been 14 | # indented by @indent characters. Resulting text does NOT have a newline in it. 15 | def wrap(text) 16 | return text if text.nil? 17 | wrapped_text = '' 18 | current_graf = '' 19 | 20 | paragraphs = text.split(/\n\n+/) 21 | paragraphs.each do |graf| 22 | current_line = '' 23 | current_line_length = @indent 24 | 25 | words = graf.split(/\s+/) 26 | current_line = words.shift || '' 27 | current_line_length += current_line.length 28 | 29 | words.each do |word| 30 | if current_line_length + word.length + 1 > @width 31 | current_graf << current_line << "\n" 32 | current_line = '' 33 | current_line << ' ' * @indent << word 34 | current_line_length = @indent + word.length 35 | else 36 | if current_line == '' 37 | current_line << word 38 | else 39 | current_line << ' ' << word 40 | end 41 | current_line_length += (word.length + 1) 42 | end 43 | end 44 | current_graf << current_line 45 | wrapped_text << current_graf << "\n\n" << ' ' * @indent 46 | current_graf = '' 47 | end 48 | wrapped_text.gsub(/[\n\s]*\Z/,'') 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/unit/flag_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class FlagTest < MiniTest::Test 4 | include TestHelper 5 | 6 | def test_basics_simple 7 | setup_for_flag_with_names(:f) 8 | assert_attributes_set 9 | assert_equal(:f,@cli_option.name) 10 | assert_nil @cli_option.aliases 11 | end 12 | 13 | def test_basics_kinda_complex 14 | setup_for_flag_with_names([:f]) 15 | assert_attributes_set 16 | assert_equal(:f,@cli_option.name) 17 | assert_nil @cli_option.aliases 18 | end 19 | 20 | def test_basics_complex 21 | setup_for_flag_with_names([:f,:file,:filename]) 22 | assert_attributes_set 23 | assert_equal(:f,@cli_option.name) 24 | assert_equal [:file,:filename], @cli_option.aliases 25 | assert_equal ["-f VAL","--file VAL","--filename VAL",/foobar/,Float],@flag.arguments_for_option_parser 26 | end 27 | 28 | def test_flag_can_mask_its_value 29 | setup_for_flag_with_names(:password, :mask => true) 30 | assert_attributes_set(:safe_default_value => "********") 31 | end 32 | 33 | def setup_for_flag_with_names(names,options = {}) 34 | @options = { 35 | :desc => 'Filename', 36 | :long_desc => 'The Filename', 37 | :arg_name => 'file', 38 | :default_value => '~/.blah.rc', 39 | :safe_default_value => '~/.blah.rc', 40 | :must_match => /foobar/, 41 | :type => Float, 42 | }.merge(options) 43 | @flag = GLI::Flag.new(names,@options) 44 | @cli_option = @flag 45 | end 46 | 47 | def assert_attributes_set(override={}) 48 | expected = @options.merge(override) 49 | assert_equal(expected[:desc],@flag.description) 50 | assert_equal(expected[:long_desc],@flag.long_description) 51 | assert_equal(expected[:default_value],@flag.default_value) 52 | assert_equal(expected[:safe_default_value],@flag.safe_default_value) 53 | assert_equal(expected[:must_match],@flag.must_match) 54 | assert_equal(expected[:type],@flag.type) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/gli/commands/help_modules/options_formatter.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | module Commands 3 | module HelpModules 4 | class OptionsFormatter 5 | def initialize(flags_and_switches,sorter,wrapper_class) 6 | @flags_and_switches = sorter.call(flags_and_switches) 7 | @wrapper_class = wrapper_class 8 | end 9 | 10 | def format 11 | list_formatter = ListFormatter.new(@flags_and_switches.map { |option| 12 | if option.respond_to? :argument_name 13 | [option_names_for_help_string(option,option.argument_name),description_with_default(option)] 14 | else 15 | [option_names_for_help_string(option),description_with_default(option)] 16 | end 17 | },@wrapper_class) 18 | stringio = StringIO.new 19 | list_formatter.output(stringio) 20 | stringio.string 21 | end 22 | 23 | private 24 | 25 | def description_with_default(option) 26 | if option.kind_of? Flag 27 | required = option.required? ? 'required, ' : '' 28 | multiple = option.multiple? ? 'may be used more than once, ' : '' 29 | 30 | String(option.description) + " (#{required}#{multiple}default: #{option.safe_default_value || 'none'})" 31 | else 32 | String(option.description) + (option.default_value ? " (default: enabled)" : "") 33 | end 34 | end 35 | 36 | def option_names_for_help_string(option,arg_name=nil) 37 | names = [option.name,Array(option.aliases)].flatten 38 | names = names.map { |name| CommandLineOption.name_as_string(name,option.kind_of?(Switch) ? option.negatable? : false) } 39 | if arg_name.nil? 40 | names.join(', ') 41 | else 42 | if names[-1] =~ /^--/ 43 | names.join(', ') + "=#{arg_name}" 44 | else 45 | names.join(', ') + " #{arg_name}" 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/gli/command_line_token.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | # Abstract base class for a logical element of a command line, mostly so that subclasses can have similar 3 | # initialization and interface 4 | class CommandLineToken 5 | attr_reader :name #:nodoc: 6 | attr_reader :aliases #:nodoc: 7 | attr_reader :description #:nodoc: 8 | attr_reader :long_description #:nodoc: 9 | 10 | def initialize(names,description,long_description=nil) #:nodoc: 11 | @description = description 12 | @long_description = long_description 13 | @name,@aliases,@names = parse_names(names) 14 | end 15 | 16 | # Sort based on primary name 17 | def <=>(other) 18 | self.name.to_s <=> other.name.to_s 19 | end 20 | 21 | # Array of the name and aliases, as string 22 | def names_and_aliases 23 | [self.name,self.aliases].flatten.compact.map(&:to_s) 24 | end 25 | 26 | private 27 | # Returns a string of all possible forms 28 | # of this flag. Mostly intended for printing 29 | # to the user. 30 | def all_forms(joiner=', ') 31 | forms = all_forms_a 32 | forms.join(joiner) 33 | end 34 | 35 | 36 | # Handles dealing with the "names" param, parsing 37 | # it into the primary name and aliases list 38 | def parse_names(names) 39 | # Allow strings; convert to symbols 40 | names = [names].flatten.map { |name| name.to_sym } 41 | names_hash = {} 42 | names.each do |name| 43 | raise ArgumentError.new("#{name} has spaces; they are not allowed") if name.to_s =~ /\s/ 44 | names_hash[self.class.name_as_string(name)] = true 45 | end 46 | name = names.shift 47 | aliases = names.length > 0 ? names : nil 48 | [name,aliases,names_hash] 49 | end 50 | 51 | def negatable? 52 | false; 53 | end 54 | 55 | def all_forms_a 56 | forms = [self.class.name_as_string(name,negatable?)] 57 | if aliases 58 | forms |= aliases.map { |one_alias| self.class.name_as_string(one_alias,negatable?) }.sort { |one,two| one.length <=> two.length } 59 | end 60 | forms 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/integration/gli_cli_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | require "open3" 3 | 4 | class GLICLITest < MiniTest::Test 5 | include TestHelper 6 | 7 | class AppHelp < GLICLITest 8 | def test_running_with_no_options_produces_help 9 | out = run_gli 10 | assert_output_looks_like_help out 11 | end 12 | 13 | def test_running_with_help_command_produces_help 14 | out = run_gli("help") 15 | assert_output_looks_like_help out 16 | end 17 | 18 | def test_running_with_help_switch_produces_help 19 | out = run_gli("--help") 20 | assert_output_looks_like_help out 21 | end 22 | 23 | private 24 | 25 | def assert_output_looks_like_help(out) 26 | assert_match /gli - create scaffolding for a GLI-powered application/,out 27 | assert_match /SYNOPSIS/,out 28 | assert_match /GLOBAL OPTIONS/,out 29 | assert_match /COMMANDS/,out 30 | end 31 | 32 | end 33 | 34 | class Scaffolding < GLICLITest 35 | def test_help_on_scaffold_command 36 | out = run_gli("help scaffold") 37 | assert_output_looks_like_help(out) 38 | end 39 | def test_help_on_scaffold_command_as_init 40 | out = run_gli("help init") 41 | assert_output_looks_like_help(out) 42 | end 43 | 44 | private 45 | 46 | def assert_output_looks_like_help(out) 47 | assert_match /init - Create a new GLI-based project/,out 48 | assert_match /SYNOPSIS/,out 49 | assert_match /COMMAND OPTIONS/,out 50 | end 51 | end 52 | 53 | private 54 | 55 | def run_gli(args="", return_err_and_status: false, expect_failure: false) 56 | command_line_invocation = "bin/gli #{args}" 57 | stdout_string, stderr_string, status = Open3.capture3(command_line_invocation) 58 | if expect_failure 59 | refute_equal 0,status.exitstatus,"Expected failure for '#{command_line_invocation}' but it succeeded" 60 | else 61 | assert_equal 0,status.exitstatus,"Expected success for '#{command_line_invocation}' but it failed" 62 | end 63 | if return_err_and_status 64 | [ stdout_string, stderr_string, status ] 65 | else 66 | stdout_string 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /exe/gli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'gli' 4 | require 'gli/commands/scaffold' 5 | 6 | class App 7 | extend GLI::App 8 | 9 | program_desc 'create scaffolding for a GLI-powered application' 10 | 11 | version GLI::VERSION 12 | 13 | # Can't use these without changing the current behavior of gli 14 | # arguments :strict 15 | # subcommand_option_handling :normal 16 | 17 | switch :v, :desc => 'Be verbose' 18 | 19 | switch :n, :desc => 'Dry run; don''t change the disk' 20 | 21 | desc 'Root dir of project' 22 | long_desc < '.' 29 | 30 | desc 'Create a new GLI-based project' 31 | long_desc < 'Create an ext dir' 43 | 44 | c.switch :notest, :desc => 'Do not create a test or features dir', :negatable => false 45 | 46 | c.switch :force, :desc => 'Overwrite/ignore existing files and directories' 47 | 48 | c.switch :rvmrc, :desc => 'Create an .rvmrc based on your current RVM setup' 49 | 50 | c.action do |g,o,args| 51 | if args.length < 1 52 | raise 'You must specify the name of your project' 53 | end 54 | GLI::Commands::Scaffold.create_scaffold(g[:r],!o[:notest],o[:e],args[0],args[1..-1],o[:force],g[:n],o[:rvmrc]) 55 | end 56 | end 57 | 58 | pre do |global,command,options,args| 59 | puts "Executing #{command.name}" if global[:v] 60 | true 61 | end 62 | 63 | post do |global,command,options,args| 64 | puts "Executed #{command.name}" if global[:v] 65 | end 66 | end 67 | 68 | exit App.run(ARGV) 69 | -------------------------------------------------------------------------------- /lib/gli/commands/help_modules/command_finder.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | module Commands 3 | module HelpModules 4 | # Finds commands from the application/command data structures 5 | class CommandFinder 6 | 7 | attr_reader :last_unknown_command 8 | attr_reader :last_found_command 9 | attr_writer :squelch_stderr 10 | 11 | def initialize(app,arguments,error) 12 | @app = app 13 | @arguments = arguments 14 | @error = error 15 | @squelch_stderr = false 16 | @last_unknown_command = nil 17 | end 18 | 19 | def find_command(name) 20 | command = find_command_from_base(name,@app) 21 | return if unknown_command?(command,name,@error) 22 | @last_found_command = command 23 | while !@arguments.empty? 24 | name = @arguments.shift 25 | command = find_command_from_base(name,command) 26 | return if unknown_command?(command,name,@error) 27 | @last_found_command = command 28 | end 29 | command 30 | end 31 | 32 | private 33 | 34 | # Given the name of a command to find, and a base, either the app or another command, returns 35 | # the command object or nil. 36 | def find_command_from_base(command_name,base) 37 | base.commands.values.select { |command| 38 | if [command.name,Array(command.aliases)].flatten.map(&:to_s).any? { |_| _ == command_name } 39 | command 40 | end 41 | }.first 42 | end 43 | 44 | # Checks if the return from find_command was unknown and, if so, prints an error 45 | # for the user on the error device, returning true or false if the command was unknown. 46 | def unknown_command?(command,name,error) 47 | if command.nil? 48 | @last_unknown_command = name 49 | unless @squelch_stderr 50 | error.puts "error: Unknown command '#{name}'. Use '#{@app.exe_name} help' for a list of commands." 51 | end 52 | true 53 | else 54 | false 55 | end 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/apps/todo/bin/todo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # These would not be in a real GLI app; we do this so we can easily run this on the command line 4 | $: << File.expand_path(File.join(File.dirname(__FILE__),'..','..','..','..','lib')) 5 | $: << File.expand_path(File.join(File.dirname(__FILE__),'..','lib')) 6 | 7 | require 'gli' 8 | require 'todo/version' 9 | 10 | class App 11 | if ENV['GLI1_COMPATIBILITY'] 12 | extend GLI 13 | else 14 | extend GLI::App 15 | end 16 | 17 | sort_help (ENV['TODO_SORT_HELP'] || 'alpha').to_sym 18 | wrap_help_text (ENV['TODO_WRAP_HELP_TEXT'] || 'to_terminal').to_sym 19 | synopsis_format (ENV['SYNOPSES'] || 'full').to_sym 20 | hide_commands_without_desc ENV['HIDE_COMMANDS_WITHOUT_DESC'] === 'true' 21 | 22 | subcommand_option_handling :normal 23 | arguments :strict 24 | 25 | program_desc 'Manages tasks' 26 | program_long_desc "A test program that has a sophisticated UI that can be used to exercise a lot of GLI's power" 27 | 28 | config_file "gli_test_todo.rc" 29 | 30 | flag :flag 31 | switch :switch 32 | switch :otherswitch, :negatable => true 33 | 34 | version Todo::VERSION 35 | 36 | commands_from 'todo/commands' 37 | commands_from File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'todo_plugins', 'commands')) 38 | 39 | arg_name :argument, :optional 40 | command :first do |c| c.action { |g,o,a| puts "first: #{a.join(',')}" } end 41 | 42 | arg :argument, :optional 43 | command :second do |c| c.action { |g,o,a| puts "second: #{a.join(',')}" } end 44 | 45 | arg :first 46 | arg :second 47 | command :chained => [ :first, :second ] 48 | 49 | arg :first 50 | arg :second 51 | command [:chained2,:ch2] => [ :second, :first ] 52 | 53 | pre do |global,command,options,args| 54 | # Pre logic here 55 | # Return true to proceed; false to abort and not call the 56 | # chosen command 57 | # Use skips_pre before a command to skip this block 58 | # on that command only 59 | true 60 | end 61 | 62 | post do |global,command,options,args| 63 | # Post logic here 64 | # Use skips_post before a command to skip this 65 | # block on that command only 66 | end 67 | 68 | on_error do |exception| 69 | # Error logic here 70 | # return false to skip default error handling 71 | true 72 | end 73 | end 74 | exit App.run(ARGV) 75 | -------------------------------------------------------------------------------- /lib/gli/command_finder.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | class CommandFinder 3 | attr_accessor :options 4 | 5 | DEFAULT_OPTIONS = { 6 | :default_command => nil, 7 | :autocomplete => true 8 | } 9 | 10 | def initialize(commands, options = {}) 11 | self.options = DEFAULT_OPTIONS.merge(options) 12 | self.commands_with_aliases = expand_with_aliases(commands) 13 | end 14 | 15 | def find_command(name) 16 | name = String(name || options[:default_command]).strip 17 | raise UnknownCommand.new("No command name given nor default available") if name == '' 18 | 19 | command_found = commands_with_aliases.fetch(name) do |command_to_match| 20 | if options[:autocomplete] 21 | found_match = find_command_by_partial_name(commands_with_aliases, command_to_match) 22 | if found_match.kind_of? GLI::Command 23 | if ENV["GLI_DEBUG"] == 'true' 24 | $stderr.puts "Using '#{name}' as it's is short for #{found_match.name}." 25 | $stderr.puts "Set autocomplete false for any command you don't want matched like this" 26 | end 27 | elsif found_match.kind_of?(Array) && !found_match.empty? 28 | raise AmbiguousCommand.new("Ambiguous command '#{name}'. It matches #{found_match.sort.join(',')}") 29 | end 30 | found_match 31 | end 32 | end 33 | 34 | raise UnknownCommand.new("Unknown command '#{name}'") if Array(command_found).empty? 35 | command_found 36 | end 37 | 38 | private 39 | attr_accessor :commands_with_aliases 40 | 41 | def expand_with_aliases(commands) 42 | expanded = {} 43 | commands.each do |command_name, command| 44 | expanded[command_name.to_s] = command 45 | Array(command.aliases).each do |command_alias| 46 | expanded[command_alias.to_s] = command 47 | end 48 | end 49 | expanded 50 | end 51 | 52 | def find_command_by_partial_name(commands_with_aliases, command_to_match) 53 | partial_matches = commands_with_aliases.keys.select { |command_name| command_name =~ /^#{command_to_match}/ } 54 | return commands_with_aliases[partial_matches[0]] if partial_matches.size == 1 55 | partial_matches 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/apps/todo/lib/todo/commands/list.rb: -------------------------------------------------------------------------------- 1 | class App 2 | desc "List things, such as tasks or contexts" 3 | long_desc %( 4 | List a whole lot of things that you might be keeping track of 5 | in your overall todo list. 6 | 7 | This is your go-to place or finding all of the things that you 8 | might have 9 | stored in 10 | your todo databases. 11 | ) 12 | 13 | command [:list] do |c| 14 | c.default_command :tasks 15 | 16 | c.desc "Show long form" 17 | c.switch [:l,:long] 18 | 19 | c.flag :required_flag, :required => true 20 | c.flag :required_flag2, :required => true 21 | 22 | c.desc "List tasks" 23 | c.long_desc %( 24 | Lists all of your tasks that you have, in varying orders, and 25 | all that stuff. Yes, this is long, but I need a long description. 26 | ) 27 | 28 | c.arg :task, [:optional, :multiple] 29 | c.command :tasks do |tasks| 30 | tasks.desc "blah blah crud x whatever" 31 | tasks.flag [:x], :must_match => Array 32 | 33 | tasks.flag :flag 34 | 35 | tasks.action do |global,options,args| 36 | puts options[:x].inspect 37 | puts "list tasks: #{args.join(',')}" 38 | end 39 | 40 | tasks.desc 'list open tasks' 41 | tasks.command :open do |open| 42 | open.desc "blah blah crud x whatever" 43 | open.flag [:x], :must_match => Array 44 | 45 | open.flag :flag 46 | 47 | open.example "todo list tasks open --flag=blah", desc: "example number 1" 48 | open.example "todo list tasks open -x foo" 49 | 50 | open.action do |global,options,args| 51 | puts "tasks open" 52 | end 53 | end 54 | 55 | tasks.default_desc 'list all tasks' 56 | end 57 | 58 | c.desc "List contexts" 59 | c.long_desc %( 60 | Lists all of your contexts, which are places you might be 61 | where you can do stuff and all that. 62 | ) 63 | c.command :contexts do |contexts| 64 | 65 | contexts.desc "Foobar" 66 | contexts.switch [:f,'foobar'] 67 | 68 | contexts.desc "Blah" 69 | contexts.switch [:b] 70 | 71 | contexts.flag :otherflag 72 | 73 | contexts.action do |global,options,args| 74 | puts "list contexts: #{args.join(',')}" 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/gli/commands/help_modules/global_help_format.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | module GLI 4 | module Commands 5 | module HelpModules 6 | class GlobalHelpFormat 7 | def initialize(app,sorter,wrapper_class) 8 | @app = app 9 | @sorter = sorter 10 | @wrapper_class = wrapper_class 11 | end 12 | 13 | def format 14 | program_desc = @app.program_desc 15 | program_long_desc = @app.program_long_desc 16 | if program_long_desc 17 | wrapper = @wrapper_class.new(Terminal.instance.size[0],4) 18 | program_long_desc = "\n #{wrapper.wrap(program_long_desc)}\n\n" if program_long_desc 19 | else 20 | program_long_desc = "\n" 21 | end 22 | 23 | command_formatter = ListFormatter.new(@sorter.call(@app.commands_declaration_order.reject(&:nodoc)).map { |command| 24 | [[command.name,Array(command.aliases)].flatten.join(', '),command.description] 25 | }, @wrapper_class) 26 | stringio = StringIO.new 27 | command_formatter.output(stringio) 28 | commands = stringio.string 29 | 30 | global_option_descriptions = OptionsFormatter.new(global_flags_and_switches,@sorter,@wrapper_class).format 31 | 32 | GLOBAL_HELP.result(binding) 33 | end 34 | 35 | private 36 | 37 | GLOBAL_HELP = ERB.new(%q(NAME 38 | <%= @app.exe_name %> - <%= program_desc %> 39 | <%= program_long_desc %> 40 | SYNOPSIS 41 | <%= usage_string %> 42 | 43 | <% unless @app.version_string.nil? %> 44 | VERSION 45 | <%= @app.version_string %> 46 | 47 | <% end %> 48 | <% unless global_flags_and_switches.empty? %> 49 | GLOBAL OPTIONS 50 | <%= global_option_descriptions %> 51 | 52 | <% end %> 53 | COMMANDS 54 | <%= commands %>)) 55 | 56 | def global_flags_and_switches 57 | @app.flags_declaration_order + @app.switches_declaration_order 58 | end 59 | 60 | def usage_string 61 | "#{@app.exe_name} ".tap do |string| 62 | string << "[global options] " unless global_flags_and_switches.empty? 63 | string << "command " 64 | string << "[command options] [arguments...]" 65 | end 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/gli/commands/initconfig.rb: -------------------------------------------------------------------------------- 1 | require 'gli' 2 | require 'gli/command' 3 | require 'yaml' 4 | require 'fileutils' 5 | 6 | module GLI 7 | # Command that initializes the configuration file for apps that use it. 8 | class InitConfig < Command # :nodoc: 9 | COMMANDS_KEY = 'commands' 10 | 11 | def initialize(config_file_name,commands,flags,switches) 12 | @filename = config_file_name 13 | super(:names => :initconfig, 14 | :description => "Initialize the config file using current global options", 15 | :long_desc => 'Initializes a configuration file where you can set default options for command line flags, both globally and on a per-command basis. These defaults override the built-in defaults and allow you to omit commonly-used command line flags when invoking this program', 16 | :skips_pre => true,:skips_post => true, :skips_around => true) 17 | 18 | @app_commands = commands 19 | @app_flags = flags 20 | @app_switches = switches 21 | 22 | self.desc 'force overwrite of existing config file' 23 | self.switch :force 24 | 25 | action do |global_options,options,arguments| 26 | if options[:force] || !File.exist?(@filename) 27 | create_config(global_options,options,arguments) 28 | else 29 | raise "Not overwriting existing config file #{@filename}, use --force to override" 30 | end 31 | end 32 | end 33 | 34 | 35 | private 36 | 37 | def create_config(global_options,options,arguments) 38 | config = Hash[(@app_switches.keys + @app_flags.keys).map { |option_name| 39 | option_value = global_options[option_name] 40 | [option_name,option_value] 41 | }] 42 | config[COMMANDS_KEY] = {} 43 | @app_commands.each do |name,command| 44 | if (command != self) && (name != :rdoc) && (name != :help) 45 | if command != self 46 | config[COMMANDS_KEY][name.to_sym] = config_for_command(@app_commands,name.to_sym) 47 | end 48 | end 49 | end 50 | 51 | FileUtils.mkdir_p(File.dirname(@filename)) unless File.dirname(@filename) == '.' 52 | 53 | File.open(@filename,'w', 0600) do |file| 54 | YAML.dump(config,file) 55 | puts "Configuration file '#{@filename}' written." 56 | end 57 | end 58 | 59 | def config_for_command(commands,command_name) 60 | {}.tap do |hash| 61 | subcommands = commands[command_name].commands 62 | subcommands.each do |name,subcommand| 63 | next unless name.kind_of? Symbol 64 | hash[COMMANDS_KEY] ||= {} 65 | puts "#{command_name}:#{name}" 66 | hash[COMMANDS_KEY][name.to_sym] = config_for_command(subcommands,name) 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /gli.cheat: -------------------------------------------------------------------------------- 1 | gli - create command-suite apps, a la git, using this awesome Ruby DSL 2 | ====================================================================== 3 | 4 | Setup and Usage 5 | --------------- 6 | 7 | Installation: 8 | $ gem install gli 9 | 10 | Show list of commands 11 | $ gli help [command] 12 | 13 | Show help for one command 14 | $ gli help init 15 | 16 | Create a new GLI-based project with the commands foo and bar 17 | $ gli init project_name foo bar 18 | 19 | Create a new GLI-based project with an ext directory 20 | $ gli init -e project_name foo bar 21 | 22 | Create a new GLI-based project without a test directory (bad!) 23 | $ gli init --notest project_name foo bar 24 | 25 | Create a new GLI-based project somewhere else than . 26 | $ gli -r /tmp init project_name foo bar 27 | 28 | Just see what GLI would create 29 | $ gli -n init -e project_name foo bar 30 | 31 | 32 | Using GLI's DSL 33 | --------------- 34 | 35 | Create a switch (option that takes no args) 36 | 37 | desc 'Dry-run; don't change the disk 38 | switch [:n,'dry-run'] 39 | # Both -n and --dry-run will work 40 | * --no-dry-run will set the switch to false 41 | # Access in code via global_options[:n] 42 | # or global_options[:'dry-run'] 43 | 44 | Create it on one line 45 | switch :n,'dry-run', :desc => 'Dry-run; don't change the disk 46 | 47 | Don't create a negatable version 48 | switch :n,'dry-run', :negatable => false, :desc => 'Dry-run; don't change the disk 49 | # --no-dry-run is not accepted 50 | 51 | Create a flag (option that takes an argument) 52 | 53 | desc 'Location of the config file' 54 | arg_name 'path_to_file' 55 | default_value '~/.glirc' 56 | flag [:c,:conf], :must_match => /^\..*rc$/ 57 | # Both -c and --conf work, if this flag is omitted 58 | # then the default of ~/.glirc is avaialble to the code 59 | # The argument must match the given regex 60 | # Access in code via global_options[:c] (or :conf) 61 | 62 | Create a flag in a more compact style 63 | 64 | flag :c,:conf, :desc => 'Location of the config file', 65 | :arg_name => 'path_to_file', :default_value => '~/.glirc' 66 | 67 | Create a command 68 | 69 | desc 'Get the list of open tickets' 70 | command [:tickets] do |c| 71 | c.desc 'Only assigned to me' 72 | c.switch [:m,:me] 73 | 74 | c.desc 'Show only tickets for one project' 75 | c.flag [:p,:project,'only-project'] 76 | 77 | c.action do |global_options,options,args| 78 | # global_options has the global options as a hash 79 | # options are the command specific ones (e.g. options[:p]) 80 | # args are the command line arguments that weren't parsed 81 | # raise an exception or exit_now! if things go wrong 82 | end 83 | end 84 | 85 | Set up stuff ahead of time 86 | 87 | pre do |global_options,command,options,args| 88 | return true if things_are_ok 89 | return false if we_should_abort_the_command 90 | end 91 | 92 | Use GLI 93 | 94 | exit run(ARGV) 95 | # run returns a suitable exit status 96 | -------------------------------------------------------------------------------- /lib/gli/option_parser_factory.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | # Factory for creating an OptionParser based on app configuration and DSL calls 3 | class OptionParserFactory 4 | 5 | # Create an option parser factory for a command. This has the added 6 | # feature of setting up -h and --help on the command if those 7 | # options aren't otherwise configured, e.g. to allow todo add --help as an 8 | # alternate to todo help add 9 | def self.for_command(command,accepts) 10 | self.new(command.flags,command.switches,accepts).tap { |factory| 11 | add_help_switches_to_command(factory.option_parser,command) 12 | } 13 | end 14 | 15 | # Create an OptionParserFactory for the given 16 | # flags, switches, and accepts 17 | def initialize(flags,switches,accepts) 18 | @flags = flags 19 | @switches = switches 20 | @options_hash = {} 21 | @option_parser = OptionParser.new do |opts| 22 | self.class.setup_accepts(opts,accepts) 23 | self.class.setup_options(opts,@switches,@options_hash) 24 | self.class.setup_options(opts,@flags,@options_hash) 25 | end 26 | end 27 | 28 | attr_reader :option_parser 29 | attr_reader :options_hash 30 | 31 | def options_hash_with_defaults_set! 32 | set_defaults(@flags,@options_hash) 33 | set_defaults(@switches,@options_hash) 34 | @options_hash 35 | end 36 | 37 | private 38 | 39 | def set_defaults(options_by_name,options_hash) 40 | options_by_name.values.each do |option| 41 | option.names_and_aliases.each do |option_name| 42 | [option_name,option_name.to_sym].each do |name| 43 | options_hash[name] = option.default_value if options_hash[name].nil? 44 | end 45 | end 46 | end 47 | end 48 | 49 | def self.setup_accepts(opts,accepts) 50 | accepts.each do |object,block| 51 | opts.accept(object) do |arg_as_string| 52 | block.call(arg_as_string) 53 | end 54 | end 55 | end 56 | 57 | def self.setup_options(opts,tokens,options) 58 | tokens.each do |ignore,token| 59 | opts.on(*token.arguments_for_option_parser) do |arg| 60 | token.names_and_aliases.each do |name| 61 | if token.kind_of?(Flag) && token.multiple? 62 | options[name] ||= [] 63 | options[name.to_sym] ||= [] 64 | options[name] << arg 65 | options[name.to_sym] << arg 66 | else 67 | options[name] = arg 68 | options[name.to_sym] = arg 69 | end 70 | end 71 | end 72 | end 73 | end 74 | 75 | def self.add_help_switches_to_command(option_parser,command) 76 | help_args = %w(-h --help).reject { |_| command.has_option?(_) } 77 | 78 | unless help_args.empty? 79 | help_args << "Get help for #{command.name}" 80 | option_parser.on(*help_args) do 81 | raise RequestHelp.new(command) 82 | end 83 | end 84 | end 85 | 86 | 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/gli/gli_option_block_parser.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | # An "option block" is a set of parseable options, starting from the beginning of 3 | # the argument list, stopping with the first unknown command-line element. 4 | # This class handles parsing that block 5 | class GLIOptionBlockParser 6 | 7 | # Create the parser using the given +OptionParser+ instance and exception handling 8 | # strategy. 9 | # 10 | # option_parser_factory:: An +OptionParserFactory+ instance, configured to parse wherever you are on the command line 11 | # exception_klass_or_block:: means of handling exceptions from +OptionParser+. One of: 12 | # an exception class:: will be raised on errors with a message 13 | # lambda/block:: will be called with a single argument - the error message. 14 | def initialize(option_parser_factory,exception_klass_or_block) 15 | @option_parser_factory = option_parser_factory 16 | @extra_error_context = nil 17 | @exception_handler = if exception_klass_or_block.kind_of?(Class) 18 | lambda { |message,extra_error_context| 19 | raise exception_klass_or_block,message 20 | } 21 | else 22 | exception_klass_or_block 23 | end 24 | end 25 | 26 | # Parse the given argument list, returning the unparsed arguments and options hash of parsed arguments. 27 | # Exceptions from +OptionParser+ are given to the handler configured in the constructor 28 | # 29 | # args:: argument list. This will be mutated 30 | # 31 | # Returns unparsed args 32 | def parse!(args) 33 | do_parse(args) 34 | rescue OptionParser::InvalidOption => ex 35 | @exception_handler.call("Unknown option #{ex.args.join(' ')}",@extra_error_context) 36 | rescue OptionParser::InvalidArgument => ex 37 | @exception_handler.call("#{ex.reason}: #{ex.args.join(' ')}",@extra_error_context) 38 | end 39 | 40 | protected 41 | 42 | def do_parse(args) 43 | first_non_option = nil 44 | @option_parser_factory.option_parser.order!(args) do |non_option| 45 | first_non_option = non_option 46 | break 47 | end 48 | args.unshift(first_non_option) 49 | end 50 | end 51 | 52 | class CommandOptionBlockParser < GLIOptionBlockParser 53 | 54 | def command=(command_being_parsed) 55 | @extra_error_context = command_being_parsed 56 | end 57 | 58 | protected 59 | 60 | def break_on_non_option? 61 | true 62 | end 63 | 64 | def do_parse(args) 65 | unknown_options = [] 66 | @option_parser_factory.option_parser.order!(args) do |non_option| 67 | unknown_options << non_option 68 | break if break_on_non_option? 69 | end 70 | unknown_options.reverse.each do |unknown_option| 71 | args.unshift(unknown_option) 72 | end 73 | args 74 | end 75 | end 76 | 77 | class LegacyCommandOptionBlockParser < CommandOptionBlockParser 78 | 79 | protected 80 | def break_on_non_option? 81 | false 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/gli/exceptions.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | # Mixed into all exceptions that GLI handles; you can use this to catch 3 | # anything that came from GLI intentionally. You can also mix this into non-GLI 4 | # exceptions to get GLI's exit behavior. 5 | module StandardException 6 | def exit_code; 1; end 7 | end 8 | 9 | # Hack to request help from within a command 10 | # Will *not* be rethrown when GLI_DEBUG is ON 11 | class RequestHelp < StandardError 12 | include StandardException 13 | def exit_code; 0; end 14 | 15 | # The command for which the argument was unknown 16 | attr_reader :command_in_context 17 | 18 | def initialize(command_in_context) 19 | @command_in_context = command_in_context 20 | end 21 | end 22 | 23 | # Indicates that the command line invocation was bad 24 | class BadCommandLine < StandardError 25 | include StandardException 26 | def exit_code; 64; end 27 | end 28 | 29 | class PreconditionFailed < StandardError 30 | include StandardException 31 | def exit_code; 65; end 32 | end 33 | 34 | # Indicates the bad command line was an unknown command 35 | class UnknownCommand < BadCommandLine 36 | end 37 | 38 | # The command issued partially matches more than one command 39 | class AmbiguousCommand < BadCommandLine 40 | end 41 | 42 | # Indicates the bad command line was an unknown global argument 43 | class UnknownGlobalArgument < BadCommandLine 44 | end 45 | 46 | class CommandException < BadCommandLine 47 | # The command for which the argument was unknown 48 | attr_reader :command_in_context 49 | # +message+:: the error message to show the user 50 | # +command+:: the command we were using to parse command-specific options 51 | def initialize(message,command_in_context,exit_code=nil) 52 | super(message) 53 | @command_in_context = command_in_context 54 | @exit_code = exit_code 55 | end 56 | 57 | def exit_code 58 | @exit_code || super 59 | end 60 | end 61 | 62 | class MissingRequiredArgumentsException < BadCommandLine 63 | # The command for which the argument was unknown 64 | attr_reader :command_in_context 65 | # +message+:: the error message to show the user 66 | # +command+:: the command we were using to parse command-specific options 67 | def initialize(message,command) 68 | super(message) 69 | @command_in_context = command 70 | end 71 | end 72 | 73 | # Indicates the bad command line was an unknown command argument 74 | class UnknownCommandArgument < CommandException 75 | end 76 | 77 | # Raise this if you want to use an exit status that isn't the default 78 | # provided by GLI. Note that GLI::App#exit_now! might be a bit more to your liking. 79 | # 80 | # Example: 81 | # 82 | # raise CustomExit.new("Not connected to DB",-5) unless connected? 83 | # raise CustomExit.new("Bad SQL",-6) unless valid_sql?(args[0]) 84 | # 85 | class CustomExit < StandardError 86 | include StandardException 87 | attr_reader :exit_code #:nodoc: 88 | # Create a custom exit exception 89 | # 90 | # +message+:: String containing error message to show the user 91 | # +exit_code+:: the exit code to use (as an Int), overridding GLI's default 92 | def initialize(message,exit_code) 93 | super(message) 94 | @exit_code = exit_code 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'sdoc' 2 | require 'bundler' 3 | require 'rake/clean' 4 | require 'rake/testtask' 5 | require 'rdoc/task' 6 | 7 | include Rake::DSL 8 | 9 | CLEAN << "log" 10 | CLOBBER << FileList['**/*.rbc'] 11 | 12 | 13 | task :rdoc => [:build_rdoc, :hack_css] 14 | Rake::RDocTask.new(:build_rdoc) do |rd| 15 | rd.main = "README.rdoc" 16 | rd.rdoc_files = FileList["lib/**/*.rb","README.rdoc"] - 17 | FileList["lib/gli/commands/help_modules/*.rb"] - 18 | ["lib/gli/commands/help.rb", 19 | "lib/gli/commands/scaffold.rb", 20 | "lib/gli/support/*.rb", 21 | "lib/gli/app_support.rb", 22 | "lib/gli/option_parser_factory.rb", 23 | "lib/gli/gli_option_parser.rb", 24 | "lib/gli/command_support.rb",] 25 | rd.title = 'GLI - Git Like Interface for your command-line apps' 26 | rd.options << '-f' << 'sdoc' 27 | rd.template = 'direct' 28 | end 29 | 30 | FONT_FIX = { 31 | "0.82em" => "16px", 32 | "0.833em" => "16px", 33 | "0.85em" => "16px", 34 | "1.15em" => "20px", 35 | "1.1em" => "20px", 36 | "1.2em" => "20px", 37 | "1.4em" => "24px", 38 | "1.5em" => "24px", 39 | "1.6em" => "32px", 40 | "1em" => "16px", 41 | "2.1em" => "38px", 42 | } 43 | 44 | 45 | task :hack_css do 46 | maincss = File.open('html/css/main.css').readlines 47 | File.open('html/css/main.css','w') do |file| 48 | file.puts '@import url(http://fonts.googleapis.com/css?family=Karla:400,700,400italic,700italic|Alegreya);' 49 | 50 | maincss.each do |line| 51 | if line.strip == 'font-family: "Helvetica Neue", Arial, sans-serif;' 52 | file.puts 'font-family: Karla, "Helvetica Neue", Arial, sans-serif;' 53 | elsif line.strip == 'font-family: monospace;' 54 | file.puts 'font-family: Monaco, monospace;' 55 | elsif line =~ /^pre\s*$/ 56 | file.puts "pre { 57 | font-family: Monaco, monospace; 58 | margin-bottom: 1em; 59 | } 60 | pre.original" 61 | elsif line =~ /^\s*font-size:\s*(.*)\s*;/ 62 | if FONT_FIX[$1] 63 | file.puts "font-size: #{FONT_FIX[$1]};" 64 | else 65 | file.puts line.chomp 66 | end 67 | else 68 | file.puts line.chomp 69 | end 70 | end 71 | end 72 | end 73 | 74 | Bundler::GemHelper.install_tasks 75 | 76 | desc "run unit tests" 77 | Rake::TestTask.new("test:unit") do |t| 78 | t.libs << "test" 79 | t.libs << "lib" 80 | ENV["RUBYOPT"].split(/\s/).each do |opt| 81 | t.ruby_opts << opt 82 | end 83 | t.test_files = FileList["test/unit/**/*_test.rb"] 84 | end 85 | 86 | desc "run integration tests" 87 | Rake::TestTask.new("test:integration") do |t| 88 | t.libs << "test" 89 | ENV["RUBYOPT"].split(/\s/).each do |opt| 90 | t.ruby_opts << opt 91 | end 92 | explicitly_named_files = ARGV[1..-1] 93 | if Array(explicitly_named_files).size == 0 94 | t.test_files = FileList["test/integration/**/*_test.rb"] 95 | else 96 | t.test_files = explicitly_named_files 97 | end 98 | end 99 | 100 | 101 | begin 102 | require 'simplecov' 103 | rescue LoadError 104 | end 105 | 106 | desc 'Publish rdoc on github pages and push to github' 107 | task :publish_rdoc => [:rdoc,:publish] 108 | 109 | task :default => ["test:unit", "test:integraton"] 110 | 111 | -------------------------------------------------------------------------------- /lib/gli/flag.rb: -------------------------------------------------------------------------------- 1 | require 'gli/command_line_option.rb' 2 | 3 | module GLI 4 | # Defines a flag, which is to say a switch that takes an argument 5 | class Flag < CommandLineOption # :nodoc: 6 | 7 | # Regexp that is used to see if the flag's argument matches 8 | attr_reader :must_match 9 | 10 | # Type to which we want to cast the values 11 | attr_reader :type 12 | 13 | # Name of the argument that user configured 14 | attr_reader :argument_name 15 | 16 | # Creates a new option 17 | # 18 | # names:: Array of symbols or strings representing the names of this switch 19 | # options:: hash of options: 20 | # :desc:: the short description 21 | # :long_desc:: the long description 22 | # :default_value:: the default value of this option 23 | # :arg_name:: the name of the flag's argument, default is "arg" 24 | # :must_match:: a regexp that the flag's value must match 25 | # :type:: a class to convert the value to 26 | # :required:: if true, this flag must be specified on the command line 27 | # :multiple:: if true, flag may be used multiple times and values are stored in an array 28 | # :mask:: if true, the default value of this flag will not be output in the help. 29 | # This is useful for password flags where you might not want to show it 30 | # on the command-line. 31 | def initialize(names,options) 32 | super(names,options) 33 | @argument_name = options[:arg_name] || "arg" 34 | @must_match = options[:must_match] 35 | @type = options[:type] 36 | @mask = options[:mask] 37 | @required = options[:required] 38 | @multiple = options[:multiple] 39 | end 40 | 41 | # True if this flag is required on the command line 42 | def required? 43 | @required 44 | end 45 | 46 | # True if the flag may be used multiple times. 47 | def multiple? 48 | @multiple 49 | end 50 | 51 | def safe_default_value 52 | if @mask 53 | "********" 54 | else 55 | # This uses @default_value instead of the `default_value` method because 56 | # this method is only used for display, and for flags that may be passed 57 | # multiple times, we want to display whatever is set in the code as the 58 | # the default, or the string "none" rather than displaying an empty 59 | # array. 60 | @default_value 61 | end 62 | end 63 | 64 | # The default value for this flag. Uses the value passed if one is set; 65 | # otherwise uses `[]` if the flag support multiple arguments and `nil` if 66 | # it does not. 67 | def default_value 68 | if @default_value 69 | @default_value 70 | elsif @multiple 71 | [] 72 | end 73 | end 74 | 75 | def arguments_for_option_parser 76 | args = all_forms_a.map { |name| "#{name} VAL" } 77 | args << @must_match if @must_match 78 | args << @type if @type 79 | args 80 | end 81 | 82 | # Returns a string of all possible forms 83 | # of this flag. Mostly intended for printing 84 | # to the user. 85 | def all_forms(joiner=', ') 86 | forms = all_forms_a 87 | string = forms.join(joiner) 88 | if forms[-1] =~ /^\-\-/ 89 | string += '=' 90 | else 91 | string += ' ' 92 | end 93 | string += @argument_name 94 | return string 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/gli/commands/help_modules/command_help_format.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | module GLI 4 | module Commands 5 | module HelpModules 6 | class CommandHelpFormat 7 | def initialize(command,app,sorter,synopsis_formatter_class,wrapper_class=TextWrapper) 8 | @app = app 9 | @command = command 10 | @sorter = sorter 11 | @wrapper_class = wrapper_class 12 | @synopsis_formatter = synopsis_formatter_class.new(@app,flags_and_switches(@command,@app)) 13 | end 14 | 15 | def format 16 | command_wrapper = @wrapper_class.new(Terminal.instance.size[0],4 + @command.name.to_s.size + 3) 17 | wrapper = @wrapper_class.new(Terminal.instance.size[0],4) 18 | 19 | options_description = OptionsFormatter.new(flags_and_switches(@command,@app),@sorter,@wrapper_class).format 20 | commands_description = format_subcommands(@command) 21 | command_examples = format_examples(@command) 22 | 23 | synopses = @synopsis_formatter.synopses_for_command(@command) 24 | COMMAND_HELP.result(binding) 25 | end 26 | 27 | private 28 | COMMAND_HELP = ERB.new(%q(NAME 29 | <%= @command.name %> - <%= command_wrapper.wrap(@command.description) %> 30 | 31 | SYNOPSIS 32 | <% synopses.each do |s| %> 33 | <%= s %> 34 | <% end %> 35 | <% unless @command.long_description.nil? %> 36 | 37 | DESCRIPTION 38 | <%= wrapper.wrap(@command.long_description) %> 39 | <% end %> 40 | <% if options_description.strip.length != 0 %> 41 | 42 | COMMAND OPTIONS 43 | <%= options_description %> 44 | <% end %> 45 | <% unless @command.commands.empty? %> 46 | 47 | COMMANDS 48 | <%= commands_description %> 49 | <% end %> 50 | <% unless @command.examples.empty? %> 51 | 52 | <%= @command.examples.size == 1 ? 'EXAMPLE' : 'EXAMPLES' %> 53 | 54 | 55 | <%= command_examples %> 56 | <% end %>)) 57 | 58 | 59 | def flags_and_switches(command,app) 60 | if app.subcommand_option_handling_strategy == :legacy 61 | ( 62 | command.topmost_ancestor.flags_declaration_order + 63 | command.topmost_ancestor.switches_declaration_order 64 | ).select { |option| option.associated_command == command } 65 | else 66 | ( 67 | command.flags_declaration_order + 68 | command.switches_declaration_order 69 | ) 70 | end 71 | end 72 | 73 | def format_subcommands(command) 74 | commands_array = @sorter.call(command.commands_declaration_order).map { |cmd| 75 | if command.get_default_command == cmd.name 76 | [cmd.names,String(cmd.description) + " (default)"] 77 | else 78 | [cmd.names,cmd.description] 79 | end 80 | } 81 | if command.has_action? 82 | commands_array.unshift(["",command.default_description]) 83 | end 84 | formatter = ListFormatter.new(commands_array,@wrapper_class) 85 | StringIO.new.tap { |io| formatter.output(io) }.string 86 | end 87 | 88 | def format_examples(command) 89 | command.examples.map {|example| 90 | string = "" 91 | if example[:desc] 92 | string << " # #{example[:desc]}\n" 93 | end 94 | string << " #{example.fetch(:example)}\n" 95 | }.join("\n") 96 | end 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/gli/commands/rdoc_document_listener.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | require 'gli/commands/help_modules/arg_name_formatter' 3 | module GLI 4 | module Commands 5 | class RdocDocumentListener 6 | 7 | def initialize(global_options,options,arguments,app) 8 | @app = app 9 | @io = File.new("#{app.exe_name}.rdoc",'w') 10 | @nest = '' 11 | @arg_name_formatter = GLI::Commands::HelpModules::ArgNameFormatter.new 12 | end 13 | 14 | def beginning 15 | end 16 | 17 | # Called when processing has completed 18 | def ending 19 | @io.close 20 | end 21 | 22 | # Gives you the program description 23 | def program_desc(desc) 24 | @io.puts "== #{@app.exe_name} - #{desc}" 25 | @io.puts 26 | end 27 | 28 | def program_long_desc(desc) 29 | @io.puts desc 30 | @io.puts 31 | end 32 | 33 | # Gives you the program version 34 | def version(version) 35 | @io.puts "v#{version}" 36 | @io.puts 37 | end 38 | 39 | def options 40 | if @nest.size == 0 41 | @io.puts "=== Global Options" 42 | else 43 | @io.puts "#{@nest}=== Options" 44 | end 45 | end 46 | 47 | # Gives you a flag in the current context 48 | def flag(name,aliases,desc,long_desc,default_value,arg_name,must_match,type) 49 | invocations = ([name] + Array(aliases)).map { |_| add_dashes(_) }.join('|') 50 | usage = "#{invocations} #{arg_name || 'arg'}" 51 | @io.puts "#{@nest}=== #{usage}" 52 | @io.puts 53 | @io.puts String(desc).strip 54 | @io.puts 55 | @io.puts "[Default Value] #{default_value || 'None'}" 56 | @io.puts "[Must Match] #{must_match.to_s}" unless must_match.nil? 57 | @io.puts String(long_desc).strip 58 | @io.puts 59 | end 60 | 61 | # Gives you a switch in the current context 62 | def switch(name,aliases,desc,long_desc,negatable) 63 | if negatable 64 | name = "[no-]#{name}" if name.to_s.length > 1 65 | aliases = aliases.map { |_| _.to_s.length > 1 ? "[no-]#{_}" : _ } 66 | end 67 | invocations = ([name] + aliases).map { |_| add_dashes(_) }.join('|') 68 | @io.puts "#{@nest}=== #{invocations}" 69 | @io.puts String(desc).strip 70 | @io.puts 71 | @io.puts String(long_desc).strip 72 | @io.puts 73 | end 74 | 75 | def end_options 76 | end 77 | 78 | def commands 79 | @io.puts "#{@nest}=== Commands" 80 | @nest = "#{@nest}=" 81 | end 82 | 83 | # Gives you a command in the current context and creates a new context of this command 84 | def command(name,aliases,desc,long_desc,arg_name,arg_options) 85 | @io.puts "#{@nest}=== Command: #{([name] + aliases).join('|')} #{@arg_name_formatter.format(arg_name,arg_options,[])}" 86 | @io.puts String(desc).strip 87 | @io.puts 88 | @io.puts String(long_desc).strip 89 | @nest = "#{@nest}=" 90 | end 91 | 92 | # Ends a command, and "pops" you back up one context 93 | def end_command(name) 94 | @nest.gsub!(/=$/,'') 95 | end 96 | 97 | # Gives you the name of the current command in the current context 98 | def default_command(name) 99 | @io.puts "[Default Command] #{name}" unless name.nil? 100 | end 101 | 102 | def end_commands 103 | @nest.gsub!(/=$/,'') 104 | end 105 | 106 | private 107 | 108 | def add_dashes(name) 109 | name = "-#{name}" 110 | name = "-#{name}" if name.length > 2 111 | name 112 | end 113 | 114 | 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /test/unit/terminal_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class TerminalTest < MiniTest::Test 4 | include TestHelper 5 | 6 | # TODO: Make this test not mess with the internals of the class 7 | def xtest_command_exists 8 | assert GLI::Terminal.instance.command_exists?('ls') 9 | assert !GLI::Terminal.instance.command_exists?('asdfasfasdf') 10 | end 11 | 12 | def setup 13 | @old_columns = ENV['COLUMNS'] 14 | @old_lines = ENV['LINES'] 15 | end 16 | 17 | def teardown 18 | ENV['COLUMNS'] = @old_columns 19 | ENV['LINES'] = @old_lines 20 | GLI::Terminal.default_size = [80,24] 21 | end 22 | 23 | def test_shared_instance_is_same 24 | assert_equal GLI::Terminal.instance,GLI::Terminal.instance 25 | end 26 | 27 | def test_size_based_on_columns 28 | ENV['COLUMNS'] = '666' 29 | ENV['LINES'] = '777' 30 | assert_equal [666,777],GLI::Terminal.instance.size 31 | end 32 | 33 | def test_size_using_tput 34 | terminal = GLI::Terminal.new 35 | terminal.make_unsafe! 36 | GLI::Terminal.instance_eval do 37 | def run_command(command) 38 | if command == 'tput cols' 39 | return '888' 40 | elsif command == 'tput lines' 41 | return '999' 42 | else 43 | raise "Unexpected command called: #{command}" 44 | end 45 | end 46 | def command_exists?(command); true; end 47 | def jruby?; true; end 48 | end 49 | ENV['COLUMNS'] = 'foo' 50 | assert_equal [888,999],terminal.size 51 | end 52 | 53 | def test_size_using_stty 54 | terminal = GLI::Terminal.new 55 | terminal.make_unsafe! 56 | GLI::Terminal.instance_eval do 57 | def run_command(command) 58 | 59 | if RUBY_PLATFORM == 'java' 60 | return '5678' if command == 'tput cols' 61 | return '1234' if command == 'tput lines' 62 | else 63 | return '1234 5678' if command == 'stty size' 64 | return '1234 5678' if command == 'stty' 65 | end 66 | 67 | raise "Unexpected command called: #{command} for #{RUBY_PLATFORM}" 68 | end 69 | def command_exists?(command); true; end 70 | def jruby?; false; end 71 | def solaris?; false; end 72 | end 73 | ENV['COLUMNS'] = 'foo' 74 | assert_equal [5678,1234],terminal.size 75 | end 76 | 77 | def test_size_using_stty_but_returns_0 78 | terminal = GLI::Terminal.new 79 | terminal.make_unsafe! 80 | GLI::Terminal.instance_eval do 81 | def run_command(command) 82 | 83 | if RUBY_PLATFORM == 'java' 84 | return '0' if command == 'tput cols' 85 | return '0' if command == 'tput lines' 86 | else 87 | return '0 0' if command == 'stty size' 88 | return '0 0' if command == 'stty' 89 | end 90 | 91 | raise "Unexpected command called: #{command} for #{RUBY_PLATFORM}" 92 | end 93 | def command_exists?(command); true; end 94 | def jruby?; false; end 95 | def solaris?; false; end 96 | end 97 | ENV['COLUMNS'] = 'foo' 98 | assert_equal [80,24],terminal.size 99 | end 100 | 101 | def test_size_using_default 102 | terminal = GLI::Terminal.new 103 | terminal.make_unsafe! 104 | GLI::Terminal.instance_eval do 105 | def command_exists?(command); false; end 106 | def jruby?; false; end 107 | def solaris?; false; end 108 | end 109 | ENV['COLUMNS'] = 'foo' 110 | assert_equal [80,24],terminal.size 111 | # While we have this set up, lets make sure the default change falls through 112 | GLI::Terminal.default_size = [90,45] 113 | assert_equal [90,45],terminal.size 114 | end 115 | 116 | def test_size_using_default_when_exception 117 | terminal = GLI::Terminal.new 118 | GLI::Terminal.instance_eval do 119 | def jruby?; raise "Problem"; end 120 | def solaris?; false; end 121 | end 122 | ENV['COLUMNS'] = 'foo' 123 | assert_equal [80,24],terminal.size 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/gli/terminal.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | # Class to encapsulate stuff about the terminal. This is useful to application developers 3 | # as a canonical means to get information about the user's current terminal configuraiton. 4 | # GLI uses this to determine the number of columns to use when printing to the screen. 5 | # 6 | # To access it, use Terminal.instance. This is a singleton mostly to facilitate testing, but 7 | # it seems reasonable enough, since there's only one terminal in effect 8 | # 9 | # Example: 10 | # 11 | # Terminal.instance.size[0] # => columns in the terminal 12 | # Terminal.default_size = [128,24] # => change default when we can't figure it out 13 | # raise "no ls?!?!?" unless Terminal.instance.command_exists?("ls") 14 | # 15 | class Terminal 16 | 17 | @@default_size = [80,24] 18 | 19 | # Get the default size of the terminal when we can't figure it out 20 | # 21 | # Returns an array of int [cols,rows] 22 | def self.default_size 23 | @@default_size 24 | end 25 | 26 | # Set the default size of the terminal to use when we can't figure it out 27 | # 28 | # +size+:: array of two int [cols,rows] 29 | def self.default_size=(size) 30 | @@default_size = size 31 | end 32 | 33 | # Provide access to the shared instance. 34 | def self.instance; @@instance ||= Terminal.new; end 35 | 36 | # Call this to cause methods to throw exceptions rather than return a sane default. You 37 | # probably don't want to call this unless you are writing tests 38 | def make_unsafe! #:nodoc: 39 | @unsafe = true 40 | end 41 | 42 | # Returns true if the given command exists on this system 43 | # 44 | # +command+:: The command, as a String, to check for, without any path information. 45 | def self.command_exists?(command) 46 | ENV['PATH'].split(File::PATH_SEPARATOR).any? {|dir| File.exist? File.join(dir, command) } 47 | end 48 | 49 | def command_exists?(command) 50 | self.class.command_exists?(command) 51 | end 52 | 53 | SIZE_DETERMINERS = [ 54 | [ 55 | lambda { (ENV['COLUMNS'] =~ /^\d+$/) && (ENV['LINES'] =~ /^\d+$/) }, 56 | lambda { [ENV['COLUMNS'].to_i, ENV['LINES'].to_i] } 57 | ], 58 | [ 59 | lambda { (jruby? || (!STDIN.tty? && ENV['TERM'])) && command_exists?('tput') }, 60 | lambda { [run_command('tput cols').to_i, run_command('tput lines').to_i] } 61 | ], 62 | [ 63 | lambda { (solaris? && STDIN.tty? && command_exists?('stty')) }, 64 | lambda { run_command('stty').split("\n")[1].scan(/\d+/)[0..1].map { |size_element| size_element.to_i }.reverse } 65 | ], 66 | [ 67 | lambda { STDIN.tty? && command_exists?('stty') }, 68 | lambda { run_command('stty size').scan(/\d+/).map { |size_element| size_element.to_i }.reverse } 69 | ], 70 | [ 71 | lambda { true }, 72 | lambda { Terminal.default_size }, 73 | ], 74 | ] 75 | 76 | # Get the size of the current terminal. 77 | # Ripped from hirb[https://github.com/cldwalker/hirb/blob/master/lib/hirb/util.rb] 78 | # 79 | # Returns an Array of size two Ints representing the terminal width and height 80 | def size 81 | SIZE_DETERMINERS.each do |predicate, get_size| 82 | next unless predicate.call 83 | size = get_size.call 84 | return size unless size == [0, 0] 85 | end 86 | rescue Exception => ex 87 | raise ex if @unsafe 88 | Terminal.default_size 89 | end 90 | 91 | private 92 | 93 | # Runs a command using backticks. Extracted to allow for testing 94 | def self.run_command(command) 95 | `#{command}` 96 | end 97 | 98 | # True if we are JRuby; exposed to allow for testing 99 | def self.jruby?; RUBY_PLATFORM =~ /java/; end 100 | 101 | # True if this is running under Solaris Sparc 102 | def self.solaris?; RUBY_PLATFORM =~ /solaris/; end 103 | 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/gli/commands/help_modules/full_synopsis_formatter.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | module Commands 3 | module HelpModules 4 | class FullSynopsisFormatter 5 | def initialize(app,flags_and_switches) 6 | @app = app 7 | @basic_invocation = @app.exe_name.to_s 8 | @flags_and_switches = flags_and_switches 9 | end 10 | 11 | def synopses_for_command(command) 12 | synopses = [] 13 | one_line_usage = basic_usage(command) 14 | one_line_usage << ArgNameFormatter.new.format(command.arguments_description,command.arguments_options,command.arguments).strip 15 | if command.commands.empty? 16 | synopses << one_line_usage 17 | else 18 | synopses = sorted_synopses(command) 19 | if command.has_action? 20 | synopses.unshift(one_line_usage) 21 | end 22 | end 23 | synopses 24 | end 25 | 26 | protected 27 | 28 | def sub_options_doc(sub_options) 29 | sub_options_doc = sub_options.map { |_,option| 30 | doc = option.names_and_aliases.map { |name| 31 | CommandLineOption.name_as_string(name,false) + (option.kind_of?(Flag) ? " #{option.argument_name }" : '') 32 | }.join('|') 33 | option.required?? doc : "[#{doc}]" 34 | }.sort.join(' ').strip 35 | end 36 | 37 | private 38 | 39 | def path_to_command(command) 40 | path = [] 41 | c = command 42 | while c.kind_of? Command 43 | path.unshift(c.name) 44 | c = c.parent 45 | end 46 | path.join(' ') 47 | end 48 | 49 | 50 | def basic_usage(command) 51 | usage = @basic_invocation.dup 52 | usage << " [global options]" unless global_flags_and_switches.empty? 53 | usage << " #{path_to_command(command)}" 54 | usage << " [command options]" unless @flags_and_switches.empty? 55 | usage << " " 56 | usage 57 | end 58 | 59 | 60 | def command_with_subcommand_usage(command,sub,is_default_command) 61 | usage = basic_usage(command) 62 | sub_options = if @app.subcommand_option_handling_strategy == :legacy 63 | command.flags.merge(command.switches).select { |_,o| o.associated_command == sub } 64 | else 65 | sub.flags.merge(sub.switches) 66 | end 67 | if is_default_command 68 | usage << "[#{sub.name}]" 69 | else 70 | usage << sub.name.to_s 71 | end 72 | sub_options_doc = sub_options_doc(sub_options) 73 | if sub_options_doc.length > 0 74 | usage << ' ' 75 | usage << sub_options_doc 76 | end 77 | arg_name_doc = ArgNameFormatter.new.format(sub.arguments_description,sub.arguments_options,sub.arguments).strip 78 | if arg_name_doc.length > 0 79 | usage << ' ' 80 | usage << arg_name_doc 81 | end 82 | usage 83 | end 84 | 85 | def sorted_synopses(command) 86 | synopses_command = {} 87 | command.commands.each do |name,sub| 88 | default = command.get_default_command == name 89 | synopsis = command_with_subcommand_usage(command,sub,default) 90 | synopses_command[synopsis] = sub 91 | end 92 | synopses = synopses_command.keys.sort { |one,two| 93 | if synopses_command[one].name == command.get_default_command 94 | -1 95 | elsif synopses_command[two].name == command.get_default_command 96 | 1 97 | else 98 | synopses_command[one] <=> synopses_command[two] 99 | end 100 | } 101 | end 102 | 103 | def global_flags_and_switches 104 | @app.flags.merge(@app.switches) 105 | end 106 | 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/gli/commands/help.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | require 'gli/command' 3 | require 'gli/terminal' 4 | require 'gli/commands/help_modules/list_formatter' 5 | require 'gli/commands/help_modules/text_wrapper' 6 | require 'gli/commands/help_modules/one_line_wrapper' 7 | require 'gli/commands/help_modules/verbatim_wrapper' 8 | require 'gli/commands/help_modules/tty_only_wrapper' 9 | require 'gli/commands/help_modules/options_formatter' 10 | require 'gli/commands/help_modules/global_help_format' 11 | require 'gli/commands/help_modules/command_help_format' 12 | require 'gli/commands/help_modules/help_completion_format' 13 | require 'gli/commands/help_modules/command_finder' 14 | require 'gli/commands/help_modules/arg_name_formatter' 15 | require 'gli/commands/help_modules/full_synopsis_formatter' 16 | require 'gli/commands/help_modules/compact_synopsis_formatter' 17 | require 'gli/commands/help_modules/terminal_synopsis_formatter' 18 | 19 | module GLI 20 | module Commands 21 | SORTERS = { 22 | :manually => lambda { |list| list }, 23 | :alpha => lambda { |list| list.sort }, 24 | } 25 | 26 | WRAPPERS = { 27 | :to_terminal => HelpModules::TextWrapper, 28 | :never => HelpModules::OneLineWrapper, 29 | :one_line => HelpModules::OneLineWrapper, 30 | :tty_only => HelpModules::TTYOnlyWrapper, 31 | :none => HelpModules::VerbatimWrapper, 32 | :verbatim => HelpModules::VerbatimWrapper, 33 | } 34 | 35 | SYNOPSIS_FORMATTERS = { 36 | :full => HelpModules::FullSynopsisFormatter, 37 | :compact => HelpModules::CompactSynopsisFormatter, 38 | :terminal => HelpModules::TerminalSynopsisFormatter, 39 | } 40 | # The help command used for the two-level interactive help system 41 | class Help < Command 42 | @@skips_pre = true 43 | @@skips_post = true 44 | @@skips_around = true 45 | 46 | # Configure help to explicitly skip or not skip the pre block when the help command runs. 47 | # This is here because the creation of the help command is outside of the client programmer's control 48 | def self.skips_pre=(skips_pre) ; @@skips_pre = skips_pre ; end 49 | 50 | # Configure help to explicitly skip or not skip the post block when the help command runs. 51 | # This is here because the creation of the help command is outside of the client programmer's control 52 | def self.skips_post=(skips_post) ; @@skips_post = skips_post ; end 53 | 54 | # Configure help to explicitly skip or not skip the around block when the help command runs. 55 | # This is here because the creation of the help command is outside of the client programmer's control 56 | def self.skips_around=(skips_around) ; @@skips_around = skips_around ; end 57 | 58 | def initialize(app,output=$stdout,error=$stderr) 59 | super(:names => :help, 60 | :description => 'Shows a list of commands or help for one command', 61 | :arguments_name => 'command', 62 | :long_desc => 'Gets help for the application or its commands. Can also list the commands in a way helpful to creating a bash-style completion function', 63 | :arguments => [Argument.new(:command_name, [:multiple, :optional])]) 64 | @app = app 65 | @parent = app 66 | @sorter = SORTERS[@app.help_sort_type] 67 | @text_wrapping_class = WRAPPERS[@app.help_text_wrap_type] 68 | @synopsis_formatter_class = SYNOPSIS_FORMATTERS[@app.synopsis_format_type] 69 | 70 | desc 'List commands one per line, to assist with shell completion' 71 | switch :c 72 | 73 | action do |global_options,options,arguments| 74 | if global_options[:version] && !global_options[:help] 75 | puts "#{@app.exe_name} version #{@app.version_string}" 76 | else 77 | show_help(global_options,options,arguments,output,error) 78 | end 79 | end 80 | end 81 | 82 | def skips_pre ; @@skips_pre ; end 83 | def skips_post ; @@skips_post ; end 84 | def skips_around ; @@skips_around ; end 85 | 86 | private 87 | 88 | def show_help(global_options,options,arguments,out,error) 89 | command_finder = HelpModules::CommandFinder.new(@app,arguments,error) 90 | if options[:c] 91 | help_output = HelpModules::HelpCompletionFormat.new(@app,command_finder,arguments).format 92 | out.puts help_output unless help_output.nil? 93 | elsif arguments.empty? || options[:c] 94 | out.puts HelpModules::GlobalHelpFormat.new(@app,@sorter,@text_wrapping_class).format 95 | else 96 | name = arguments.shift 97 | command = command_finder.find_command(name) 98 | unless command.nil? 99 | out.puts HelpModules::CommandHelpFormat.new( 100 | command, 101 | @app, 102 | @sorter, 103 | @synopsis_formatter_class, 104 | @text_wrapping_class).format 105 | end 106 | end 107 | end 108 | 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/gli/command_support.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | # Things unrelated to the true public interface of Command that are needed for bookkeeping 3 | # and help support. Generally, you shouldn't be calling these methods; they are technically public 4 | # but are essentially part of GLI's internal implementation and subject to change 5 | module CommandSupport 6 | # The parent of this command, either the GLI app, or another command 7 | attr_accessor :parent 8 | 9 | def context_description 10 | "in the command #{name}" 11 | end 12 | 13 | # Return true to avoid including this command in your help strings 14 | # Will honor the hide_commands_without_desc flag 15 | def nodoc 16 | @hide_commands_without_desc and description.nil? 17 | end 18 | 19 | # Return the arguments description 20 | def arguments_description 21 | @arguments_description 22 | end 23 | 24 | def arguments_options 25 | @arguments_options 26 | end 27 | 28 | def arguments 29 | @arguments 30 | end 31 | 32 | # If true, this command doesn't want the pre block run before it executes 33 | def skips_pre 34 | @skips_pre 35 | end 36 | 37 | # If true, this command doesn't want the post block run before it executes 38 | def skips_post 39 | @skips_post 40 | end 41 | 42 | # If true, this command doesn't want the around block called 43 | def skips_around 44 | @skips_around 45 | end 46 | 47 | # Return the Array of the command's names 48 | def names 49 | all_forms 50 | end 51 | 52 | # Returns the array of examples 53 | def examples 54 | @examples 55 | end 56 | 57 | # Get an array of commands, ordered by when they were declared 58 | def commands_declaration_order # :nodoc: 59 | @commands_declaration_order 60 | end 61 | 62 | def flag(*names) 63 | if send_declarations_to_parent? 64 | new_flag = if parent.kind_of? Command 65 | super(*names) 66 | parent.flag(*names) 67 | else 68 | super(*names) 69 | end 70 | new_flag.associated_command = self 71 | new_flag 72 | else 73 | super(*names) 74 | end 75 | end 76 | 77 | def switch(*names) 78 | if send_declarations_to_parent? 79 | new_switch = if parent.kind_of? Command 80 | super(*names) 81 | parent.switch(*names) 82 | else 83 | super(*names) 84 | end 85 | new_switch.associated_command = self 86 | new_switch 87 | else 88 | super(*names) 89 | end 90 | end 91 | 92 | def desc(d) 93 | parent.desc(d) if parent.kind_of?(Command) && send_declarations_to_parent? 94 | super(d) 95 | end 96 | 97 | def long_desc(d) 98 | parent.long_desc(d) if parent.kind_of?(Command) && send_declarations_to_parent? 99 | super(d) 100 | end 101 | 102 | def arg_name(d,options=[]) 103 | parent.arg_name(d,options) if parent.kind_of?(Command) && send_declarations_to_parent? 104 | super(d,options) 105 | end 106 | 107 | def default_value(d) 108 | parent.default_value(d) if parent.kind_of?(Command) && send_declarations_to_parent? 109 | super(d) 110 | end 111 | 112 | # Return the flags as a Hash 113 | def flags 114 | @flags ||= {} 115 | end 116 | # Return the switches as a Hash 117 | def switches 118 | @switches ||= {} 119 | end 120 | 121 | def commands # :nodoc: 122 | @commands ||= {} 123 | end 124 | 125 | def default_description 126 | @default_desc 127 | end 128 | 129 | # Executes the command 130 | def execute(global_options,options,arguments) 131 | get_action(arguments).call(global_options,options,arguments) 132 | end 133 | 134 | def topmost_ancestor 135 | some_command = self 136 | top = some_command 137 | while some_command.kind_of? self.class 138 | top = some_command 139 | some_command = some_command.parent 140 | end 141 | top 142 | end 143 | 144 | def has_action? 145 | !!@action 146 | end 147 | 148 | def get_default_command 149 | @default_command 150 | end 151 | 152 | private 153 | 154 | def send_declarations_to_parent? 155 | app = topmost_ancestor.parent 156 | app.nil? ? true : (app.subcommand_option_handling_strategy == :legacy) 157 | end 158 | 159 | def get_action(arguments) 160 | if @action 161 | @action 162 | else 163 | generate_error_action(arguments) 164 | end 165 | end 166 | 167 | def generate_error_action(arguments) 168 | lambda { |global_options,options,arguments| 169 | if am_subcommand? && arguments.size > 0 170 | raise UnknownCommand,"Unknown command '#{arguments[0]}'" 171 | elsif have_subcommands? 172 | raise BadCommandLine,"Command '#{name}' requires a subcommand #{self.commands.keys.join(',')}" 173 | else 174 | raise "Command '#{name}' has no action block" 175 | end 176 | } 177 | end 178 | 179 | def am_subcommand? 180 | parent.kind_of?(Command) 181 | end 182 | 183 | def have_subcommands? 184 | !self.commands.empty? 185 | end 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = GLI, the Git-Like Interface Command Line Parser 2 | 3 | GLI allows you to create command-line app in Ruby that behaves like git in that it takes subcommands to perform a series of complex action, e.g. git remote add. 4 | 5 | * {Overview}[http://davetron5000.github.io/gli] 6 | * {Source on Github}[http://github.com/davetron5000/gli] 7 | * RDoc[http://davetron5000.github.io/gli/rdoc/index.html] 8 | 9 | {Build Status}[https://travis-ci.org/davetron5000/gli] 10 | 11 | == What Problem does GLI Solve? 12 | 13 | Creating a command-line app that uses subcommands, each of which might accept different command-line options, is somewhat difficult with Ruby's built-in OptionParser. GLI provides an API that wraps OptionParser so that you can create a subcommand-based command-line app with minimal boilerplate. This API also produces complete documentation for your command-line app. 14 | 15 | == Why is GLI's solution different from others? 16 | 17 | There are other RubyGems that allow you to create a command-line app that takes subcommands. These solutions are often quite limited (e.g. they don't allow deeply nested subcommand structures or sophisticated command-line options per subcommand), or require more code that we think is needed. Some solutions make it difficult or impossible to properly document your command-line app. 18 | 19 | == What you need to know to use GLI 20 | 21 | You should know Ruby, and have a basic understanding of how the UNIX command line works: standard input, standard output, standard error, and exit codes. 22 | 23 | == Use 24 | 25 | Install if you need to: 26 | 27 | gem install gli 28 | 29 | You can validate you have installed it correctly by running gli help. You should see formatted help output. 30 | 31 | If you are using GLI in another application, add it to your Gemfile: 32 | 33 | gem "gli" 34 | 35 | You can test your install via Bundler by running bundle exec gli help. This should produce formatted help output from GLI. 36 | 37 | == Getting Started 38 | 39 | The simplest way to get started is to create a scaffold project 40 | 41 | gli init todo list add complete 42 | 43 | (note if you installed via Bundler you will need to execute bundle exec gli init todo list add complete) 44 | 45 | This will create a basic scaffold project in ./todo with: 46 | 47 | * executable in ./todo/bin/todo. This file demonstrates most of what you need to describe your command line interface. 48 | * an empty test in ./todo/test/default_test.rb that can bootstrap your tests 49 | * a gemspec shell 50 | * a README shell 51 | * Rakefile that can generate RDoc, package your Gem and run tests 52 | * A Gemfile suitable for use with Bundler to manage development-time dependencies 53 | 54 | Now, you are ready to go: 55 | 56 | > cd todo 57 | > bundle exec bin/todo help 58 | NAME 59 | todo - Describe your application here 60 | 61 | SYNOPSIS 62 | todo [global options] command [command options] [arguments...] 63 | 64 | VERSION 65 | 0.0.1 66 | 67 | GLOBAL OPTIONS 68 | -f, --flagname=The name of the argument - Describe some flag here (default: the default) 69 | --help - Show this message 70 | -s, --[no-]switch - Describe some switch here 71 | 72 | COMMANDS 73 | add - Describe add here 74 | complete - Describe complete here 75 | help - Shows a list of commands or help for one command 76 | list - Describe list here 77 | 78 | > bundle exec bin/todo help list 79 | NAME 80 | list - Describe list here 81 | 82 | SYNOPSIS 83 | todo [global options] list [command options] Describe arguments to list here 84 | 85 | COMMAND OPTIONS 86 | -f arg - Describe a flag to list (default: default) 87 | -s - Describe a switch to list 88 | 89 | All you need to do is fill in the documentation and your code; the help system, command-line parsing and many other awesome features are all handled for you. 90 | 91 | Get a more detailed walkthrough on the {main site}[http://davetron5000.github.io/gli] 92 | 93 | == Supported Platforms 94 | 95 | See `.circleci/config.yml` for the supported rubies, but general we're running tests on the all MRI rubies receiving support, which tends to be the most recent four versions. 96 | 97 | GLI should work on older Rubies and JRuby, but it's too much work to keep tests passing for those. 98 | 99 | == Documentation 100 | 101 | Extensive documentation is {available at the wiki}[https://github.com/davetron5000/gli/wiki]. 102 | 103 | API Documentation is available {here}[http://davetron5000.github.io/gli/rdoc/index.html]. Recommend starting with GLI::DSL or GLI::App. 104 | 105 | == Credits 106 | 107 | Author:: Dave Copeland (mailto:davetron5000 at g mail dot com) 108 | Copyright:: Copyright (c) 2010 by Dave Copeland 109 | License:: Distributes under the Apache License, see LICENSE.txt in the source distro 110 | 111 | == Links 112 | 113 | * [http://davetron5000.github.io/gli] - RubyDoc 114 | * [http://www.github.com/davetron5000/gli] - Source on GitHub 115 | * [http://www.github.com/davetron5000/gli/wiki] - Documentation Wiki 116 | * [http://www.github.com/davetron5000/gli/wiki/Changelog] - Changelog 117 | 118 | = gli CLI documentation 119 | 120 | :include:gli.rdoc 121 | -------------------------------------------------------------------------------- /lib/gli/command.rb: -------------------------------------------------------------------------------- 1 | require 'gli/command_line_token.rb' 2 | require 'gli/dsl.rb' 3 | 4 | module GLI 5 | # A command to be run, in context of global flags and switches. You are given an instance of this class 6 | # to the block you use for GLI::DSL#command. This class mixes in GLI::DSL so all of those methods are available 7 | # to describe the command, in addition to the methods documented here, most importantly 8 | # #action. 9 | # 10 | # Example: 11 | # 12 | # command :list do |c| # <- c is an instance of GLI::Command 13 | # c.desc 'use long form' 14 | # c.switch :l 15 | # 16 | # c.action do |global,options,args| 17 | # # list things here 18 | # end 19 | # 20 | # c.command :tasks do |t| # <- t is an instance of GLI::Command 21 | # # this is a "subcommand" of list 22 | # 23 | # t.action do |global,options,args| 24 | # # do whatever list tasks should do 25 | # end 26 | # end 27 | # end 28 | # 29 | class Command < CommandLineToken 30 | include DSL 31 | include CommandSupport 32 | 33 | class ParentKey 34 | def to_sym 35 | "__parent__".to_sym 36 | end 37 | end 38 | 39 | # Key in an options hash to find the parent's parsed options. Note that if you are 40 | # using openstruct, e.g. via `use_openstruct true` in your app setup, you will need 41 | # to use the method `__parent__` to access parent parsed options. 42 | PARENT = ParentKey.new 43 | 44 | # Create a new command. 45 | # 46 | # options:: Keys should be: 47 | # +names+:: A String, Symbol, or Array of String or Symbol that represents the name(s) of this command (required). 48 | # +description+:: short description of this command as a String 49 | # +arguments_name+:: description of the arguments as a String, or nil if this command doesn't take arguments 50 | # +long_desc+:: a longer description of the command, possibly with multiple lines. A double line-break is treated 51 | # as a paragraph break. No other formatting is respected, though inner whitespace is maintained. 52 | # +skips_pre+:: if true, this command advertises that it doesn't want the pre block called first 53 | # +skips_post+:: if true, this command advertises that it doesn't want the post block called after it 54 | # +skips_around+:: if true, this command advertises that it doesn't want the around block called 55 | # +hide_commands_without_desc+:: if true and there isn't a description the command is not going to be shown in the help 56 | # +examples+:: An array of Hashes, where each hash must have the key +:example+ mapping to a string, and may optionally have the key +:desc+ 57 | # that documents that example. 58 | def initialize(options) 59 | super(options[:names],options[:description],options[:long_desc]) 60 | @arguments_description = options[:arguments_name] || '' 61 | @arguments_options = Array(options[:arguments_options]).flatten 62 | @arguments = options[:arguments] || [] 63 | @skips_pre = options[:skips_pre] 64 | @skips_post = options[:skips_post] 65 | @skips_around = options[:skips_around] 66 | @hide_commands_without_desc = options[:hide_commands_without_desc] 67 | @commands_declaration_order = [] 68 | @flags_declaration_order = [] 69 | @switches_declaration_order = [] 70 | @examples = options[:examples] || [] 71 | clear_nexts 72 | end 73 | 74 | # Specify an example invocation. 75 | # 76 | # example_invocation:: test of a complete command-line invocation you want to show 77 | # options:: refine the example: 78 | # +:desc+:: A description of the example to be shown with it (optional) 79 | def example(example_invocation,options = {}) 80 | @examples << { 81 | example: example_invocation 82 | }.merge(options) 83 | end 84 | 85 | # Set the default command if this command has subcommands and the user doesn't 86 | # provide a subcommand when invoking THIS command. When nil, this will show an error and the help 87 | # for this command; when set, the command with this name will be executed. 88 | # 89 | # +command_name+:: The primary name of the subcommand of this command that should be run by default as a String or Symbol. 90 | def default_command(command_name) 91 | @default_command = command_name 92 | end 93 | 94 | # Define the action to take when the user executes this command. Every command should either define this 95 | # action block, or have subcommands (or both). 96 | # 97 | # +block+:: A block of code to execute. The block will be given 3 arguments: 98 | # +global_options+:: A Hash of the _global_ options specified 99 | # by the user, with defaults set and config file values used (if using a config file, see 100 | # GLI::App#config_file) 101 | # +options+:: A Hash of the command-specific options specified by the 102 | # user, with defaults set and config file values used (if using a config file, see 103 | # GLI::App#config_file). 104 | # +arguments+:: An Array of Strings representing the unparsed command line arguments 105 | # The block's result value is not used; raise an exception or use GLI#exit_now! if you need an early exit based 106 | # on an error condition 107 | # 108 | def action(&block) 109 | @action = block 110 | end 111 | 112 | # Describes this commands action block when it *also* has subcommands. 113 | # In this case, the GLI::DSL#desc value is the general description of the commands 114 | # that this command groups, and the value for *this* method documents what 115 | # will happen if you omit a subcommand. 116 | # 117 | # Note that if you omit the action block and specify a subcommand, that subcommand's 118 | # description will be used to describe what happens by default. 119 | # 120 | # desc:: the description of what this command's action block does. 121 | # 122 | # Example 123 | # 124 | # desc 'list things' 125 | # command :list do |c| 126 | # 127 | # c.desc 'list tasks' 128 | # c.command :tasks do |t| 129 | # t.action do |global,options,args| 130 | # end 131 | # end 132 | # 133 | # c.desc 'list contexts' 134 | # c.command :contexts do |t| 135 | # t.action do |global,options,args| 136 | # end 137 | # end 138 | # 139 | # c.default_desc 'list both tasks and contexts' 140 | # c.action do |global,options,args| 141 | # # list everything 142 | # end 143 | # end 144 | # 145 | # 146 | # > todo help list 147 | # NAME 148 | # list - List things 149 | # 150 | # SYNOPSIS 151 | # todo [global options] list [command options] 152 | # todo [global options] list [command options] tasks 153 | # todo [global options] list [command options] contexts 154 | # 155 | # COMMANDS 156 | # - list both tasks and contexts 157 | # tasks - list tasks 158 | # contexts - list contexts 159 | # 160 | def default_desc(desc) 161 | @default_desc = desc 162 | end 163 | 164 | # Returns true if this command has the given option defined 165 | def has_option?(option) #:nodoc: 166 | option = option.gsub(/^\-+/,'') 167 | ((flags.values.map { |_| [_.name,_.aliases] }) + 168 | (switches.values.map { |_| [_.name,_.aliases] })).flatten.map(&:to_s).include?(option) 169 | end 170 | 171 | # Returns full name for help command including parents 172 | # 173 | # Example 174 | # 175 | # command :remote do |t| 176 | # t.command :add do |global,options,args| 177 | # end 178 | # end 179 | # 180 | # @add_command.name_for_help # => ["remote", "add"] 181 | # 182 | def name_for_help 183 | name_array = [name.to_s] 184 | command_parent = parent 185 | while(command_parent.is_a?(GLI::Command)) do 186 | name_array.unshift(command_parent.name.to_s) 187 | command_parent = command_parent.parent 188 | end 189 | name_array 190 | end 191 | 192 | def self.name_as_string(name,negatable=false) #:nodoc: 193 | name.to_s 194 | end 195 | end 196 | end 197 | -------------------------------------------------------------------------------- /test/unit/subcommands_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | require_relative "support/fake_std_out" 3 | 4 | class SubcommandsTest < MiniTest::Test 5 | include TestHelper 6 | 7 | def setup 8 | @fake_stdout = FakeStdOut.new 9 | @fake_stderr = FakeStdOut.new 10 | 11 | @original_stdout = $stdout 12 | $stdout = @fake_stdout 13 | @original_stderr = $stderr 14 | $stderr = @fake_stderr 15 | 16 | @app = CLIApp.new 17 | @app.reset 18 | @app.subcommand_option_handling :legacy 19 | @app.error_device=@fake_stderr 20 | ENV.delete('GLI_DEBUG') 21 | end 22 | 23 | def teardown 24 | $stdout = @original_stdout 25 | $stderr = @original_stderr 26 | end 27 | 28 | def test_we_run_add_command_using_add 29 | we_have_a_command_with_two_subcommands 30 | run_app('remote',"add",'-f','foo','bar') 31 | assert_command_ran_with(:add, :command_options => {:f => true}, :args => %w(foo bar)) 32 | end 33 | def test_we_run_add_command_using_new 34 | we_have_a_command_with_two_subcommands 35 | run_app('remote',"new",'-f','foo','bar') 36 | assert_command_ran_with(:add, :command_options => {:f => true}, :args => %w(foo bar)) 37 | end 38 | 39 | def test_subcommands_not_used_on_command_line_uses_base_action 40 | we_have_a_command_with_two_subcommands 41 | run_app('remote','foo','bar') 42 | assert_command_ran_with(:base, :command_options => {:f => false}, :args => %w(foo bar)) 43 | end 44 | 45 | def test_switches_and_flags_on_subcommand_available 46 | we_have_a_command_with_two_subcommands(:switches => [:addswitch], :flags => [:addflag]) 47 | run_app('remote','add','--addswitch','--addflag','foo','bar') 48 | assert_command_ran_with(:add,:command_options => { :addswitch => true, :addflag => 'foo', :f => false }, 49 | :args => ['bar']) 50 | end 51 | 52 | def test_help_flag_works_in_normal_mode 53 | @app.subcommand_option_handling :normal 54 | we_have_a_command_with_two_subcommands 55 | @app.run(["remote", "add", "--help"]) rescue nil 56 | refute_match /^error/, @fake_stderr.to_s, "should not output an error message" 57 | end 58 | 59 | def test_help_flag_works_in_legacy_mode 60 | @app.subcommand_option_handling :legacy 61 | we_have_a_command_with_two_subcommands 62 | @app.run(["remote", "add", "--help"]) rescue nil 63 | refute_match /^error/, @fake_stderr.to_s, "should not output an error message" 64 | end 65 | 66 | def test_we_can_reopen_commands_to_add_new_subcommands 67 | @app.command :remote do |p| 68 | p.command :add do |c| 69 | c.action do |global_options,command_options,args| 70 | @ran_command = :add 71 | end 72 | end 73 | end 74 | @app.command :remote do |p| 75 | p.command :new do |c| 76 | c.action do |global_options,command_options,args| 77 | @ran_command = :new 78 | end 79 | end 80 | end 81 | run_app('remote','new') 82 | assert_equal(@ran_command, :new) 83 | run_app('remote', 'add') 84 | assert_equal(@ran_command, :add) 85 | end 86 | 87 | def test_reopening_commands_doesnt_readd_to_output 88 | @app.command :remote do |p| 89 | p.command(:add) { } 90 | end 91 | @app.command :remote do |p| 92 | p.command(:new) { } 93 | end 94 | command_names = @app.instance_variable_get("@commands_declaration_order").collect { |c| c.name } 95 | assert_equal 1, command_names.grep(:remote).size 96 | end 97 | 98 | 99 | def test_we_can_reopen_commands_without_causing_conflicts 100 | @app.command :remote do |p| 101 | p.command :add do |c| 102 | c.action do |global_options,command_options,args| 103 | @ran_command = :remote_add 104 | end 105 | end 106 | end 107 | @app.command :local do |p| 108 | p.command :add do |c| 109 | c.action do |global_options,command_options,args| 110 | @ran_command = :local_add 111 | end 112 | end 113 | end 114 | run_app('remote','add') 115 | assert_equal(@ran_command, :remote_add) 116 | run_app('local', 'add') 117 | assert_equal(@ran_command, :local_add) 118 | end 119 | 120 | 121 | def test_we_can_nest_subcommands_very_deep 122 | @run_results = { :add => nil, :rename => nil, :base => nil } 123 | @app.command :remote do |c| 124 | 125 | c.switch :f 126 | c.command :add do |add| 127 | add.command :some do |some| 128 | some.command :cmd do |cmd| 129 | cmd.switch :s 130 | cmd.action do |global_options,command_options,args| 131 | @run_results[:cmd] = [global_options,command_options,args] 132 | end 133 | end 134 | end 135 | end 136 | end 137 | ENV['GLI_DEBUG'] = 'true' 138 | run_app('remote','add','some','cmd','-s','blah') 139 | assert_command_ran_with(:cmd, :command_options => {:s => true, :f => false},:args => ['blah']) 140 | end 141 | 142 | def test_when_any_command_has_no_action_but_there_are_args_indicate_unknown_command 143 | a_very_deeply_nested_command_structure 144 | assert_raises GLI::UnknownCommand do 145 | When run_app('remote','add','some','foo') 146 | end 147 | assert_match /Unknown command 'foo'/,@fake_stderr.to_s 148 | end 149 | 150 | def test_when_any_command_has_no_action_but_there_are_no_args_indicate_subcommand_needed 151 | a_very_deeply_nested_command_structure 152 | assert_raises GLI::BadCommandLine do 153 | When run_app('remote','add','some') 154 | end 155 | assert_match /Command 'some' requires a subcommand/,@fake_stderr.to_s 156 | end 157 | 158 | private 159 | 160 | def run_app(*args) 161 | @exit_code = @app.run(args) 162 | end 163 | 164 | def a_very_deeply_nested_command_structure 165 | @run_results = { :add => nil, :rename => nil, :base => nil } 166 | @app.command :remote do |c| 167 | 168 | c.switch :f 169 | c.command :add do |add| 170 | add.command :some do |some| 171 | some.command :cmd do |cmd| 172 | cmd.switch :s 173 | cmd.action do |global_options,command_options,args| 174 | @run_results[:cmd] = [global_options,command_options,args] 175 | end 176 | end 177 | end 178 | end 179 | end 180 | ENV['GLI_DEBUG'] = 'true' 181 | end 182 | 183 | # expected_command - name of command exepcted to have been run 184 | # options: 185 | # - global_options => hash of expected options 186 | # - command_options => hash of expected command options 187 | # - args => array of expected args 188 | def assert_command_ran_with(expected_command,options) 189 | global_options = options[:global_options] || { :help => false } 190 | @run_results.each do |command,results| 191 | if command == expected_command 192 | assert_equal(indifferent_hash(global_options),results[0]) 193 | assert_equal(indifferent_hash(options[:command_options]),results[1]) 194 | assert_equal(options[:args],results[2]) 195 | else 196 | assert_nil results 197 | end 198 | end 199 | end 200 | 201 | def indifferent_hash(possibly_nil_hash) 202 | return {} if possibly_nil_hash.nil? 203 | possibly_nil_hash.keys.each do |key| 204 | if key.kind_of? Symbol 205 | possibly_nil_hash[key.to_s] = possibly_nil_hash[key] unless possibly_nil_hash.has_key?(key.to_s) 206 | elsif key.kind_of? String 207 | possibly_nil_hash[key.to_sym] = possibly_nil_hash[key] unless possibly_nil_hash.has_key?(key.to_sym) 208 | end 209 | end 210 | possibly_nil_hash 211 | end 212 | 213 | # options - 214 | # :flags => flags to add to :add 215 | # :switiches => switiches to add to :add 216 | def we_have_a_command_with_two_subcommands(options = {}) 217 | @run_results = { :add => nil, :rename => nil, :base => nil } 218 | @app.command :remote do |c| 219 | 220 | c.switch :f 221 | 222 | c.desc "add a remote" 223 | c.command [:add,:new] do |add| 224 | 225 | Array(options[:flags]).each { |_| add.flag _ } 226 | Array(options[:switches]).each { |_| add.switch _ } 227 | add.action do |global_options,command_options,args| 228 | @run_results[:add] = [global_options,command_options,args] 229 | end 230 | end 231 | 232 | c.desc "rename a remote" 233 | c.command :rename do |rename| 234 | rename.action do |global_options,command_options,args| 235 | @run_results[:rename] = [global_options,command_options,args] 236 | end 237 | end 238 | 239 | c.action do |global_options,command_options,args| 240 | @run_results[:base] = [global_options,command_options,args] 241 | end 242 | end 243 | ENV['GLI_DEBUG'] = 'true' 244 | end 245 | end 246 | -------------------------------------------------------------------------------- /lib/gli/commands/doc.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | module Commands 3 | # Takes a DocListener which will be called with all of the meta-data and documentation 4 | # about your app, so as to create documentation in whatever format you want 5 | class Doc < Command 6 | FORMATS = { 7 | 'rdoc' => GLI::Commands::RdocDocumentListener 8 | } 9 | # Create the Doc generator based on the GLI app passed in 10 | def initialize(app) 11 | super(:names => "_doc", 12 | :description => "Generate documentation of your application's UI", 13 | :long_desc => "Introspects your application's UI meta-data to generate documentation in a variety of formats. This is intended to be extensible via the DocumentListener interface, so that you can provide your own documentation formats without them being a part of GLI", 14 | :skips_pre => true, :skips_post => true, :skips_around => true, :hidden => true) 15 | 16 | @app = app 17 | @parent = @app 18 | @subcommand_option_handling_strategy = @app.subcommand_option_handling_strategy 19 | 20 | desc 'The format name of the documentation to generate or the class name to use to generate it' 21 | default_value 'rdoc' 22 | arg_name 'name_or_class' 23 | flag :format 24 | 25 | action do |global_options,options,arguments| 26 | self.document(format_class(options[:format]).new(global_options,options,arguments,app)) 27 | end 28 | end 29 | 30 | def nodoc 31 | true 32 | end 33 | 34 | # Generates documentation using the listener 35 | def document(document_listener) 36 | document_listener.beginning 37 | document_listener.program_desc(@app.program_desc) unless @app.program_desc.nil? 38 | document_listener.program_long_desc(@app.program_long_desc) unless @app.program_long_desc.nil? 39 | document_listener.version(@app.version_string) 40 | if any_options?(@app) 41 | document_listener.options 42 | end 43 | document_flags_and_switches(document_listener, 44 | @app.flags.values.sort(&by_name), 45 | @app.switches.values.sort(&by_name)) 46 | if any_options?(@app) 47 | document_listener.end_options 48 | end 49 | document_listener.commands 50 | document_commands(document_listener,@app) 51 | document_listener.end_commands 52 | document_listener.ending 53 | end 54 | 55 | # Interface for a listener that is called during various parts of the doc process 56 | class DocumentListener 57 | def initialize(global_options,options,arguments,app) 58 | @global_options = global_options 59 | @options = options 60 | @arguments = arguments 61 | @app = app 62 | end 63 | # Called before processing begins 64 | def beginning 65 | abstract! 66 | end 67 | 68 | # Called when processing has completed 69 | def ending 70 | abstract! 71 | end 72 | 73 | # Gives you the program description 74 | def program_desc(desc) 75 | abstract! 76 | end 77 | 78 | # Gives you the program long description 79 | def program_long_desc(desc) 80 | abstract! 81 | end 82 | 83 | # Gives you the program version 84 | def version(version) 85 | abstract! 86 | end 87 | 88 | # Called at the start of options for the current context 89 | def options 90 | abstract! 91 | end 92 | 93 | # Called when all options for the current context have been vended 94 | def end_options 95 | abstract! 96 | end 97 | 98 | # Called at the start of commands for the current context 99 | def commands 100 | abstract! 101 | end 102 | 103 | # Called when all commands for the current context have been vended 104 | def end_commands 105 | abstract! 106 | end 107 | 108 | # Gives you a flag in the current context 109 | def flag(name,aliases,desc,long_desc,default_value,arg_name,must_match,type) 110 | abstract! 111 | end 112 | 113 | # Gives you a switch in the current context 114 | def switch(name,aliases,desc,long_desc,negatable) 115 | abstract! 116 | end 117 | 118 | # Gives you the name of the current command in the current context 119 | def default_command(name) 120 | abstract! 121 | end 122 | 123 | # Gives you a command in the current context and creates a new context of this command 124 | def command(name,aliases,desc,long_desc,arg_name,arg_options) 125 | abstract! 126 | end 127 | 128 | # Ends a command, and "pops" you back up one context 129 | def end_command(name) 130 | abstract! 131 | end 132 | 133 | private 134 | def abstract! 135 | raise "Subclass must implement" 136 | end 137 | end 138 | 139 | private 140 | 141 | def format_class(format_name) 142 | FORMATS.fetch(format_name) { 143 | begin 144 | return format_name.split(/::/).reduce(Kernel) { |context,part| context.const_get(part) } 145 | rescue => ex 146 | raise IndexError,"Couldn't find formatter or class named #{format_name}" 147 | end 148 | } 149 | end 150 | 151 | def document_commands(document_listener,context) 152 | context.commands.values.reject {|_| _.nodoc }.sort(&by_name).each do |command| 153 | call_command_method_being_backwards_compatible(document_listener,command) 154 | document_listener.options if any_options?(command) 155 | document_flags_and_switches(document_listener,command_flags(command),command_switches(command)) 156 | document_listener.end_options if any_options?(command) 157 | document_listener.commands if any_commands?(command) 158 | document_commands(document_listener,command) 159 | document_listener.end_commands if any_commands?(command) 160 | document_listener.end_command(command.name) 161 | end 162 | document_listener.default_command(context.get_default_command) 163 | end 164 | 165 | def call_command_method_being_backwards_compatible(document_listener,command) 166 | command_args = [command.name, 167 | Array(command.aliases), 168 | command.description, 169 | command.long_description, 170 | command.arguments_description] 171 | if document_listener.method(:command).arity >= 6 172 | command_args << command.arguments_options 173 | if document_listener.method(:command).arity >= 7 174 | command_args << command.arguments 175 | end 176 | if document_listener.method(:command).arity >= 8 177 | command_args << command.examples 178 | end 179 | end 180 | document_listener.command(*command_args) 181 | end 182 | 183 | def by_name 184 | lambda { |a,b| a.name.to_s <=> b.name.to_s } 185 | end 186 | 187 | def command_flags(command) 188 | if @subcommand_option_handling_strategy == :legacy 189 | command.topmost_ancestor.flags.values.select { |flag| flag.associated_command == command }.sort(&by_name) 190 | else 191 | command.flags.values.sort(&by_name) 192 | end 193 | end 194 | 195 | def command_switches(command) 196 | if @subcommand_option_handling_strategy == :legacy 197 | command.topmost_ancestor.switches.values.select { |switch| switch.associated_command == command }.sort(&by_name) 198 | else 199 | command.switches.values.sort(&by_name) 200 | end 201 | end 202 | 203 | def document_flags_and_switches(document_listener,flags,switches) 204 | flags.each do |flag| 205 | document_listener.flag(flag.name, 206 | Array(flag.aliases), 207 | flag.description, 208 | flag.long_description, 209 | flag.safe_default_value, 210 | flag.argument_name, 211 | flag.must_match, 212 | flag.type) 213 | end 214 | switches.each do |switch| 215 | document_listener.switch(switch.name, 216 | Array(switch.aliases), 217 | switch.description, 218 | switch.long_description, 219 | switch.negatable) 220 | end 221 | end 222 | 223 | def any_options?(context) 224 | options = if context.kind_of?(Command) 225 | command_flags(context) + command_switches(context) 226 | else 227 | context.flags.values + context.switches.values 228 | end 229 | !options.empty? 230 | end 231 | 232 | def any_commands?(command) 233 | !command.commands.empty? 234 | end 235 | end 236 | end 237 | end 238 | -------------------------------------------------------------------------------- /test/unit/doc_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class String 4 | def blank? 5 | self.strip.length == 0 6 | end 7 | end 8 | 9 | class NilClass 10 | def blank? 11 | true 12 | end 13 | end 14 | 15 | class Object 16 | def blank? 17 | false 18 | end 19 | end 20 | 21 | class DocTest < MiniTest::Test 22 | include TestHelper 23 | 24 | class TestApp 25 | include GLI::App 26 | end 27 | 28 | class TestListener 29 | @@last = nil 30 | def self.last 31 | @@last 32 | end 33 | def initialize(*ignored) 34 | @stringio = StringIO.new 35 | @indent = '' 36 | @@last = self 37 | end 38 | def options 39 | end 40 | def end_options 41 | end 42 | def commands 43 | end 44 | def end_commands 45 | end 46 | def beginning 47 | @stringio << 'BEGIN' << "\n" 48 | end 49 | 50 | def ending 51 | @stringio << 'END' << "\n" 52 | end 53 | 54 | def program_desc(desc) 55 | @stringio << desc << "\n" 56 | end 57 | 58 | def program_long_desc(desc) 59 | @stringio << desc << "\n" 60 | end 61 | 62 | def version(version) 63 | @stringio << version << "\n" 64 | end 65 | 66 | def default_command(name) 67 | @stringio << @indent << "default_command: " << name << "\n" 68 | end 69 | 70 | def flag(name,aliases,desc,long_desc,default_value,arg_name,must_match,type) 71 | @stringio << @indent << "flag: " << name << "\n" 72 | @indent += ' ' 73 | @stringio << @indent << "aliases: " << aliases.join(',') << "\n" unless aliases.empty? 74 | @stringio << @indent << "desc: " << desc << "\n" unless desc.blank? 75 | @stringio << @indent << "long_desc: " << long_desc << "\n" unless long_desc.blank? 76 | @stringio << @indent << "default_value: " << default_value << "\n" unless default_value.blank? 77 | @stringio << @indent << "arg_name: " << arg_name << "\n" unless arg_name.blank? 78 | @indent.gsub!(/ $/,'') 79 | end 80 | 81 | def switch(name,aliases,desc,long_desc,negatable) 82 | @stringio << @indent << "switch: " << name << "\n" 83 | @indent += ' ' 84 | @stringio << @indent << "aliases: " << aliases.join(',') << "\n" unless aliases.empty? 85 | @stringio << @indent << "desc: " << desc << "\n" unless desc.blank? 86 | @stringio << @indent << "long_desc: " << long_desc << "\n" unless long_desc.blank? 87 | @stringio << @indent << "negatable: " << negatable << "\n" unless negatable.blank? 88 | @indent.gsub!(/ $/,'') 89 | end 90 | 91 | def command(name,aliases,desc,long_desc,arg_name) 92 | @stringio << @indent << "command: " << name << "\n" 93 | @indent += ' ' 94 | @stringio << @indent << "aliases: " << aliases.join(',') << "\n" unless aliases.empty? 95 | @stringio << @indent << "desc: " << desc << "\n" unless desc.blank? 96 | @stringio << @indent << "long_desc: " << long_desc << "\n" unless long_desc.blank? 97 | @stringio << @indent << "arg_name: " << arg_name << "\n" unless arg_name.blank? 98 | end 99 | 100 | def end_command(name) 101 | @indent.gsub!(/ $/,'') 102 | @stringio << @indent << "end #{name}" << "\n" 103 | end 104 | 105 | def to_s 106 | @stringio.string 107 | end 108 | end 109 | 110 | def setup 111 | @@counter = -1 # we pre-increment so this makes 0 first 112 | end 113 | 114 | def test_app_without_docs_gets_callbacks_for_each_element 115 | setup_test_app 116 | construct_expected_output 117 | @documenter = GLI::Commands::Doc.new(@app) 118 | @listener = TestListener.new 119 | @documenter.document(@listener) 120 | lines_expected = @string.split(/\n/) 121 | lines_got = @listener.to_s.split(/\n/) 122 | lines_expected.zip(lines_got).each_with_index do |(expected,got),index| 123 | assert_equal expected,got,"At index #{index}" 124 | end 125 | end 126 | 127 | def test_doc_command_works_as_GLI_command 128 | setup_test_app 129 | construct_expected_output 130 | @documenter = GLI::Commands::Doc.new(@app) 131 | @listener = TestListener.new 132 | @documenter.execute({},{:format => "DocTest::TestListener"},[]) 133 | lines_expected = @string.split(/\n/) 134 | lines_got = TestListener.last.to_s.split(/\n/) 135 | lines_expected.zip(lines_got).each_with_index do |(expected,got),index| 136 | assert_equal expected,got,"At index #{index}" 137 | end 138 | end 139 | 140 | private 141 | 142 | @@counter = 1 143 | def self.counter 144 | @@counter += 1 145 | @@counter 146 | end 147 | 148 | def setup_test_app 149 | @app = TestApp.new 150 | @app.instance_eval do 151 | program_desc "program desc" 152 | program_long_desc "program long desc" 153 | version "1.3.4" 154 | 155 | DocTest.flag_with_everything_specified(self) 156 | DocTest.flag_with_everything_omitted(self) 157 | DocTest.switch_with_everything_specified(self) 158 | DocTest.switch_with_everything_omitted(self) 159 | 160 | desc "command desc" 161 | long_desc "command long desc" 162 | arg_name "cmd_arg_name" 163 | command [:command1,:com1] do |c| 164 | DocTest.flag_with_everything_specified(c) 165 | DocTest.flag_with_everything_omitted(c) 166 | DocTest.switch_with_everything_specified(c) 167 | DocTest.switch_with_everything_omitted(c) 168 | 169 | c.desc "subcommand desc" 170 | c.long_desc "subcommand long desc" 171 | c.arg_name "subcmd_arg_name" 172 | c.action { |g,o,a| } 173 | c.command [:sub,:subcommand] do |sub| 174 | DocTest.flag_with_everything_specified(sub,:subflag) 175 | DocTest.flag_with_everything_omitted(sub,:subflag2) 176 | DocTest.switch_with_everything_specified(sub,:subswitch) 177 | DocTest.switch_with_everything_omitted(sub,:subswitch2) 178 | sub.action { |g,o,a| } 179 | end 180 | c.command [:default] do |sub| 181 | sub.action { |g,o,a| } 182 | end 183 | c.default_command :default 184 | end 185 | 186 | command [:command2,:com2] do |c| 187 | c.action { |g,o,a| } 188 | c.command [:sub2,:subcommand2] do |sub| 189 | sub.action { |g,o,a| } 190 | end 191 | end 192 | end 193 | end 194 | 195 | def self.flag_with_everything_specified(on,name=[:f,:flag]) 196 | on.flag name,:desc => "flag desc #{counter}", 197 | :long_desc => "flag long_desc #{counter}", 198 | :default_value => "flag default_value #{counter}", 199 | :arg_name => "flag_arg_name_#{counter}", 200 | :must_match => /foo.*bar/, 201 | :type => Array 202 | end 203 | 204 | def self.flag_with_everything_omitted(on,name=[:F,:flag2]) 205 | on.flag name 206 | end 207 | 208 | def self.switch_with_everything_specified(on,name=[:s,:switch]) 209 | on.switch name, :desc => "switch desc #{counter}", 210 | :long_desc => "switch long_desc #{counter}", 211 | :negatable => false 212 | end 213 | 214 | def self.switch_with_everything_omitted(on,name=[:S,:switch2]) 215 | on.switch name 216 | end 217 | def construct_expected_output 218 | # Oh yeah. Creating a string representing the structure of the calls. 219 | @string =< "command", 184 | :global_options => global, 185 | :command_options => options, 186 | :args => args 187 | } 188 | end 189 | 190 | c.command "subcommand" do |subcommand| 191 | subcommand.flag ['f','flag'] 192 | subcommand.flag ['foo'] 193 | subcommand.switch ['s','switch'] 194 | subcommand.action do |global,options,args| 195 | @results = { 196 | :command_name => "subcommand", 197 | :global_options => global, 198 | :command_options => options, 199 | :args => args 200 | } 201 | end 202 | end 203 | end 204 | end 205 | 206 | def setup_app_with_subcommands_storing_results(subcommand_option_handling_strategy = :legacy) 207 | @app.subcommand_option_handling subcommand_option_handling_strategy 208 | @app.flag ['f','flag'] 209 | @app.switch ['s','switch'] 210 | 211 | 2.times do |i| 212 | @app.command "command#{i}" do |c| 213 | c.flag ['f','flag'] 214 | c.switch ['s','switch'] 215 | c.action do |global,options,args| 216 | @results = { 217 | :command_name => "command#{i}", 218 | :global_options => global, 219 | :command_options => options, 220 | :args => args 221 | } 222 | end 223 | 224 | 2.times do |j| 225 | c.command "subcommand#{i}#{j}" do |subcommand| 226 | subcommand.flag ['f','flag'] 227 | subcommand.flag ['foo'] 228 | subcommand.switch ['s','switch'] 229 | subcommand.action do |global,options,args| 230 | @results = { 231 | :command_name => "subcommand#{i}#{j}", 232 | :global_options => global, 233 | :command_options => options, 234 | :args => args 235 | } 236 | end 237 | end 238 | end 239 | end 240 | end 241 | end 242 | 243 | def setup_app_with_arguments(number_required_arguments, number_optional_arguments, has_argument_multiple, arguments_handling_strategy = :loose, subcommand_option_handling_strategy = :normal) 244 | @app.arguments arguments_handling_strategy 245 | @app.subcommand_option_handling subcommand_option_handling_strategy 246 | 247 | number_required_arguments.times { |i| @app.arg("needed#{i}") } 248 | number_optional_arguments.times { |i| @app.arg("optional#{i}", :optional) } 249 | @app.arg :multiple, [:multiple, :optional] if has_argument_multiple 250 | 251 | @app.command :cmd do |c| 252 | c.action do |g,o,a| 253 | @results = { 254 | :number_of_args_give_to_action => a.size 255 | } 256 | end 257 | end 258 | end 259 | 260 | def run_app_with_X_arguments(number_arguments) 261 | @exit_code = @app.run [].tap{|args| args << "cmd"; number_arguments.times {|i| args << "arg#{i}"}} 262 | end 263 | end 264 | -------------------------------------------------------------------------------- /lib/gli/app_support.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | # Internals for make App work 3 | module AppSupport 4 | # Override the device of stderr; exposed only for testing 5 | def error_device=(e) #:nodoc: 6 | @stderr = e 7 | end 8 | 9 | def context_description 10 | "in global context" 11 | end 12 | 13 | # Reset the GLI module internal data structures; mostly useful for testing 14 | def reset # :nodoc: 15 | switches.clear 16 | flags.clear 17 | @commands = nil 18 | @command_missing_block = nil 19 | @commands_declaration_order = [] 20 | @flags_declaration_order = [] 21 | @switches_declaration_order = [] 22 | @version = nil 23 | @config_file = nil 24 | @use_openstruct = false 25 | @prog_desc = nil 26 | @error_block = false 27 | @pre_block = false 28 | @post_block = false 29 | @default_command = :help 30 | @autocomplete = false 31 | @around_block = nil 32 | @subcommand_option_handling_strategy = :legacy 33 | @argument_handling_strategy = :loose 34 | clear_nexts 35 | end 36 | 37 | def exe_name 38 | File.basename($0) 39 | end 40 | 41 | # Get an array of commands, ordered by when they were declared 42 | def commands_declaration_order # :nodoc: 43 | @commands_declaration_order 44 | end 45 | 46 | # Get the version string 47 | def version_string #:nodoc: 48 | @version 49 | end 50 | 51 | # Get the default command for the entire app 52 | def get_default_command 53 | @default_command 54 | end 55 | 56 | # Runs whatever command is needed based on the arguments. 57 | # 58 | # +args+:: the command line ARGV array 59 | # 60 | # Returns a number that would be a reasonable exit code 61 | def run(args) #:nodoc: 62 | args = args.dup if @preserve_argv 63 | the_command = nil 64 | begin 65 | override_defaults_based_on_config(parse_config) 66 | 67 | add_help_switch_if_needed(self) 68 | 69 | gli_option_parser = GLIOptionParser.new(commands, 70 | flags, 71 | switches, 72 | accepts, 73 | :default_command => @default_command, 74 | :autocomplete => autocomplete, 75 | :subcommand_option_handling_strategy => subcommand_option_handling_strategy, 76 | :argument_handling_strategy => argument_handling_strategy, 77 | :command_missing_block => @command_missing_block) 78 | 79 | parsing_result = gli_option_parser.parse_options(args) 80 | parsing_result.convert_to_openstruct! if @use_openstruct 81 | 82 | the_command = parsing_result.command 83 | 84 | if proceed?(parsing_result) 85 | call_command(parsing_result) 86 | 0 87 | else 88 | raise PreconditionFailed, "preconditions failed" 89 | end 90 | rescue Exception => ex 91 | if the_command.nil? && ex.respond_to?(:command_in_context) 92 | the_command = ex.command_in_context 93 | end 94 | handle_exception(ex,the_command) 95 | end 96 | end 97 | 98 | 99 | # Return the name of the config file; mostly useful for generating help docs 100 | def config_file_name #:nodoc: 101 | @config_file 102 | end 103 | def accepts #:nodoc: 104 | @accepts ||= {} 105 | 106 | end 107 | 108 | def parse_config # :nodoc: 109 | config = { 110 | 'commands' => {}, 111 | } 112 | if @config_file && File.exist?(@config_file) 113 | require 'yaml' 114 | config.merge!(File.open(@config_file) { |file| YAML::load(file) }) 115 | end 116 | config 117 | end 118 | 119 | def clear_nexts # :nodoc: 120 | super 121 | @skips_post = false 122 | @skips_pre = false 123 | @skips_around = false 124 | end 125 | 126 | def stderr 127 | @stderr ||= STDERR 128 | end 129 | 130 | def self.included(klass) 131 | @stderr = $stderr 132 | end 133 | 134 | def flags # :nodoc: 135 | @flags ||= {} 136 | end 137 | 138 | def switches # :nodoc: 139 | @switches ||= {} 140 | end 141 | 142 | def commands # :nodoc: 143 | if !@commands 144 | @commands = { :help => GLI::Commands::Help.new(self), :_doc => GLI::Commands::Doc.new(self) } 145 | @commands_declaration_order ||= [] 146 | @commands_declaration_order << @commands[:help] 147 | @commands_declaration_order << @commands[:_doc] 148 | end 149 | @commands 150 | end 151 | 152 | def pre_block 153 | @pre_block ||= Proc.new do 154 | true 155 | end 156 | end 157 | 158 | def post_block 159 | @post_block ||= Proc.new do 160 | end 161 | end 162 | 163 | def around_blocks 164 | @around_blocks || [] 165 | end 166 | 167 | def help_sort_type 168 | @help_sort_type || :alpha 169 | end 170 | 171 | def help_text_wrap_type 172 | @help_text_wrap_type || :to_terminal 173 | end 174 | 175 | def synopsis_format_type 176 | @synopsis_format_type || :full 177 | end 178 | 179 | # Sets the default values for flags based on the configuration 180 | def override_defaults_based_on_config(config) 181 | override_default(flags,config) 182 | override_default(switches,config) 183 | 184 | override_command_defaults(commands,config) 185 | end 186 | 187 | def override_command_defaults(command_list,config) 188 | command_list.each do |command_name,command| 189 | next if command_name == :initconfig || command.nil? 190 | command_config = (config['commands'] || {})[command_name] || {} 191 | 192 | if @subcommand_option_handling_strategy == :legacy 193 | override_default(command.topmost_ancestor.flags,command_config) 194 | override_default(command.topmost_ancestor.switches,command_config) 195 | else 196 | override_default(command.flags,command_config) 197 | override_default(command.switches,command_config) 198 | end 199 | 200 | override_command_defaults(command.commands,command_config) 201 | end 202 | end 203 | 204 | def override_default(tokens,config) 205 | tokens.each do |name,token| 206 | token.default_value=config[name] unless config[name].nil? 207 | end 208 | end 209 | 210 | def argument_handling_strategy 211 | @argument_handling_strategy || :loose 212 | end 213 | 214 | def subcommand_option_handling_strategy 215 | @subcommand_option_handling_strategy || :legacy 216 | end 217 | 218 | def autocomplete 219 | @autocomplete.nil? ? true : @autocomplete 220 | end 221 | 222 | private 223 | 224 | def handle_exception(ex,command) 225 | if regular_error_handling?(ex) 226 | output_error_message(ex) 227 | if ex.kind_of?(OptionParser::ParseError) || ex.kind_of?(BadCommandLine) || ex.kind_of?(RequestHelp) 228 | if commands[:help] 229 | command_for_help = command.nil? ? [] : command.name_for_help 230 | commands[:help].execute({},{},command_for_help) 231 | end 232 | end 233 | elsif ENV['GLI_DEBUG'] == 'true' 234 | stderr.puts "Custom error handler exited false, skipping normal error handling" 235 | end 236 | 237 | raise ex if ENV['GLI_DEBUG'] == 'true' && !ex.kind_of?(RequestHelp) 238 | 239 | ex.extend(GLI::StandardException) 240 | ex.exit_code 241 | end 242 | 243 | def output_error_message(ex) 244 | stderr.puts error_message(ex) unless no_message_given?(ex) 245 | if ex.kind_of?(OptionParser::ParseError) || ex.kind_of?(BadCommandLine) 246 | stderr.puts unless no_message_given?(ex) 247 | end 248 | end 249 | 250 | def no_message_given?(ex) 251 | ex.message == ex.class.name 252 | 253 | end 254 | 255 | def add_help_switch_if_needed(target) 256 | help_switch_exists = target.switches.values.find { |switch| 257 | switch.names_and_aliases.map(&:to_s).find { |an_alias| an_alias == 'help' } 258 | } 259 | unless help_switch_exists 260 | target.desc 'Show this message' 261 | target.switch :help, :negatable => false 262 | end 263 | end 264 | 265 | # True if we should proceed with executing the command; this calls 266 | # the pre block if it's defined 267 | def proceed?(parsing_result) #:nodoc: 268 | if parsing_result.command && parsing_result.command.skips_pre 269 | true 270 | else 271 | pre_block.call(*parsing_result) 272 | end 273 | end 274 | 275 | # Returns true if we should proceed with GLI's basic error handling. 276 | # This calls the error block if the user provided one 277 | def regular_error_handling?(ex) #:nodoc: 278 | if @error_block 279 | return true if (ex.respond_to?(:exit_code) && ex.exit_code == 0) 280 | @error_block.call(ex) 281 | else 282 | true 283 | end 284 | end 285 | 286 | # Returns a String of the error message to show the user 287 | # +ex+:: The exception we caught that launched the error handling routines 288 | def error_message(ex) #:nodoc: 289 | "error: #{ex.message}" 290 | end 291 | 292 | def call_command(parsing_result) 293 | command = parsing_result.command 294 | global_options = parsing_result.global_options 295 | options = parsing_result.command_options 296 | arguments = parsing_result.arguments.map { |arg| arg.dup } # unfreeze 297 | 298 | code = lambda { command.execute(global_options,options,arguments) } 299 | nested_arounds = unless command.skips_around 300 | around_blocks.inject do |outer_around, inner_around| 301 | lambda { |go,c,o,a, code1| 302 | inner = lambda { inner_around.call(go,c,o,a, code1) } 303 | outer_around.call(go,c,o,a, inner) 304 | } 305 | end 306 | end 307 | 308 | if nested_arounds 309 | nested_arounds.call(global_options,command, options, arguments, code) 310 | else 311 | code.call 312 | end 313 | 314 | unless command.skips_post 315 | post_block.call(global_options,command,options,arguments) 316 | end 317 | end 318 | 319 | end 320 | end 321 | -------------------------------------------------------------------------------- /lib/gli/gli_option_parser.rb: -------------------------------------------------------------------------------- 1 | module GLI 2 | # Parses the command-line options using an actual +OptionParser+ 3 | class GLIOptionParser 4 | attr_accessor :options 5 | 6 | DEFAULT_OPTIONS = { 7 | :default_command => nil, 8 | :autocomplete => true, 9 | :subcommand_option_handling_strategy => :legacy, 10 | :argument_handling_strategy => :loose 11 | } 12 | 13 | def initialize(commands,flags,switches,accepts, options={}) 14 | self.options = DEFAULT_OPTIONS.merge(options) 15 | 16 | command_finder = CommandFinder.new(commands, 17 | :default_command => (options[:default_command] || :help), 18 | :autocomplete => options[:autocomplete]) 19 | @global_option_parser = GlobalOptionParser.new(OptionParserFactory.new(flags,switches,accepts),command_finder,flags, :command_missing_block => options[:command_missing_block]) 20 | @accepts = accepts 21 | if options[:argument_handling_strategy] == :strict && options[:subcommand_option_handling_strategy] != :normal 22 | raise ArgumentError, "To use strict argument handling, you must enable normal subcommand_option_handling, e.g. subcommand_option_handling :normal" 23 | end 24 | end 25 | 26 | # Given the command-line argument array, returns an OptionParsingResult 27 | def parse_options(args) # :nodoc: 28 | option_parser_class = self.class.const_get("#{options[:subcommand_option_handling_strategy].to_s.capitalize}CommandOptionParser") 29 | OptionParsingResult.new.tap { |parsing_result| 30 | parsing_result.arguments = args 31 | parsing_result = @global_option_parser.parse!(parsing_result) 32 | option_parser_class.new(@accepts).parse!(parsing_result, options[:argument_handling_strategy], options[:autocomplete]) 33 | } 34 | end 35 | 36 | private 37 | 38 | class GlobalOptionParser 39 | def initialize(option_parser_factory,command_finder,flags,options={}) 40 | @option_parser_factory = option_parser_factory 41 | @command_finder = command_finder 42 | @flags = flags 43 | @options = options 44 | end 45 | 46 | def parse!(parsing_result) 47 | parsing_result.arguments = GLIOptionBlockParser.new(@option_parser_factory,UnknownGlobalArgument).parse!(parsing_result.arguments) 48 | parsing_result.global_options = @option_parser_factory.options_hash_with_defaults_set! 49 | command_name = if parsing_result.global_options[:help] || parsing_result.global_options[:version] 50 | "help" 51 | else 52 | parsing_result.arguments.shift 53 | end 54 | parsing_result.command = begin 55 | @command_finder.find_command(command_name) 56 | rescue UnknownCommand => e 57 | raise e unless @options[:command_missing_block] 58 | command = @options[:command_missing_block].call(command_name.to_sym,parsing_result.global_options) 59 | if command 60 | unless command.is_a?(Command) 61 | raise UnknownCommand.new("Expected the `command_missing` block to return a GLI::Command object, got a #{command.class.name} instead.") 62 | end 63 | else 64 | raise e 65 | end 66 | command 67 | end 68 | 69 | unless command_name == 'help' 70 | verify_required_options!(@flags, parsing_result.command, parsing_result.global_options) 71 | end 72 | parsing_result 73 | end 74 | 75 | protected 76 | def verify_arguments!(arguments, command) 77 | # Lets assume that if the user sets a 'arg_name' for the command it is for a complex scenario 78 | # and we should not validate the arguments 79 | return unless command.arguments_description.empty? 80 | 81 | # Go through all declared arguments for the command, counting the min and max number 82 | # of arguments 83 | min_number_of_arguments = 0 84 | max_number_of_arguments = 0 85 | command.arguments.each do |arg| 86 | if arg.optional? 87 | max_number_of_arguments = max_number_of_arguments + 1 88 | else 89 | min_number_of_arguments = min_number_of_arguments + 1 90 | max_number_of_arguments = max_number_of_arguments + 1 91 | end 92 | 93 | # Special case, as soon as we have a 'multiple' arguments, all bets are off for the 94 | # maximum number of arguments ! 95 | if arg.multiple? 96 | max_number_of_arguments = 99999 97 | end 98 | end 99 | 100 | # Now validate the number of arguments 101 | if arguments.size < min_number_of_arguments 102 | raise MissingRequiredArgumentsException.new("Not enough arguments for command", command) 103 | end 104 | if arguments.size > max_number_of_arguments 105 | raise MissingRequiredArgumentsException.new("Too many arguments for command", command) 106 | end 107 | end 108 | 109 | def verify_required_options!(flags, command, options) 110 | missing_required_options = flags.values. 111 | select(&:required?). 112 | reject { |option| 113 | options[option.name] != nil 114 | } 115 | unless missing_required_options.empty? 116 | missing_required_options.sort! 117 | raise MissingRequiredArgumentsException.new(missing_required_options.map { |option| 118 | "#{option.name} is required" 119 | }.join(', '), command) 120 | end 121 | end 122 | end 123 | 124 | class NormalCommandOptionParser < GlobalOptionParser 125 | def initialize(accepts) 126 | @accepts = accepts 127 | end 128 | 129 | def error_handler 130 | lambda { |message,extra_error_context| 131 | raise UnknownCommandArgument.new(message,extra_error_context) 132 | } 133 | end 134 | 135 | def parse!(parsing_result,argument_handling_strategy,autocomplete) 136 | parsed_command_options = {} 137 | command = parsing_result.command 138 | arguments = nil 139 | 140 | loop do 141 | option_parser_factory = OptionParserFactory.for_command(command,@accepts) 142 | option_block_parser = CommandOptionBlockParser.new(option_parser_factory, self.error_handler) 143 | option_block_parser.command = command 144 | arguments = parsing_result.arguments 145 | 146 | arguments = option_block_parser.parse!(arguments) 147 | 148 | parsed_command_options[command] = option_parser_factory.options_hash_with_defaults_set! 149 | command_finder = CommandFinder.new(command.commands, :default_command => command.get_default_command, :autocomplete => autocomplete) 150 | next_command_name = arguments.shift 151 | 152 | verify_required_options!(command.flags, command, parsed_command_options[command]) 153 | 154 | begin 155 | command = command_finder.find_command(next_command_name) 156 | rescue AmbiguousCommand 157 | arguments.unshift(next_command_name) 158 | break 159 | rescue UnknownCommand 160 | arguments.unshift(next_command_name) 161 | # Although command finder could certainly know if it should use 162 | # the default command, it has no way to put the "unknown command" 163 | # back into the argument stack. UGH. 164 | unless command.get_default_command.nil? 165 | command = command_finder.find_command(command.get_default_command) 166 | end 167 | break 168 | end 169 | end 170 | parsed_command_options[command] ||= {} 171 | command_options = parsed_command_options[command] 172 | 173 | this_command = command.parent 174 | child_command_options = command_options 175 | 176 | while this_command.kind_of?(command.class) 177 | this_command_options = parsed_command_options[this_command] || {} 178 | child_command_options[GLI::Command::PARENT] = this_command_options 179 | this_command = this_command.parent 180 | child_command_options = this_command_options 181 | end 182 | 183 | parsing_result.command_options = command_options 184 | parsing_result.command = command 185 | parsing_result.arguments = Array(arguments.compact) 186 | 187 | # Lets validate the arguments now that we know for sure the command that is invoked 188 | verify_arguments!(parsing_result.arguments, parsing_result.command) if argument_handling_strategy == :strict 189 | 190 | parsing_result 191 | end 192 | 193 | end 194 | 195 | class LegacyCommandOptionParser < NormalCommandOptionParser 196 | def parse!(parsing_result,argument_handling_strategy,autocomplete) 197 | command = parsing_result.command 198 | option_parser_factory = OptionParserFactory.for_command(command,@accepts) 199 | option_block_parser = LegacyCommandOptionBlockParser.new(option_parser_factory, self.error_handler) 200 | option_block_parser.command = command 201 | 202 | parsing_result.arguments = option_block_parser.parse!(parsing_result.arguments) 203 | parsing_result.command_options = option_parser_factory.options_hash_with_defaults_set! 204 | 205 | subcommand,args = find_subcommand(command,parsing_result.arguments,autocomplete) 206 | parsing_result.command = subcommand 207 | parsing_result.arguments = args 208 | verify_required_options!(command.flags, parsing_result.command, parsing_result.command_options) 209 | end 210 | 211 | private 212 | 213 | def find_subcommand(command,arguments,autocomplete) 214 | arguments = Array(arguments) 215 | command_name = if arguments.empty? 216 | nil 217 | else 218 | arguments.first 219 | end 220 | 221 | default_command = command.get_default_command 222 | finder = CommandFinder.new(command.commands, :default_command => default_command.to_s, :autocomplete => autocomplete) 223 | 224 | begin 225 | results = [finder.find_command(command_name),arguments[1..-1]] 226 | find_subcommand(results[0],results[1],autocomplete) 227 | rescue UnknownCommand, AmbiguousCommand 228 | begin 229 | results = [finder.find_command(default_command.to_s),arguments] 230 | find_subcommand(results[0],results[1],autocomplete) 231 | rescue UnknownCommand, AmbiguousCommand 232 | [command,arguments] 233 | end 234 | end 235 | end 236 | end 237 | end 238 | end 239 | -------------------------------------------------------------------------------- /lib/gli/commands/scaffold.rb: -------------------------------------------------------------------------------- 1 | require 'gli' 2 | require 'fileutils' 3 | 4 | module GLI 5 | module Commands 6 | class Scaffold #:nodoc: 7 | 8 | def self.create_scaffold(root_dir, 9 | create_test_dir, 10 | create_ext_dir, 11 | project_name, 12 | commands, 13 | force=false, 14 | dry_run=false, 15 | create_rvmrc=false) 16 | dirs = [File.join(root_dir,project_name,'lib')] 17 | dirs << File.join(root_dir,project_name,'bin') 18 | dirs << File.join(root_dir,project_name,'test') if create_test_dir 19 | dirs << File.join(root_dir,project_name,'ext') if create_ext_dir 20 | 21 | if mkdirs(dirs,force,dry_run) 22 | mk_binfile(root_dir,create_ext_dir,force,dry_run,project_name,commands) 23 | mk_readme(root_dir,dry_run,project_name) 24 | mk_gemspec(root_dir,dry_run,project_name) 25 | mk_rakefile(root_dir,dry_run,project_name,create_test_dir) 26 | mk_lib_files(root_dir,dry_run,project_name) 27 | if create_rvmrc 28 | rvmrc = File.join(root_dir,project_name,".rvmrc") 29 | File.open(rvmrc,'w') do |file| 30 | file.puts "rvm use #{ENV['rvm_ruby_string']}@#{project_name} --create" 31 | end 32 | puts "Created #{rvmrc}" 33 | end 34 | init_git(root_dir, project_name) 35 | end 36 | end 37 | 38 | def self.mk_readme(root_dir,dry_run,project_name) 39 | return if dry_run 40 | File.open("#{root_dir}/#{project_name}/README.rdoc",'w') do |file| 41 | file << "= #{project_name}\n\n" 42 | file << "Describe your project here\n\n" 43 | file << ":include:#{project_name}.rdoc\n\n" 44 | end 45 | puts "Created #{root_dir}/#{project_name}/README.rdoc" 46 | File.open("#{root_dir}/#{project_name}/#{project_name}.rdoc",'w') do |file| 47 | file << "= #{project_name}\n\n" 48 | file << "Generate this with\n #{project_name} _doc\nAfter you have described your command line interface" 49 | end 50 | puts "Created #{root_dir}/#{project_name}/#{project_name}.rdoc" 51 | end 52 | 53 | def self.mk_gemspec(root_dir,dry_run,project_name) 54 | return if dry_run 55 | File.open("#{root_dir}/#{project_name}/#{project_name}.gemspec",'w') do |file| 56 | file.puts < 0.9.2') 74 | s.add_development_dependency('rdoc', '~> 4.3') 75 | s.add_development_dependency('minitest', '~> 5.14') 76 | s.add_runtime_dependency('gli','~> #{GLI::VERSION}') 77 | end 78 | EOS 79 | end 80 | puts "Created #{root_dir}/#{project_name}/#{project_name}.gemspec" 81 | end 82 | 83 | def self.project_name_as_module_name(project_name) 84 | project_name.split(/[_-]/).map { |part| part[0..0].upcase + part[1..-1] }.join('') 85 | end 86 | 87 | def self.mk_lib_files(root_dir,dry_run,project_name) 88 | return if dry_run 89 | FileUtils.mkdir("#{root_dir}/#{project_name}/lib/#{project_name}") 90 | File.open("#{root_dir}/#{project_name}/lib/#{project_name}/version.rb",'w') do |file| 91 | file.puts < :test 138 | EOS 139 | File.open("#{root_dir}/#{project_name}/test/default_test.rb",'w') do |test_file| 140 | test_file.puts < :package\n" 169 | end 170 | end 171 | puts "Created #{root_dir}/#{project_name}/Rakefile" 172 | File.open("#{root_dir}/#{project_name}/Gemfile",'w') do |bundler_file| 173 | bundler_file.puts "source 'https://rubygems.org'" 174 | bundler_file.puts "gemspec" 175 | end 176 | puts "Created #{root_dir}/#{project_name}/Gemfile" 177 | end 178 | 179 | def self.mk_binfile(root_dir,create_ext_dir,force,dry_run,project_name,commands) 180 | bin_file = File.join(root_dir,project_name,'bin',project_name) 181 | if !File.exist?(bin_file) || force 182 | if !dry_run 183 | File.open(bin_file,'w') do |file| 184 | file.chmod(0755) 185 | file.puts '#!/usr/bin/env ruby' 186 | file.puts <