cli-topic 0.9.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/Changelog +3 -0
- data/License +20 -0
- data/README.md +12 -0
- data/lib/clitopic/cli.rb +34 -0
- data/lib/clitopic/command/base.rb +136 -0
- data/lib/clitopic/command/clito.rb +99 -0
- data/lib/clitopic/command/help.rb +173 -0
- data/lib/clitopic/command/version.rb +22 -0
- data/lib/clitopic/command.rb +4 -0
- data/lib/clitopic/commands.rb +123 -0
- data/lib/clitopic/helpers.rb +569 -0
- data/lib/clitopic/parser/dummy.rb +15 -0
- data/lib/clitopic/parser/option_parser.rb +70 -0
- data/lib/clitopic/parsers.rb +3 -0
- data/lib/clitopic/topic/base.rb +84 -0
- data/lib/clitopic/topic.rb +4 -0
- data/lib/clitopic/topics.rb +23 -0
- data/lib/clitopic/utils.rb +65 -0
- data/lib/clitopic/version.rb +3 -0
- data/lib/clitopic.rb +44 -0
- data/spec/command_base_spec.rb +25 -0
- data/spec/commands_spec.rb +55 -0
- data/spec/spec_helper.rb +134 -0
- data/spec/topic_base_spec.rb +35 -0
- data/spec/topics_spec.rb +22 -0
- data/spec/utils_spec.rb +17 -0
- data/spec/version_spec.rb +7 -0
- metadata +94 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: db41d9576d7783ae342b2baf3038f6ec6b352c0c
|
4
|
+
data.tar.gz: 6b4599889c0274065ae9476057cde4578c8d1fd8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 43c307785885c77a48275e7816178d086310c828b708ab0aa2e7f20969842713854be436cc35535203e320b2992541c73d2039f0d267dce208e86397df58792e
|
7
|
+
data.tar.gz: 9acf2789a9ffea0eed9bcebfea86de716e3e2cc2ce2018d695392796a88da7bf8457d919d4668612a453d75257eb33b2cea43de87697c02bc3e5e7c0ee8e92ef
|
data/Changelog
ADDED
data/License
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013 Antoine Legrand (ant.legrand@gmail.com)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Topicli
|
2
|
+
==========
|
3
|
+
'Small framework to build CLI. It provide simple way to declare options/descriptions/topics to focus only on the "action" part of commands.
|
4
|
+
Some features:
|
5
|
+
- Light DSL
|
6
|
+
- Commands are organised in Topic (aka Subcommands)
|
7
|
+
- DRY options declaration, it\'s use 3 layers: global -> topic -> command
|
8
|
+
- Each topic has it\'s own description/options list
|
9
|
+
- Load options values from a config file
|
10
|
+
- Built-in Help command: ./cli help TOPIC/COMMAND
|
11
|
+
- Flexible option-parser, Cli-topic use the stdlib OptionParser by default, but can be changed to Slop/Trollop or any custom one.
|
12
|
+
- Command suggestions',
|
data/lib/clitopic/cli.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'clitopic/commands'
|
2
|
+
require 'clitopic/command'
|
3
|
+
require 'clitopic/helpers'
|
4
|
+
module Clitopic
|
5
|
+
module Cli
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def run(args)
|
10
|
+
args = args.dup
|
11
|
+
$stdin.sync = true if $stdin.isatty
|
12
|
+
$stdout.sync = true if $stdout.isatty
|
13
|
+
command = args.shift.strip rescue "help"
|
14
|
+
if !Clitopic.commands_dir.nil?
|
15
|
+
Clitopic::Commands.load_commands(Clitopic.commands_dir)
|
16
|
+
end
|
17
|
+
Clitopic::Commands.run(command, args)
|
18
|
+
rescue Errno::EPIPE => e
|
19
|
+
puts e.message #error(e.message)
|
20
|
+
puts e.backtrace
|
21
|
+
rescue Interrupt => e
|
22
|
+
`stty icanon echo`
|
23
|
+
if Clitopic.debug
|
24
|
+
Clitopic::Helpers.styled_error(e)
|
25
|
+
else
|
26
|
+
Clitopic::Helpers.error("Command cancelled.", false)
|
27
|
+
end
|
28
|
+
rescue => error
|
29
|
+
Clitopic::Helpers.styled_error(error)
|
30
|
+
exit(1)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'clitopic/utils'
|
2
|
+
require 'clitopic/parsers'
|
3
|
+
require 'clitopic/topic'
|
4
|
+
|
5
|
+
module Clitopic
|
6
|
+
module Command
|
7
|
+
class Base
|
8
|
+
class << self
|
9
|
+
include Clitopic.parser
|
10
|
+
|
11
|
+
attr_accessor :name, :banner, :description, :hidden, :short_description
|
12
|
+
attr_accessor :arguments, :options
|
13
|
+
|
14
|
+
def cmd_options
|
15
|
+
@cmd_options ||= []
|
16
|
+
end
|
17
|
+
|
18
|
+
def banner
|
19
|
+
@banner ||= "Usage: #{Clitopic.name} #{self.fullname} [options]"
|
20
|
+
end
|
21
|
+
|
22
|
+
def short_description
|
23
|
+
if @short_description.nil?
|
24
|
+
if description
|
25
|
+
@short_description = description.split("\n").first
|
26
|
+
end
|
27
|
+
end
|
28
|
+
return @short_description
|
29
|
+
end
|
30
|
+
|
31
|
+
def option(name, *args, &blk)
|
32
|
+
opt = Clitopic::Utils.parse_option(name, *args, &blk)
|
33
|
+
if !opt[:default].nil?
|
34
|
+
options[name] = opt[:default]
|
35
|
+
end
|
36
|
+
cmd_options << opt
|
37
|
+
end
|
38
|
+
|
39
|
+
def fullname
|
40
|
+
if topic.nil?
|
41
|
+
return name
|
42
|
+
elsif name == 'index'
|
43
|
+
"#{topic.name}"
|
44
|
+
else
|
45
|
+
"#{topic.name}:#{name}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def call()
|
50
|
+
puts "call with #{options} #{arguments}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def arguments
|
54
|
+
@arguments ||= []
|
55
|
+
end
|
56
|
+
|
57
|
+
def options
|
58
|
+
@options ||= {}
|
59
|
+
end
|
60
|
+
|
61
|
+
def load_defaults(file=nil)
|
62
|
+
if file.nil?
|
63
|
+
Clitopic.default_files.each do |f|
|
64
|
+
if File.exist?(f)
|
65
|
+
file = f
|
66
|
+
break
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
if file.nil?
|
72
|
+
return
|
73
|
+
end
|
74
|
+
|
75
|
+
defaults = YAML.load_file(file)
|
76
|
+
if self.topic.nil?
|
77
|
+
cmd_defaults = defaults[self.name]
|
78
|
+
else
|
79
|
+
cmd_defaults = defaults[self.topic.name][self.name]
|
80
|
+
end
|
81
|
+
|
82
|
+
if cmd_defaults.nil?
|
83
|
+
return
|
84
|
+
end
|
85
|
+
|
86
|
+
cmd_defaults[:options].each do |name, value|
|
87
|
+
if !value.nil?
|
88
|
+
if options[name].nil?
|
89
|
+
options[name] = value
|
90
|
+
elsif options[name].is_a?(Array)
|
91
|
+
options[name] += value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
if cmd_defaults[:arguments] && !arguments
|
96
|
+
arguments += Array(cmd_defaults[:arguments])
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def topic(arg=nil)
|
101
|
+
if !arg.nil?
|
102
|
+
if arg.is_a?(String)
|
103
|
+
@topic ||= Topics[arg]
|
104
|
+
elsif arg.is_a?(Class) && arg < Clitopic::Topic::Base
|
105
|
+
@topic ||= arg.instance
|
106
|
+
elsif arg.is_a?(Hash)
|
107
|
+
@topic ||= Clitopic::Topic::Base.register(arg)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
return @topic
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
def register(opts={})
|
115
|
+
opts = {hidden: false}.merge(opts)
|
116
|
+
if !opts.has_key?(:name)
|
117
|
+
raise ArgumentError.new("missing Command name")
|
118
|
+
end
|
119
|
+
|
120
|
+
topic(opts[:topic])
|
121
|
+
@description = opts[:description]
|
122
|
+
@name = opts[:name]
|
123
|
+
@hidden = opts[:hidden]
|
124
|
+
@banner = opts[:banner]
|
125
|
+
@short_description = opts[:short_description]
|
126
|
+
|
127
|
+
if @topic.nil?
|
128
|
+
Clitopic::Commands.global_commands[name] = self
|
129
|
+
else
|
130
|
+
topic.commands[name] = self
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'clitopic/command/base'
|
3
|
+
require 'clitopic/helpers'
|
4
|
+
|
5
|
+
module Clitopic
|
6
|
+
module Command
|
7
|
+
class ClitoTopic < Clitopic::Topic::Base
|
8
|
+
register name: 'clito',
|
9
|
+
description: 'clitopic commands',
|
10
|
+
hidden: true
|
11
|
+
end
|
12
|
+
|
13
|
+
class Suggestions < Clitopic::Command::Base
|
14
|
+
register name: 'suggestions',
|
15
|
+
description: 'suggests available commands base on incomplete input',
|
16
|
+
hidden: true,
|
17
|
+
topic: 'clito'
|
18
|
+
|
19
|
+
def self.call
|
20
|
+
puts Clitopic::Helpers.suggestion(@arguments[0], Clitopic::Commands.all_commands)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
class DefaultFile < Clitopic::Command::Base
|
26
|
+
register name: 'defaults_file',
|
27
|
+
description: "create default file",
|
28
|
+
hidden: true,
|
29
|
+
topic: 'clito'
|
30
|
+
|
31
|
+
option :merge, "--[no-]merge", "Merge options with current file", default: true
|
32
|
+
option :force, "-f", "--force", "Overwrite file", default: false
|
33
|
+
option :hidden, "--with-hidden", "include hidden cmds/topics", default: false
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def cmd_opts(cmd, opts)
|
37
|
+
if cmd.cmd_options.size > 0 && (!cmd.hidden || options[:hidden])
|
38
|
+
opts[cmd.name] = {options: {}, args: []}
|
39
|
+
cmd.cmd_options.each do |opt|
|
40
|
+
opts[cmd.name][:options][opt[:name].to_s] = opt[:default]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def dump_options(file, merge=true, force=false)
|
46
|
+
opts = {}
|
47
|
+
Clitopic::Commands.global_commands.each do |c, cmd|
|
48
|
+
cmd_opts(cmd, opts)
|
49
|
+
end
|
50
|
+
Clitopic::Topics.topics.each do |topic_name, topic|
|
51
|
+
if topic.commands.size > 0 && (!topic.hidden || options[:hidden])
|
52
|
+
opts[topic_name] = {}
|
53
|
+
topic.commands.each do |c, cmd|
|
54
|
+
cmd_opts(cmd, opts[topic_name])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
if File.exist?(file)
|
60
|
+
if merge == false && force == false
|
61
|
+
raise ArgumentError.new("File #{file} exists, use --merge or --force")
|
62
|
+
end
|
63
|
+
if merge && !force
|
64
|
+
opts = opts.merge(YAML.load_file(file))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
puts "write: #{file}"
|
68
|
+
File.open(file, 'wb') do |file|
|
69
|
+
file.write(opts.to_yaml)
|
70
|
+
end
|
71
|
+
puts opts.to_yaml
|
72
|
+
end
|
73
|
+
|
74
|
+
def call
|
75
|
+
puts @options
|
76
|
+
if @arguments.size == 0
|
77
|
+
raise ArgumentError.new("Missing file")
|
78
|
+
end
|
79
|
+
file = @arguments[0]
|
80
|
+
dump_options(file, @options[:merge], @options[:force])
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
class ClitoVersion < Clitopic::Command::Base
|
87
|
+
register name: 'version',
|
88
|
+
description: "Display clitopic version",
|
89
|
+
hidden: true,
|
90
|
+
topic: 'clito'
|
91
|
+
|
92
|
+
class << self
|
93
|
+
def call
|
94
|
+
puts "cli-topic version: #{Clitopic::VERSION}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'clitopic/command/base'
|
2
|
+
|
3
|
+
module Clitopic
|
4
|
+
module Topic
|
5
|
+
class Help < Clitopic::Topic::Base
|
6
|
+
register name: "help", description: "topic description"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module Command
|
11
|
+
class Help2 < Clitopic::Command::Base
|
12
|
+
register name: 'help2',
|
13
|
+
description: "Display helps",
|
14
|
+
banner: "Display helps",
|
15
|
+
topic: "help"
|
16
|
+
|
17
|
+
option :all, "--all", "Display all topics with all commands"
|
18
|
+
option :topics, "--topics", "Display all availables topics"
|
19
|
+
option :topic, "--topic=TOPIC", "Display availables commands for the TOPIC"
|
20
|
+
option :with_hidden, "--with-hidden", "Include hidden commands/topics"
|
21
|
+
end
|
22
|
+
|
23
|
+
class Help < Clitopic::Command::Base
|
24
|
+
register name: 'index',
|
25
|
+
banner: "Usage: #{Clitopic.name} help [COMMAND]",
|
26
|
+
description: "list available commands or display help for a specific command",
|
27
|
+
topic: "help"
|
28
|
+
|
29
|
+
option :all, "--all", "Display all topics with all commands"
|
30
|
+
option :topics, "--topics", "Display all availables topics"
|
31
|
+
option :topic, "--topic=TOPIC", "Display availables commands for the TOPIC"
|
32
|
+
option :with_hidden, "--with-hidden", "Include hidden commands/topics"
|
33
|
+
class << self
|
34
|
+
|
35
|
+
|
36
|
+
def header(obj)
|
37
|
+
puts obj.description
|
38
|
+
puts "\n"
|
39
|
+
end
|
40
|
+
|
41
|
+
def display_globals
|
42
|
+
puts "Primary help topics, type \"#{Clitopic.name} help TOPIC\" for more details:\n\n"
|
43
|
+
Clitopic::Commands.global_commands.each do |name, cmd|
|
44
|
+
puts ("%-#{longest_global_cmd + 3}s # %s" % [ "#{name}", "#{cmd.short_description}"]).indent(2)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def display_cmd(cmd, with_header=false)
|
49
|
+
if with_header
|
50
|
+
header(cmd)
|
51
|
+
end
|
52
|
+
if cmd.hidden == false || options[:with_hidden] == true
|
53
|
+
puts cmd.help
|
54
|
+
puts "\n\n"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def longest_global_cmd
|
59
|
+
@longest_global_cmd ||= Clitopic::Commands.global_commands.keys.map{|k| k.size}.max
|
60
|
+
end
|
61
|
+
|
62
|
+
def longest_cmd
|
63
|
+
@longest_cmd ||= Clitopic::Commands.all_commands.map{|k| k.size}.max
|
64
|
+
end
|
65
|
+
|
66
|
+
def longest_topic
|
67
|
+
@longest_topic ||= Topics.topics.keys.map{|k| k.size}.max
|
68
|
+
end
|
69
|
+
|
70
|
+
def display_topic(topic_name, with_commands=false, with_header=false)
|
71
|
+
if with_commands
|
72
|
+
longest = longest_cmd
|
73
|
+
else
|
74
|
+
longest = longest_topic
|
75
|
+
end
|
76
|
+
topic = Topics[topic_name]
|
77
|
+
if with_header
|
78
|
+
header(topic)
|
79
|
+
end
|
80
|
+
if topic.hidden == false || options[:with_hidden] == true
|
81
|
+
if with_header
|
82
|
+
if !topic.commands['index'].nil?
|
83
|
+
display_cmd(topic.commands['index'])
|
84
|
+
end
|
85
|
+
else
|
86
|
+
puts ("%-#{longest + 3}s # %s" % [ "#{topic_name}", "#{topic.short_description}" ]).indent(2)
|
87
|
+
end
|
88
|
+
if with_commands
|
89
|
+
puts "Additional commands, type \"#{Clitopic.name} help COMMAND\" for more details:\n\n" if with_header
|
90
|
+
topic.commands.each do |cmd_name, cmd|
|
91
|
+
puts (" %-#{longest}s # %s" % [ "#{cmd.fullname}", "#{cmd.short_description}"]).indent(2)
|
92
|
+
end
|
93
|
+
puts ""
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def display_topic_help(topic_name)
|
99
|
+
topic = Topics[topic_name]
|
100
|
+
longest = topic.commands.keys.map{|a| "#{topic_name}:#{a}".size}.max
|
101
|
+
header(topic)
|
102
|
+
|
103
|
+
if topic.hidden == false || options[:with_hidden] == true
|
104
|
+
if !topic.commands['index'].nil?
|
105
|
+
display_cmd(topic.commands['index'], true)
|
106
|
+
end
|
107
|
+
puts "Additional commands, type \"#{Clitopic.name} help COMMAND\" for more details:\n\n"
|
108
|
+
topic.commands.each do |cmd_name, cmd|
|
109
|
+
puts ("%-#{longest}s # %s" % [ "#{cmd.fullname}", "#{cmd.short_description}"]).indent(2)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def display_topic(topic_name, with_commands=false, with_header=false)
|
115
|
+
if with_commands
|
116
|
+
longest = longest_cmd
|
117
|
+
else
|
118
|
+
longest = longest_topic
|
119
|
+
end
|
120
|
+
topic = Topics[topic_name]
|
121
|
+
header(topic) if with_header
|
122
|
+
|
123
|
+
if topic.hidden == false || options[:with_hidden] == true
|
124
|
+
puts ("%-#{longest + 3}s # %s" % [ "#{topic_name}", "#{topic.short_description}" ]).indent(2)
|
125
|
+
if with_commands
|
126
|
+
topic.commands.each do |cmd_name, cmd|
|
127
|
+
puts (" %-#{longest}s # %s" % [ "#{cmd.fullname}", "#{cmd.short_description}"]).indent(2)
|
128
|
+
end
|
129
|
+
puts ""
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def display_topics(with_commands=false)
|
135
|
+
puts "Additional topics:\n\n"
|
136
|
+
Clitopic::Topics.topics.each do |topic_name, topic|
|
137
|
+
display_topic(topic_name, with_commands)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def display_all(with_commands=false)
|
142
|
+
puts "Usage: #{Clitopic.name} COMMAND [command-specific-options]\n\n"
|
143
|
+
|
144
|
+
header(self)
|
145
|
+
display_globals
|
146
|
+
puts "\n"
|
147
|
+
display_topics(with_commands)
|
148
|
+
end
|
149
|
+
|
150
|
+
def call
|
151
|
+
if options[:all] == true
|
152
|
+
display_all(true)
|
153
|
+
elsif options.has_key?(:topic)
|
154
|
+
display_topic_help(options[:topic])
|
155
|
+
elsif arguments.size > 0
|
156
|
+
if Clitopic::Topics.topics[arguments[0]] != nil
|
157
|
+
display_topic_help(arguments[0])
|
158
|
+
else
|
159
|
+
cmd, topic = Clitopic::Commands.find_cmd(arguments[0])
|
160
|
+
if cmd.nil?
|
161
|
+
puts "Unknown command: #{arguments[0]}\n show all available commands with help --all"
|
162
|
+
else
|
163
|
+
display_cmd(cmd, true)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
else
|
167
|
+
display_all
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'clitopic/command/base'
|
2
|
+
|
3
|
+
module Clitopic
|
4
|
+
module Command
|
5
|
+
|
6
|
+
class Version < Clitopic::Command::Base
|
7
|
+
register name: 'version',
|
8
|
+
description: "show #{Clitopic.name} current version
|
9
|
+
|
10
|
+
Example:
|
11
|
+
|
12
|
+
$ #{Clitopic.name} version
|
13
|
+
#{Clitopic.version}
|
14
|
+
"
|
15
|
+
class << self
|
16
|
+
def call
|
17
|
+
puts Clitopic.version
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'clitopic/utils'
|
2
|
+
require 'clitopic/topics'
|
3
|
+
|
4
|
+
module Clitopic
|
5
|
+
module Commands
|
6
|
+
class CommandFailed < RuntimeError; end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
attr_accessor :binary, :current_cmd, :current_topic
|
10
|
+
|
11
|
+
def load_commands(dir)
|
12
|
+
Dir[File.join(dir, "*.rb")].each do |file|
|
13
|
+
require file
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def current_args
|
18
|
+
@current_args
|
19
|
+
end
|
20
|
+
|
21
|
+
def current_options
|
22
|
+
@current_options ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def global_options
|
26
|
+
@global_options ||= []
|
27
|
+
end
|
28
|
+
|
29
|
+
def global_commands
|
30
|
+
@global_commands ||= {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def global_option(name, *args, &blk)
|
34
|
+
# args.sort.reverse gives -l, --long order
|
35
|
+
global_options << Clitopic::Utils.parse_option(name, *args, &blk)
|
36
|
+
end
|
37
|
+
|
38
|
+
def invalid_arguments
|
39
|
+
@invalid_arguments
|
40
|
+
end
|
41
|
+
|
42
|
+
def shift_argument
|
43
|
+
# dup argument to get a non-frozen string
|
44
|
+
@invalid_arguments.shift.dup rescue nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_arguments!(invalid_options)
|
48
|
+
unless invalid_options.empty?
|
49
|
+
arguments = invalid_options.map {|arg| "\"#{arg}\""}
|
50
|
+
if arguments.length == 1
|
51
|
+
message = "Invalid option: #{arguments.first}"
|
52
|
+
elsif arguments.length > 1
|
53
|
+
message = "Invalid options: "
|
54
|
+
message << arguments[0...-1].join(", ")
|
55
|
+
message << " and "
|
56
|
+
message << arguments[-1]
|
57
|
+
end
|
58
|
+
$stderr.puts(Clitopic::Helpers.format_with_bang(message) + "\n\n")
|
59
|
+
run(@current_cmd.fullname, ["--help"])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def all_commands
|
64
|
+
cmds = []
|
65
|
+
Topics.topics.each do |k,topic|
|
66
|
+
topic.commands.each do |name, cmd|
|
67
|
+
if name == 'index'
|
68
|
+
cmds << topic.name
|
69
|
+
else
|
70
|
+
cmds << "#{topic.name}:#{name}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
cmds += global_commands.keys
|
75
|
+
return cmds
|
76
|
+
end
|
77
|
+
|
78
|
+
def prepare_run(cmd, arguments)
|
79
|
+
@current_options, @current_args = cmd.parse(arguments.dup)
|
80
|
+
rescue OptionParser::ParseError => e
|
81
|
+
$stderr.puts Clitopic::Helpers.format_with_bang(e.message)
|
82
|
+
run("help", [cmd.fullname])
|
83
|
+
end
|
84
|
+
|
85
|
+
def run(cmd, arguments=[])
|
86
|
+
if cmd == "-h" || cmd == "--help"
|
87
|
+
cmd = 'help'
|
88
|
+
end
|
89
|
+
@current_cmd, @current_topic = find_cmd(cmd)
|
90
|
+
if !@current_cmd
|
91
|
+
Clitopic::Helpers.error([ "`#{cmd}` is not a command.",
|
92
|
+
Clitopic::Helpers.display_suggestion(cmd, all_commands),
|
93
|
+
"See `help` for a list of available commands."
|
94
|
+
].compact.join("\n\n"))
|
95
|
+
end
|
96
|
+
prepare_run(@current_cmd, arguments)
|
97
|
+
if @current_cmd.options[:load_defaults] != true && Clitopic.load_defaults?
|
98
|
+
@current_cmd.load_defaults
|
99
|
+
end
|
100
|
+
@current_cmd.call
|
101
|
+
end
|
102
|
+
|
103
|
+
def find_cmd(command)
|
104
|
+
cmd_name, sub_cmd_name = command.split(':')
|
105
|
+
if global_commands.has_key?(command)
|
106
|
+
current_cmd = global_commands[cmd_name]
|
107
|
+
elsif !Topics[cmd_name].nil?
|
108
|
+
sub_cmd_name = 'index' if sub_cmd_name.nil?
|
109
|
+
current_topic = Topics[cmd_name]
|
110
|
+
current_cmd = current_topic.commands[sub_cmd_name]
|
111
|
+
else
|
112
|
+
current_cmd = global_commands[:help]
|
113
|
+
end
|
114
|
+
return current_cmd, current_topic
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class << self
|
119
|
+
include ClassMethods
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|