cli_base 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/HISTORY.md +48 -0
- data/README.md +65 -0
- data/lib/cli/base.rb +182 -0
- data/lib/cli/config.rb +16 -0
- data/lib/cli/core_ext.rb +20 -0
- data/lib/cli/errors.rb +26 -0
- data/lib/cli/help.rb +295 -0
- data/lib/cli/parser.rb +211 -0
- data/lib/cli/source.rb +81 -0
- data/lib/cli/utils.rb +43 -0
- data/lib/cli/version.rb +5 -0
- data/lib/cli_base.rb +2 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4e6c9333d54368e8012669ea89d3b1ec55fba5f5a8851dfaa4c44df9d922bb41
|
|
4
|
+
data.tar.gz: 91a489bfed668142eb0efda172b5b4ea6d4441e5d93a34783a2e78c13f12f5e6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4a586009ff6a6c1f94476b65c529763c20a2303253072ea31762c6f390049518d437944b0a43184ec86218d40c1d58e63694fd69c49aa4cf127d2b35732ae367
|
|
7
|
+
data.tar.gz: bdbf99d8468944e32d86d1107e4cf786e5c51d52e507eb6adfa9344b7d765c793b4ee285a665cfa97828dd51075113098439b59fa8f1d48b7be63ddd6cf529d1
|
data/HISTORY.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# RELEASE HISTORY
|
|
2
|
+
|
|
3
|
+
## 0.5.1 / 2026-04-01
|
|
4
|
+
|
|
5
|
+
Maintenance release. Modernized project tooling.
|
|
6
|
+
|
|
7
|
+
Changes:
|
|
8
|
+
|
|
9
|
+
* Replace custom metadata system with standard gemspec.
|
|
10
|
+
* Simplify version handling.
|
|
11
|
+
* Update README to markdown.
|
|
12
|
+
* Clean up obsolete files and .gitignore.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## 0.5.0 / 2011-10-08
|
|
16
|
+
|
|
17
|
+
The library has been renamed to CLI::Base (gem name `cli_base`).
|
|
18
|
+
|
|
19
|
+
Changes:
|
|
20
|
+
|
|
21
|
+
* Renamed to CLI::Base.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## 0.4.0 / 2010-10-12
|
|
25
|
+
|
|
26
|
+
New API uses nested classes instead of methods for subcommands.
|
|
27
|
+
|
|
28
|
+
Changes:
|
|
29
|
+
|
|
30
|
+
* New API!
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## 0.3.0 / 2010-09-12
|
|
34
|
+
|
|
35
|
+
The most significant change is the use of bang methods (foo!) for option flags.
|
|
36
|
+
|
|
37
|
+
Changes:
|
|
38
|
+
|
|
39
|
+
* Allow bang methods for option flags.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## 0.2.0 / 2010-03-02
|
|
43
|
+
|
|
44
|
+
Initial (non-public) release. Rebranding of older library called TieClip.
|
|
45
|
+
|
|
46
|
+
Changes:
|
|
47
|
+
|
|
48
|
+
* Happy Birthday!
|
data/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# CLI::Base
|
|
2
|
+
|
|
3
|
+
[Source Code](https://github.com/rubyworks/cli_base) |
|
|
4
|
+
[Report Issue](https://github.com/rubyworks/cli_base/issues)
|
|
5
|
+
|
|
6
|
+
[](https://rubygems.org/gems/cli_base)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Description
|
|
10
|
+
|
|
11
|
+
CLI::Base is a quick-and-dirty CLI framework for Ruby. Need a command-line
|
|
12
|
+
interface without the ceremony? Just subclass `CLI::Base` and define methods —
|
|
13
|
+
writer methods (`=`) become options, query methods (`?`) become flags, and
|
|
14
|
+
nested classes become subcommands. No DSL to learn.
|
|
15
|
+
|
|
16
|
+
Think of it as a COM (Command-to-Object Mapper).
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Synopsis
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require 'cli/base'
|
|
23
|
+
|
|
24
|
+
class MyCLI < CLI::Base
|
|
25
|
+
# Require LIBRARY before executing your script.
|
|
26
|
+
def require=(lib)
|
|
27
|
+
require lib
|
|
28
|
+
end
|
|
29
|
+
alias :r= :require=
|
|
30
|
+
|
|
31
|
+
# Include PATH in $LOAD_PATH.
|
|
32
|
+
def include=(path)
|
|
33
|
+
$:.unshift path
|
|
34
|
+
end
|
|
35
|
+
alias :I= :include=
|
|
36
|
+
|
|
37
|
+
# Run in DEBUG mode.
|
|
38
|
+
def debug?
|
|
39
|
+
$DEBUG = true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Show this message.
|
|
43
|
+
def help?
|
|
44
|
+
puts self
|
|
45
|
+
exit
|
|
46
|
+
end
|
|
47
|
+
alias :h? :help?
|
|
48
|
+
|
|
49
|
+
# Run the command.
|
|
50
|
+
def main(script)
|
|
51
|
+
load(script)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
That's it. The comments above each method become the help text automatically.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## Copyright
|
|
60
|
+
|
|
61
|
+
Copyright (c) 2008 Thomas Sawyer, Rubyworks
|
|
62
|
+
|
|
63
|
+
Distributed under the terms of the **BSD-2-Clause** license.
|
|
64
|
+
|
|
65
|
+
See COPYING.rdoc for details.
|
data/lib/cli/base.rb
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
module CLI
|
|
2
|
+
|
|
3
|
+
# = CLI::Base
|
|
4
|
+
#
|
|
5
|
+
# class MyCLI < CLI::Base
|
|
6
|
+
#
|
|
7
|
+
# # cmd --debug
|
|
8
|
+
#
|
|
9
|
+
# def debug?
|
|
10
|
+
# $DEBUG
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# def debug=(bool)
|
|
14
|
+
# $DEBUG = bool
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # $ foo remote
|
|
18
|
+
#
|
|
19
|
+
# class Remote < CLI::Base
|
|
20
|
+
#
|
|
21
|
+
# # $ foo remote --verbose
|
|
22
|
+
#
|
|
23
|
+
# def verbose?
|
|
24
|
+
# @verbose
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# def verbose=(bool)
|
|
28
|
+
# @verbose = bool
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# # $ foo remote --force
|
|
32
|
+
#
|
|
33
|
+
# def force?
|
|
34
|
+
# @force
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# def force=(bool)
|
|
38
|
+
# @force = bool
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# # $ foo remote --output <path>
|
|
42
|
+
#
|
|
43
|
+
# def output=(path)
|
|
44
|
+
# @path = path
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# # $ foo remote -o <path>
|
|
48
|
+
#
|
|
49
|
+
# alias_method :o=, :output=
|
|
50
|
+
#
|
|
51
|
+
# # $ foo remote add
|
|
52
|
+
#
|
|
53
|
+
# class Add < self
|
|
54
|
+
#
|
|
55
|
+
# def main(name, branch)
|
|
56
|
+
# # ...
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# # $ foo remote show
|
|
62
|
+
#
|
|
63
|
+
# class Show < self
|
|
64
|
+
#
|
|
65
|
+
# def main(name)
|
|
66
|
+
# # ...
|
|
67
|
+
# end
|
|
68
|
+
#
|
|
69
|
+
# end
|
|
70
|
+
#
|
|
71
|
+
# end
|
|
72
|
+
#
|
|
73
|
+
# end
|
|
74
|
+
#
|
|
75
|
+
class Base
|
|
76
|
+
|
|
77
|
+
require 'cli/errors'
|
|
78
|
+
require 'cli/parser'
|
|
79
|
+
require 'cli/help'
|
|
80
|
+
require 'cli/config'
|
|
81
|
+
require 'cli/utils'
|
|
82
|
+
|
|
83
|
+
# TODO: Should #main be called #call instead?
|
|
84
|
+
|
|
85
|
+
#
|
|
86
|
+
def main(*args)
|
|
87
|
+
#puts self.class # TODO: fix help
|
|
88
|
+
raise NoCommandError
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Override option_missing if needed. This receives the name of the option
|
|
92
|
+
# and the remaining argumentslist. It must consume any arguments it uses
|
|
93
|
+
# from the begining of the list (i.e. in-place manipulation).
|
|
94
|
+
#
|
|
95
|
+
def option_missing(opt, argv)
|
|
96
|
+
raise NoOptionError, opt
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Access the help instance of the class of the command object.
|
|
100
|
+
def cli_help
|
|
101
|
+
self.class.help
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class << self
|
|
105
|
+
|
|
106
|
+
# Helper method for creating switch attributes.
|
|
107
|
+
#
|
|
108
|
+
# This is equivalent to:
|
|
109
|
+
#
|
|
110
|
+
# def name=(val)
|
|
111
|
+
# @name = val
|
|
112
|
+
# end
|
|
113
|
+
#
|
|
114
|
+
# def name?
|
|
115
|
+
# @name
|
|
116
|
+
# end
|
|
117
|
+
#
|
|
118
|
+
def attr_switch(name)
|
|
119
|
+
attr_writer name
|
|
120
|
+
module_eval %{
|
|
121
|
+
def #{name}?
|
|
122
|
+
@#{name}
|
|
123
|
+
end
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Run the command.
|
|
128
|
+
#
|
|
129
|
+
# @param argv [Array] command-line arguments
|
|
130
|
+
#
|
|
131
|
+
def execute(argv=ARGV)
|
|
132
|
+
cli, args = parser.parse(argv)
|
|
133
|
+
cli.main(*args)
|
|
134
|
+
return cli
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# CLI::Base classes don't run, they execute! But...
|
|
138
|
+
alias_method :run, :execute
|
|
139
|
+
|
|
140
|
+
# Command configuration options.
|
|
141
|
+
#
|
|
142
|
+
# @todo: This isn't used yet. Eventually the idea is to allow
|
|
143
|
+
# some additional flexibility in the parser behavior.
|
|
144
|
+
def config
|
|
145
|
+
@config ||= Config.new
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The parser for this command.
|
|
149
|
+
def parser
|
|
150
|
+
@parser ||= Parser.new(self)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# List of subcommands.
|
|
154
|
+
def subcommands
|
|
155
|
+
parser.subcommands
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Interface with cooresponding help object.
|
|
159
|
+
def help
|
|
160
|
+
@help ||= Help.new(self)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
#
|
|
164
|
+
def inspect
|
|
165
|
+
name
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# When inherited, setup up the +file+ and +line+ of the
|
|
169
|
+
# subcommand via +caller+. If for some odd reason this
|
|
170
|
+
# does not work then manually use +setup+ method.
|
|
171
|
+
#
|
|
172
|
+
def inherited(subclass)
|
|
173
|
+
file, line, _ = *caller.first.split(':')
|
|
174
|
+
file = File.expand_path(file)
|
|
175
|
+
subclass.help.setup(file,line.to_i)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
end
|
data/lib/cli/config.rb
ADDED
data/lib/cli/core_ext.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class UnboundMethod
|
|
2
|
+
if !method_defined?(:source_location)
|
|
3
|
+
if Proc.method_defined? :__file__ # /ree/
|
|
4
|
+
def source_location
|
|
5
|
+
[__file__, __line__] rescue nil
|
|
6
|
+
end
|
|
7
|
+
elsif defined?(RUBY_ENGINE) && RUBY_ENGINE =~ /jruby/
|
|
8
|
+
require 'java'
|
|
9
|
+
def source_location
|
|
10
|
+
to_java.source_location(Thread.current.to_java.getContext())
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
def comment
|
|
17
|
+
Source.get_above_comment(*source_location)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
data/lib/cli/errors.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module CLI
|
|
2
|
+
|
|
3
|
+
class Base
|
|
4
|
+
|
|
5
|
+
class NoOptionError < ::NoMethodError # ArgumentError ?
|
|
6
|
+
def initialize(name, *arg)
|
|
7
|
+
super("unknown option -- #{name}", name, *args)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
#class NoCommandError < ::NoMethodError
|
|
12
|
+
# def initialize(name, *args)
|
|
13
|
+
# super("unknown command -- #{name}", name, *args)
|
|
14
|
+
# end
|
|
15
|
+
#end
|
|
16
|
+
|
|
17
|
+
class NoCommandError < ::NoMethodError
|
|
18
|
+
def initialize(*args)
|
|
19
|
+
super("missing command", *args)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
|
data/lib/cli/help.rb
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
module CLI
|
|
2
|
+
|
|
3
|
+
require 'cli/source'
|
|
4
|
+
require 'cli/core_ext'
|
|
5
|
+
|
|
6
|
+
# Encpsulates command help for deefining and displaying well formated help
|
|
7
|
+
# output in plain text or via manpages if found.
|
|
8
|
+
class Help
|
|
9
|
+
|
|
10
|
+
# Setup new help object.
|
|
11
|
+
def initialize(cli_class)
|
|
12
|
+
@cli_class = cli_class
|
|
13
|
+
|
|
14
|
+
@usage = nil
|
|
15
|
+
@footer = nil
|
|
16
|
+
|
|
17
|
+
@options = {}
|
|
18
|
+
@subcmds = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Set file and line under which the CLI::Base subclass is defined.
|
|
22
|
+
def setup(file, line=nil)
|
|
23
|
+
@file = file
|
|
24
|
+
@line = line
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The CLI::Base subclass to which this help applies.
|
|
28
|
+
attr :cli_class
|
|
29
|
+
|
|
30
|
+
# Get or set command name.
|
|
31
|
+
#
|
|
32
|
+
# By default the name is assumed to be the class name, substituting
|
|
33
|
+
# dashes for double colons.
|
|
34
|
+
def name(name=nil)
|
|
35
|
+
@name = name if name
|
|
36
|
+
@name ||= cli_class.name.downcase.gsub('::','-')
|
|
37
|
+
#File.basename($0)
|
|
38
|
+
@name
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Get or set command usage.
|
|
42
|
+
def usage(text=nil)
|
|
43
|
+
@usage ||= "Usage: " + File.basename($0) + ' [options...] [subcommand]'
|
|
44
|
+
@usage = text unless text.nil?
|
|
45
|
+
@usage
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Set command usage.
|
|
49
|
+
def usage=(text)
|
|
50
|
+
@usage = text
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get or set command description.
|
|
54
|
+
def description(text=nil)
|
|
55
|
+
@description = text unless text.nil?
|
|
56
|
+
end
|
|
57
|
+
alias_method :header, :description
|
|
58
|
+
|
|
59
|
+
# Get or set command help footer.
|
|
60
|
+
def footer(text=nil)
|
|
61
|
+
@footer = text unless text.nil?
|
|
62
|
+
@footer
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Set comamnd help footer.
|
|
66
|
+
def footer=(text)
|
|
67
|
+
@footer = text
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Set description of an option.
|
|
71
|
+
def option(name, description)
|
|
72
|
+
@options[name.to_s] = description
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Set desciption of a subcommand.
|
|
76
|
+
def subcommand(name, description)
|
|
77
|
+
@subcmds[name.to_s] = description
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#alias_method :inspect, :to_s
|
|
81
|
+
|
|
82
|
+
# Show help.
|
|
83
|
+
#
|
|
84
|
+
# @todo man-pages will probably fail on Windows
|
|
85
|
+
def show_help(hint=nil)
|
|
86
|
+
if file = manpage(hint)
|
|
87
|
+
system "man #{file}"
|
|
88
|
+
else
|
|
89
|
+
puts text
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# M A N P A G E
|
|
94
|
+
|
|
95
|
+
# Get man-page if there is one.
|
|
96
|
+
def manpage(hint=nil)
|
|
97
|
+
@manpage ||= (
|
|
98
|
+
man = []
|
|
99
|
+
dir = @file ? File.dirname(@file) : nil
|
|
100
|
+
glob = "man/#{name}.1"
|
|
101
|
+
|
|
102
|
+
if hint
|
|
103
|
+
if File.exist?(hint)
|
|
104
|
+
return hint
|
|
105
|
+
elsif File.directory?(hint)
|
|
106
|
+
dir = hint
|
|
107
|
+
else
|
|
108
|
+
glob = hint if hint
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if dir
|
|
113
|
+
while dir != '/'
|
|
114
|
+
man.concat(Dir[File.join(dir, glob)])
|
|
115
|
+
#man.concat(Dir[File.join(dir, "man/man1/#{name}.1")])
|
|
116
|
+
#man.concat(Dir[File.join(dir, "man/#{name}.1.ronn")])
|
|
117
|
+
#man.concat(Dir[File.join(dir, "man/man1/#{name}.1")])
|
|
118
|
+
break unless man.empty?
|
|
119
|
+
dir = File.dirname(dir)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
man.first
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# H E L P T E X T
|
|
128
|
+
|
|
129
|
+
#
|
|
130
|
+
def to_s; text; end
|
|
131
|
+
|
|
132
|
+
#
|
|
133
|
+
def text(file=nil)
|
|
134
|
+
s = []
|
|
135
|
+
s << text_usage
|
|
136
|
+
s << text_description
|
|
137
|
+
s << text_subcommands
|
|
138
|
+
s << text_options
|
|
139
|
+
s << text_footer
|
|
140
|
+
s.compact.join("\n\n")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Command usage.
|
|
144
|
+
def text_usage
|
|
145
|
+
usage
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# TODO: Maybe default description should always come from `main`
|
|
149
|
+
# instead of the the class comment ?
|
|
150
|
+
|
|
151
|
+
# Description of command in printable form.
|
|
152
|
+
# But will return +nil+ if there is no description.
|
|
153
|
+
#
|
|
154
|
+
# @return [String,NilClass] command description
|
|
155
|
+
def text_description
|
|
156
|
+
if @description
|
|
157
|
+
@description
|
|
158
|
+
elsif @file
|
|
159
|
+
Source.get_above_comment(@file, @line)
|
|
160
|
+
elsif main = method_list.find{ |m| m.name == 'main' }
|
|
161
|
+
main.comment
|
|
162
|
+
else
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# List of subcommands converted to a printable string.
|
|
168
|
+
# But will return +nil+ if there are no subcommands.
|
|
169
|
+
#
|
|
170
|
+
# @return [String,NilClass] subcommand list text
|
|
171
|
+
def text_subcommands
|
|
172
|
+
commands = @cli_class.subcommands
|
|
173
|
+
s = []
|
|
174
|
+
if !commands.empty?
|
|
175
|
+
s << "COMMANDS"
|
|
176
|
+
commands.each do |cmd, klass|
|
|
177
|
+
desc = klass.help.text_description.to_s.split("\n").first
|
|
178
|
+
s << " %-17s %s" % [cmd, desc]
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
return nil if s.empty?
|
|
182
|
+
return s.join("\n")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# List of options coverted to a printable string.
|
|
186
|
+
# But will return +nil+ if there are no options.
|
|
187
|
+
#
|
|
188
|
+
# @return [String,NilClass] option list text
|
|
189
|
+
def text_options
|
|
190
|
+
option_list.each do |opt|
|
|
191
|
+
if @options.key?(opt.name)
|
|
192
|
+
opt.description = @options[opt.name]
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
max = option_list.map{ |opt| opt.usage.size }.max + 2
|
|
197
|
+
|
|
198
|
+
s = []
|
|
199
|
+
s << "OPTIONS"
|
|
200
|
+
option_list.each do |opt|
|
|
201
|
+
mark = (opt.name.size == 1 ? ' -' : '--')
|
|
202
|
+
s << " #{mark}%-#{max}s %s" % [opt.usage, opt.description]
|
|
203
|
+
end
|
|
204
|
+
s.join("\n")
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
#
|
|
208
|
+
def text_footer
|
|
209
|
+
footer
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
#
|
|
213
|
+
#def text_common_options
|
|
214
|
+
#s << "\nCOMMON OPTIONS:\n\n"
|
|
215
|
+
#global_options.each do |(name, meth)|
|
|
216
|
+
# if name.size == 1
|
|
217
|
+
# s << " -%-15s %s\n" % [name, descriptions[meth]]
|
|
218
|
+
# else
|
|
219
|
+
# s << " --%-15s %s\n" % [name, descriptions[meth]]
|
|
220
|
+
# end
|
|
221
|
+
#end
|
|
222
|
+
#end
|
|
223
|
+
|
|
224
|
+
#
|
|
225
|
+
def option_list
|
|
226
|
+
@option_list ||= (
|
|
227
|
+
method_list.map do |meth|
|
|
228
|
+
case meth.name
|
|
229
|
+
when /^(.*?)[\!\=]$/
|
|
230
|
+
Option.new(meth)
|
|
231
|
+
end
|
|
232
|
+
end.compact.sort
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
private
|
|
237
|
+
|
|
238
|
+
# Produce a list relavent methods.
|
|
239
|
+
#
|
|
240
|
+
def method_list
|
|
241
|
+
list = []
|
|
242
|
+
methods = []
|
|
243
|
+
stop_at = cli_class.ancestors.index(CLI::Base) || -1
|
|
244
|
+
ancestors = cli_class.ancestors[0...stop_at]
|
|
245
|
+
ancestors.reverse_each do |a|
|
|
246
|
+
a.instance_methods(false).each do |m|
|
|
247
|
+
list << cli_class.instance_method(m)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
list
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Encapsualtes a command line option.
|
|
254
|
+
class Option
|
|
255
|
+
def initialize(method)
|
|
256
|
+
@method = method
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def name
|
|
260
|
+
@method.name.to_s.chomp('!').chomp('=')
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def comment
|
|
264
|
+
@method.comment
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def description
|
|
268
|
+
@description ||= comment.split("\n").first
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Set description manually.
|
|
272
|
+
def description=(desc)
|
|
273
|
+
@description = desc
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def parameter
|
|
277
|
+
param = @method.parameters.first
|
|
278
|
+
param.last if param
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def usage
|
|
282
|
+
if parameter
|
|
283
|
+
"#{name}=#{parameter.to_s.upcase}"
|
|
284
|
+
else
|
|
285
|
+
"#{name}"
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def <=>(other)
|
|
290
|
+
self.name <=> other.name
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
end
|
data/lib/cli/parser.rb
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
module CLI
|
|
2
|
+
|
|
3
|
+
# The Parse class does all the heavy lifting for CLI::Base.
|
|
4
|
+
#
|
|
5
|
+
class Parser
|
|
6
|
+
|
|
7
|
+
#
|
|
8
|
+
# @param cli_class [CLI::Base] command class
|
|
9
|
+
#
|
|
10
|
+
def initialize(cli_class)
|
|
11
|
+
@cli_class = cli_class
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr :cli_class
|
|
15
|
+
|
|
16
|
+
# Parse command-line.
|
|
17
|
+
#
|
|
18
|
+
# @param argv [Array,String] command-line arguments
|
|
19
|
+
#
|
|
20
|
+
def parse(argv=ARGV)
|
|
21
|
+
argv = parse_shellwords(argv)
|
|
22
|
+
|
|
23
|
+
cmd, argv = parse_subcommand(argv)
|
|
24
|
+
cli = cmd.new
|
|
25
|
+
args = parse_arguments(cli, argv)
|
|
26
|
+
|
|
27
|
+
return cli, args
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Make sure arguments are an array. If argv is a String,
|
|
31
|
+
# then parse using Shellwords module.
|
|
32
|
+
#
|
|
33
|
+
# @param argv [Array,String] commmand-line arguments
|
|
34
|
+
def parse_shellwords(argv)
|
|
35
|
+
if String === argv
|
|
36
|
+
require 'shellwords'
|
|
37
|
+
argv = Shellwords.shellwords(argv)
|
|
38
|
+
end
|
|
39
|
+
argv.to_a
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
#
|
|
43
|
+
#
|
|
44
|
+
#
|
|
45
|
+
def parse_subcommand(argv)
|
|
46
|
+
cmd = cli_class
|
|
47
|
+
arg = argv.first
|
|
48
|
+
|
|
49
|
+
while c = cmd.subcommands[arg]
|
|
50
|
+
cmd = c
|
|
51
|
+
argv.shift
|
|
52
|
+
arg = argv.first
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
return cmd, argv
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
#
|
|
59
|
+
# Parse command line options based on given object.
|
|
60
|
+
#
|
|
61
|
+
# @param obj [Object] basis for command-line parsing
|
|
62
|
+
# @param argv [Array,String] command-line arguments
|
|
63
|
+
# @param args [Array] pre-seeded arguments to add to
|
|
64
|
+
#
|
|
65
|
+
# @return [Array] parsed arguments
|
|
66
|
+
#
|
|
67
|
+
def parse_arguments(obj, argv, args=[])
|
|
68
|
+
case argv
|
|
69
|
+
when String
|
|
70
|
+
require 'shellwords'
|
|
71
|
+
argv = Shellwords.shellwords(argv)
|
|
72
|
+
#else
|
|
73
|
+
# argv = argv.dup
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
#subc = nil
|
|
77
|
+
#@args = [] #opts, i = {}, 0
|
|
78
|
+
|
|
79
|
+
while argv.size > 0
|
|
80
|
+
case arg = argv.shift
|
|
81
|
+
when /=/
|
|
82
|
+
parse_equal(obj, arg, argv, args)
|
|
83
|
+
when /^--/
|
|
84
|
+
parse_long(obj, arg, argv, args)
|
|
85
|
+
when /^-/
|
|
86
|
+
parse_flags(obj, arg, argv, args)
|
|
87
|
+
else
|
|
88
|
+
#if CLI::Base === obj
|
|
89
|
+
# if cmd_class = obj.class.subcommands[arg]
|
|
90
|
+
# cmd = cmd_class.new(obj)
|
|
91
|
+
# subc = cmd
|
|
92
|
+
# parse(cmd, argv, args)
|
|
93
|
+
# else
|
|
94
|
+
args << arg
|
|
95
|
+
# end
|
|
96
|
+
#end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
return args
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
#
|
|
104
|
+
# Parse equal setting comman-line option.
|
|
105
|
+
#
|
|
106
|
+
def parse_equal(obj, opt, argv, args)
|
|
107
|
+
if md = /^[-]*(.*?)=(.*?)$/.match(opt)
|
|
108
|
+
x, v = md[1], md[2]
|
|
109
|
+
else
|
|
110
|
+
raise ArgumentError, "#{x}"
|
|
111
|
+
end
|
|
112
|
+
if obj.respond_to?("#{x}=")
|
|
113
|
+
v = true if v == 'true' # yes or on ?
|
|
114
|
+
v = false if v == 'false' # no or off ?
|
|
115
|
+
obj.send("#{x}=", v)
|
|
116
|
+
else
|
|
117
|
+
obj.option_missing(x, v) # argv?
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
#
|
|
122
|
+
# Parse double-dash command-line option.
|
|
123
|
+
#
|
|
124
|
+
def parse_long(obj, opt, argv, args)
|
|
125
|
+
x = opt.sub(/^\-+/, '') # remove '--'
|
|
126
|
+
if obj.respond_to?("#{x}=")
|
|
127
|
+
m = obj.method("#{x}=")
|
|
128
|
+
if obj.respond_to?("#{x}?")
|
|
129
|
+
m.call(true)
|
|
130
|
+
else
|
|
131
|
+
invoke(obj, m, argv)
|
|
132
|
+
end
|
|
133
|
+
elsif obj.respond_to?("#{x}!")
|
|
134
|
+
invoke(obj, "#{x}!", argv)
|
|
135
|
+
else
|
|
136
|
+
obj.option_missing(x, argv)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
#
|
|
141
|
+
# Parse single-dash command-line option.
|
|
142
|
+
#
|
|
143
|
+
# TODO: This needs some thought concerning character spliting and arguments.
|
|
144
|
+
def parse_flags(obj, opt, argv, args)
|
|
145
|
+
x = opt[1..-1]
|
|
146
|
+
c = 0
|
|
147
|
+
x.split(//).each do |k|
|
|
148
|
+
if obj.respond_to?("#{k}=")
|
|
149
|
+
m = obj.method("#{k}=")
|
|
150
|
+
if obj.respond_to?("#{x}?")
|
|
151
|
+
m.call(true)
|
|
152
|
+
else
|
|
153
|
+
invoke(obj, m, argv) #m.call(argv.shift)
|
|
154
|
+
end
|
|
155
|
+
elsif obj.respond_to?("#{k}!")
|
|
156
|
+
invoke(obj, "#{k}!", argv)
|
|
157
|
+
else
|
|
158
|
+
long = find_longer_option(obj, k)
|
|
159
|
+
if long
|
|
160
|
+
if long.end_with?('=') && obj.respond_to?(long.chomp('=')+'?')
|
|
161
|
+
invoke(obj, long, [true])
|
|
162
|
+
else
|
|
163
|
+
invoke(obj, long, argv)
|
|
164
|
+
end
|
|
165
|
+
else
|
|
166
|
+
obj.option_missing(x, argv)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
#
|
|
173
|
+
#
|
|
174
|
+
# TODO: Sort alphabetically?
|
|
175
|
+
def find_longer_option(obj, char)
|
|
176
|
+
meths = obj.methods.map{ |m| m.to_s }
|
|
177
|
+
meths = meths.select do |m|
|
|
178
|
+
m.start_with?(char) and (m.end_with?('=') or m.end_with?('!'))
|
|
179
|
+
end
|
|
180
|
+
meths.first
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
#
|
|
184
|
+
#
|
|
185
|
+
def invoke(obj, meth, argv)
|
|
186
|
+
m = Method === meth ? meth : obj.method(meth)
|
|
187
|
+
a = []
|
|
188
|
+
m.arity.abs.times{ a << argv.shift }
|
|
189
|
+
m.call(*a)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Index of subcommands.
|
|
193
|
+
#
|
|
194
|
+
# @return [Hash] name mapped to subcommnd class
|
|
195
|
+
def subcommands
|
|
196
|
+
@subcommands ||= (
|
|
197
|
+
consts = @cli_class.constants - @cli_class.superclass.constants
|
|
198
|
+
consts.inject({}) do |h, c|
|
|
199
|
+
c = @cli_class.const_get(c)
|
|
200
|
+
if Class === c && CLI::Base > c
|
|
201
|
+
n = c.name.split('::').last.downcase
|
|
202
|
+
h[n] = c
|
|
203
|
+
end
|
|
204
|
+
h
|
|
205
|
+
end
|
|
206
|
+
)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
end
|
data/lib/cli/source.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Manage source lookup.
|
|
2
|
+
module Source
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
# Read and cache file.
|
|
6
|
+
#
|
|
7
|
+
# @param file [String] filename, should be full path
|
|
8
|
+
#
|
|
9
|
+
# @return [Array] file content in array of lines
|
|
10
|
+
def read(file)
|
|
11
|
+
@read ||= {}
|
|
12
|
+
@read[file] ||= File.readlines(file)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Get comment from file searching up from given line number.
|
|
16
|
+
#
|
|
17
|
+
# @param file [String] filename, should be full path
|
|
18
|
+
# @param line [Integer] line number in file
|
|
19
|
+
#
|
|
20
|
+
def get_above_comment(file, line)
|
|
21
|
+
get_above_comment_lines(file, line).join("\n").strip
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Get comment from file searching up from given line number.
|
|
25
|
+
#
|
|
26
|
+
# @param file [String] filename, should be full path
|
|
27
|
+
# @param line [Integer] line number in file
|
|
28
|
+
#
|
|
29
|
+
def get_above_comment_lines(file, line)
|
|
30
|
+
text = read(file)
|
|
31
|
+
index = line - 1
|
|
32
|
+
while index >= 0 && text[index] !~ /^\s*\#/
|
|
33
|
+
return [] if text[index] =~ /^\s*end/
|
|
34
|
+
index -= 1
|
|
35
|
+
end
|
|
36
|
+
rindex = index
|
|
37
|
+
while text[index] =~ /^\s*\#/
|
|
38
|
+
index -= 1
|
|
39
|
+
end
|
|
40
|
+
result = text[index..rindex]
|
|
41
|
+
result = result.map{ |s| s.strip }
|
|
42
|
+
result = result.reject{ |s| s[0,1] != '#' }
|
|
43
|
+
result = result.map{ |s| s.sub(/^#/,'').strip }
|
|
44
|
+
#result = result.reject{ |s| s == "" }
|
|
45
|
+
result
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Get comment from file searching down from given line number.
|
|
49
|
+
#
|
|
50
|
+
# @param file [String] filename, should be full path
|
|
51
|
+
# @param line [Integer] line number in file
|
|
52
|
+
#
|
|
53
|
+
def get_following_comment(file, line)
|
|
54
|
+
get_following_comment_lines(file, line).join("\n").strip
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get comment from file searching down from given line number.
|
|
58
|
+
#
|
|
59
|
+
# @param file [String] filename, should be full path
|
|
60
|
+
# @param line [Integer] line number in file
|
|
61
|
+
#
|
|
62
|
+
def get_following_comment_lines(file, line)
|
|
63
|
+
text = read(file)
|
|
64
|
+
index = line || 0
|
|
65
|
+
while text[index] !~ /^\s*\#/
|
|
66
|
+
return nil if text[index] =~ /^\s*(class|module)/
|
|
67
|
+
index += 1
|
|
68
|
+
end
|
|
69
|
+
rindex = index
|
|
70
|
+
while text[rindex] =~ /^\s*\#/
|
|
71
|
+
rindex += 1
|
|
72
|
+
end
|
|
73
|
+
result = text[index..(rindex-2)]
|
|
74
|
+
result = result.map{ |s| s.strip }
|
|
75
|
+
result = result.reject{ |s| s[0,1] != '#' }
|
|
76
|
+
result = result.map{ |s| s.sub(/^#/,'').strip }
|
|
77
|
+
result.join("\n").strip
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
end
|
|
81
|
+
|
data/lib/cli/utils.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module CLI
|
|
4
|
+
|
|
5
|
+
# Some handy-dandy CLI utility methods.
|
|
6
|
+
#
|
|
7
|
+
module Utils
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
# TODO: Maybe #ask chould serve all purposes depending on degfault?
|
|
11
|
+
# e.g. `ask?("ok?", default=>true)`, would be same as `yes?("ok?")`.
|
|
12
|
+
|
|
13
|
+
# Strings to interprest as boolean values.
|
|
14
|
+
BOOLEAN_MAP = {"y"=>true, "yes"=>true, "n"=>false, "no"=>false}
|
|
15
|
+
|
|
16
|
+
# Query the user for a yes/no answer, defaulting to yes.
|
|
17
|
+
def yes?(question, options={})
|
|
18
|
+
print "#{question} [Y/n] "
|
|
19
|
+
input = STDIN.readline.chomp.downcase
|
|
20
|
+
BOOLEAN_MAP[input] || true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Query the user for a yes/no answer, defaulting to no.
|
|
24
|
+
def no?(question, options={})
|
|
25
|
+
print "#{question} [y/N] "
|
|
26
|
+
input = STDIN.readline.chomp.downcase
|
|
27
|
+
BOOLEAN_MAP[input] || false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Query the user for an answer.
|
|
31
|
+
def ask(question, options={})
|
|
32
|
+
print "#{question} [default: #{options[:default]}] "
|
|
33
|
+
reply = STDIN.readline.chomp
|
|
34
|
+
if reply.empty?
|
|
35
|
+
options[:default]
|
|
36
|
+
else
|
|
37
|
+
reply
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
data/lib/cli/version.rb
ADDED
data/lib/cli_base.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cli_base
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.5.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Thomas Sawyer
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rake
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '13'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '13'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5'
|
|
40
|
+
description: Think of CLI::Base as a COM, a Commandline Object Mapper, in much the
|
|
41
|
+
same way that ActiveRecord::Base is an ORM. A subclass of CLI::Base can define a
|
|
42
|
+
complete command line tool using nothing more than Ruby method definitions.
|
|
43
|
+
email:
|
|
44
|
+
- transfire@gmail.com
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- HISTORY.md
|
|
50
|
+
- README.md
|
|
51
|
+
- lib/cli/base.rb
|
|
52
|
+
- lib/cli/config.rb
|
|
53
|
+
- lib/cli/core_ext.rb
|
|
54
|
+
- lib/cli/errors.rb
|
|
55
|
+
- lib/cli/help.rb
|
|
56
|
+
- lib/cli/parser.rb
|
|
57
|
+
- lib/cli/source.rb
|
|
58
|
+
- lib/cli/utils.rb
|
|
59
|
+
- lib/cli/version.rb
|
|
60
|
+
- lib/cli_base.rb
|
|
61
|
+
homepage: https://github.com/rubyworks/cli_base
|
|
62
|
+
licenses:
|
|
63
|
+
- BSD-2-Clause
|
|
64
|
+
metadata: {}
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '3.1'
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 3.6.9
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: Command line tools, meet your Executioner!
|
|
82
|
+
test_files: []
|