hammer_cli 0.0.12 → 0.0.13
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 +4 -4
- data/bin/hammer +5 -1
- data/lib/hammer_cli/abstract.rb +40 -4
- data/lib/hammer_cli/apipie/resource.rb +2 -2
- data/lib/hammer_cli/completer.rb +193 -0
- data/lib/hammer_cli/exception_handler.rb +17 -0
- data/lib/hammer_cli/main.rb +25 -47
- data/lib/hammer_cli/options/normalizers.rb +22 -0
- data/lib/hammer_cli/options/option_definition.rb +9 -0
- data/lib/hammer_cli/output/adapter/table.rb +42 -4
- data/lib/hammer_cli/shell.rb +104 -15
- data/lib/hammer_cli/version.rb +1 -1
- data/lib/hammer_cli.rb +1 -0
- data/test/unit/apipie/command_test.rb +7 -6
- data/test/unit/apipie/read_command_test.rb +1 -1
- data/test/unit/apipie/write_command_test.rb +3 -1
- data/test/unit/completer_test.rb +178 -0
- data/test/unit/exception_handler_test.rb +13 -0
- data/test/unit/options/option_definition_test.rb +11 -1
- data/test/unit/output/adapter/table_test.rb +19 -0
- data/test/unit/output/output_test.rb +1 -1
- metadata +65 -63
- data/lib/hammer_cli/autocompletion.rb +0 -46
data/lib/hammer_cli/shell.rb
CHANGED
@@ -3,18 +3,105 @@ require 'readline'
|
|
3
3
|
|
4
4
|
module HammerCLI
|
5
5
|
|
6
|
+
class ShellMainCommand < AbstractCommand
|
7
|
+
|
8
|
+
class HelpCommand < AbstractCommand
|
9
|
+
command_name "help"
|
10
|
+
desc "Print help for commands"
|
11
|
+
|
12
|
+
parameter "[COMMAND] ...", "command"
|
13
|
+
|
14
|
+
def execute
|
15
|
+
ShellMainCommand.run('', command_list << '-h')
|
16
|
+
HammerCLI::EX_OK
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class ExitCommand < AbstractCommand
|
21
|
+
command_name "exit"
|
22
|
+
desc "Exit interactive shell"
|
23
|
+
|
24
|
+
def execute
|
25
|
+
exit HammerCLI::EX_OK
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class AuthCommand < AbstractCommand
|
30
|
+
command_name "auth"
|
31
|
+
desc "Login and logout actions"
|
32
|
+
|
33
|
+
class LoginCommand < AbstractCommand
|
34
|
+
command_name "login"
|
35
|
+
desc "Set credentials"
|
36
|
+
|
37
|
+
def execute
|
38
|
+
context[:username] = ask_username
|
39
|
+
context[:password] = ask_password
|
40
|
+
HammerCLI::EX_OK
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class LogoutCommand < AbstractCommand
|
45
|
+
command_name "logout"
|
46
|
+
desc "Wipe your credentials"
|
47
|
+
|
48
|
+
def execute
|
49
|
+
context[:username] = nil
|
50
|
+
context[:password] = nil
|
51
|
+
|
52
|
+
if username(false)
|
53
|
+
print_message("Credentials deleted, using defaults now.")
|
54
|
+
print_message("You are logged in as [ %s ]." % username(false))
|
55
|
+
else
|
56
|
+
print_message("Credentials deleted.")
|
57
|
+
end
|
58
|
+
HammerCLI::EX_OK
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class InfoCommand < AbstractCommand
|
63
|
+
command_name "status"
|
64
|
+
desc "Information about current user"
|
65
|
+
|
66
|
+
def execute
|
67
|
+
if username(false)
|
68
|
+
print_message("You are logged in as [ %s ]." % username(false))
|
69
|
+
else
|
70
|
+
print_message("You are currently not logged in.\nUse 'auth login' to set credentials.")
|
71
|
+
end
|
72
|
+
HammerCLI::EX_OK
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
autoload_subcommands
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
def self.load_commands(main_cls)
|
81
|
+
cmds = main_cls.recognised_subcommands.select do |sub_cmd|
|
82
|
+
!(sub_cmd.subcommand_class <= HammerCLI::ShellCommand)
|
83
|
+
end
|
84
|
+
self.recognised_subcommands.push(*cmds)
|
85
|
+
end
|
86
|
+
|
87
|
+
autoload_subcommands
|
88
|
+
end
|
89
|
+
|
6
90
|
class ShellCommand < AbstractCommand
|
7
91
|
|
8
92
|
def execute
|
9
|
-
|
10
|
-
|
93
|
+
ShellMainCommand.load_commands(HammerCLI::MainCommand)
|
94
|
+
|
95
|
+
Readline.completion_append_character = ''
|
96
|
+
Readline.completer_word_break_characters = ' '
|
11
97
|
Readline.completion_proc = complete_proc
|
12
98
|
|
13
99
|
stty_save = `stty -g`.chomp
|
14
100
|
|
15
101
|
begin
|
16
|
-
|
17
|
-
|
102
|
+
print_welcome_message
|
103
|
+
while line = Readline.readline(prompt, true)
|
104
|
+
ShellMainCommand.run('', line.split, context) unless line.start_with? 'shell' or line.strip.empty?
|
18
105
|
end
|
19
106
|
rescue Interrupt => e
|
20
107
|
puts
|
@@ -25,25 +112,27 @@ module HammerCLI
|
|
25
112
|
|
26
113
|
private
|
27
114
|
|
115
|
+
def prompt
|
116
|
+
'hammer> '
|
117
|
+
end
|
118
|
+
|
119
|
+
def print_welcome_message
|
120
|
+
print_message("Welcome to the hammer interactive shell")
|
121
|
+
print_message("Type 'help' for usage information")
|
122
|
+
end
|
123
|
+
|
28
124
|
def common_prefix(results)
|
29
125
|
results.delete_if{ |r| !r[0].start_with?(results[0][0][0]) }.length == results.length
|
30
126
|
end
|
31
127
|
|
32
128
|
def complete_proc
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
# readline tries to replace current input with results
|
37
|
-
# thus we should join the results with the start of the line
|
38
|
-
if res.length == 1 || common_prefix(res)
|
39
|
-
res.map { |r| r.delete_if{ |e| e == '' }.reverse.join(' ') }
|
40
|
-
else
|
41
|
-
res.map{ |e| e[0] }
|
42
|
-
end
|
129
|
+
completer = Completer.new(ShellMainCommand)
|
130
|
+
Proc.new do |last_word|
|
131
|
+
completer.complete(Readline.line_buffer)
|
43
132
|
end
|
44
133
|
end
|
45
134
|
|
46
135
|
end
|
47
136
|
|
48
|
-
HammerCLI::MainCommand.subcommand "shell", "Interactive
|
137
|
+
HammerCLI::MainCommand.subcommand "shell", "Interactive shell", HammerCLI::ShellCommand
|
49
138
|
end
|
data/lib/hammer_cli/version.rb
CHANGED
data/lib/hammer_cli.rb
CHANGED
@@ -14,13 +14,14 @@ describe HammerCLI::Apipie::Command do
|
|
14
14
|
class CommandB < ParentCommand
|
15
15
|
end
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
class CommandC < CommandA
|
19
19
|
end
|
20
20
|
|
21
21
|
|
22
|
+
let(:ctx) { { :adapter => :silent, :interactive => false } }
|
22
23
|
let(:cmd_class) { HammerCLI::Apipie::Command.dup }
|
23
|
-
let(:cmd) { cmd_class.new("") }
|
24
|
+
let(:cmd) { cmd_class.new("", ctx) }
|
24
25
|
|
25
26
|
context "setting identifiers" do
|
26
27
|
|
@@ -88,7 +89,7 @@ describe HammerCLI::Apipie::Command do
|
|
88
89
|
|
89
90
|
it "must raise exception when no attribute is passed" do
|
90
91
|
cmd_class.identifiers :id, :name
|
91
|
-
|
92
|
+
cmd.run([]).must_equal HammerCLI::EX_USAGE
|
92
93
|
end
|
93
94
|
|
94
95
|
it "must run without error when no identifiers are declared" do
|
@@ -133,19 +134,19 @@ describe HammerCLI::Apipie::Command do
|
|
133
134
|
end
|
134
135
|
|
135
136
|
it "inherits action from a parent class" do
|
136
|
-
cmd_b = CommandA::CommandB.new("")
|
137
|
+
cmd_b = CommandA::CommandB.new("", ctx)
|
137
138
|
cmd_b.action.must_equal :show
|
138
139
|
cmd_b.class.action.must_equal :show
|
139
140
|
end
|
140
141
|
|
141
142
|
it "looks up resource in the class' modules" do
|
142
|
-
cmd_b = CommandA::CommandB.new("")
|
143
|
+
cmd_b = CommandA::CommandB.new("", ctx)
|
143
144
|
cmd_b.resource.resource_class.must_equal FakeApi::Resources::Architecture
|
144
145
|
cmd_b.class.resource.resource_class.must_equal FakeApi::Resources::Architecture
|
145
146
|
end
|
146
147
|
|
147
148
|
it "looks up resource in the superclass" do
|
148
|
-
cmd_c = CommandC.new("")
|
149
|
+
cmd_c = CommandC.new("", ctx)
|
149
150
|
cmd_c.resource.resource_class.must_equal FakeApi::Resources::Architecture
|
150
151
|
cmd_c.class.resource.resource_class.must_equal FakeApi::Resources::Architecture
|
151
152
|
end
|
@@ -5,7 +5,7 @@ require File.join(File.dirname(__FILE__), 'fake_api')
|
|
5
5
|
describe HammerCLI::Apipie::ReadCommand do
|
6
6
|
|
7
7
|
let(:cmd_class) { HammerCLI::Apipie::ReadCommand.dup }
|
8
|
-
let(:cmd) { cmd_class.new("", { :adapter => :silent }) }
|
8
|
+
let(:cmd) { cmd_class.new("", { :adapter => :silent, :interactive => false }) }
|
9
9
|
let(:cmd_run) { cmd.run([]) }
|
10
10
|
|
11
11
|
it "should raise exception when no action is defined" do
|
@@ -3,7 +3,9 @@ require File.join(File.dirname(__FILE__), 'fake_api')
|
|
3
3
|
|
4
4
|
describe HammerCLI::Apipie::WriteCommand do
|
5
5
|
|
6
|
-
|
6
|
+
|
7
|
+
let(:ctx) { { :interactive => false } }
|
8
|
+
let(:cmd) { HammerCLI::Apipie::WriteCommand.new("", ctx) }
|
7
9
|
let(:cmd_run) { cmd.run([]) }
|
8
10
|
|
9
11
|
it "should raise exception when no action is defined" do
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
|
5
|
+
describe HammerCLI::CompleterLine do
|
6
|
+
|
7
|
+
let(:unfinished_line) { "architecture list --name arch" }
|
8
|
+
let(:finished_line) { "architecture list --name arch " }
|
9
|
+
|
10
|
+
context "splitting words" do
|
11
|
+
|
12
|
+
it "should split basic line" do
|
13
|
+
line = HammerCLI::CompleterLine.new(finished_line)
|
14
|
+
line.must_equal ["architecture", "list", "--name", "arch"]
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should split basic line with space at the end" do
|
18
|
+
line = HammerCLI::CompleterLine.new(finished_line)
|
19
|
+
line.must_equal ["architecture", "list", "--name", "arch"]
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
context "last word finished" do
|
25
|
+
|
26
|
+
it "should recongize unfinished line" do
|
27
|
+
line = HammerCLI::CompleterLine.new(unfinished_line)
|
28
|
+
line.finished?.must_equal false
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should recongize finished line" do
|
32
|
+
line = HammerCLI::CompleterLine.new(finished_line)
|
33
|
+
line.finished?.must_equal true
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should recongize empty line as finished" do
|
37
|
+
line = HammerCLI::CompleterLine.new("")
|
38
|
+
line.finished?.must_equal true
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
describe HammerCLI::Completer do
|
47
|
+
|
48
|
+
|
49
|
+
class FakeMainCmd < HammerCLI::AbstractCommand
|
50
|
+
|
51
|
+
class FakeNormalizer < HammerCLI::Options::Normalizers::AbstractNormalizer
|
52
|
+
|
53
|
+
def format(val)
|
54
|
+
val
|
55
|
+
end
|
56
|
+
|
57
|
+
def complete(val)
|
58
|
+
["small ", "tall "]
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
class AnabolicCmd < HammerCLI::AbstractCommand
|
64
|
+
command_name "anabolic"
|
65
|
+
end
|
66
|
+
|
67
|
+
class ApeCmd < HammerCLI::AbstractCommand
|
68
|
+
command_name "ape"
|
69
|
+
|
70
|
+
option "--hairy", :flag, "Description"
|
71
|
+
option "--weight", "WEIGHT", "Description",
|
72
|
+
:format => FakeNormalizer.new
|
73
|
+
option "--height", "HEIGHT", "Description",
|
74
|
+
:format => FakeNormalizer.new
|
75
|
+
|
76
|
+
class MakkakCmd < HammerCLI::AbstractCommand
|
77
|
+
command_name "makkak"
|
78
|
+
end
|
79
|
+
|
80
|
+
class MalpaCmd < HammerCLI::AbstractCommand
|
81
|
+
command_name "malpa"
|
82
|
+
end
|
83
|
+
|
84
|
+
class OrangutanCmd < HammerCLI::AbstractCommand
|
85
|
+
command_name "orangutan"
|
86
|
+
end
|
87
|
+
|
88
|
+
autoload_subcommands
|
89
|
+
end
|
90
|
+
|
91
|
+
class ApocalypseCmd < HammerCLI::AbstractCommand
|
92
|
+
command_name "apocalypse"
|
93
|
+
end
|
94
|
+
|
95
|
+
class BeastCmd < HammerCLI::AbstractCommand
|
96
|
+
command_name "beast"
|
97
|
+
end
|
98
|
+
|
99
|
+
autoload_subcommands
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
let(:completer) { HammerCLI::Completer.new(FakeMainCmd) }
|
104
|
+
|
105
|
+
context "command completion" do
|
106
|
+
it "should offer all available commands" do
|
107
|
+
completer.complete("").sort.must_equal ["anabolic ", "ape ", "apocalypse ", "beast ", "-h ", "--help "].sort
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should offer nothing when the line does not match" do
|
111
|
+
completer.complete("x").must_equal []
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should filter by first letter" do
|
115
|
+
completer.complete("a").sort.must_equal ["anabolic ", "ape ", "apocalypse "].sort
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should filter by first two letters" do
|
119
|
+
completer.complete("ap").sort.must_equal ["ape ", "apocalypse "].sort
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should offer all available subcommands and options" do
|
123
|
+
completer.complete("ape ").sort.must_equal ["makkak ", "malpa ", "orangutan ", "--hairy ", "--weight ", "--height ", "-h ", "--help "].sort
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should offer all available subcommands and options even if a flag has been passed" do
|
127
|
+
completer.complete("ape --hairy ").sort.must_equal ["makkak ", "malpa ", "orangutan ", "--hairy ", "--weight ", "--height ", "-h ", "--help "].sort
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should offer all available subcommands and options even if an option has been passed" do
|
131
|
+
completer.complete("ape --weight 12kg ").sort.must_equal ["makkak ", "malpa ", "orangutan ", "--hairy ", "--weight ", "--height ", "-h ", "--help "].sort
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should offer all available subcommands and options even if an egual sign option has been passed" do
|
135
|
+
completer.complete("ape --weight=12kg ").sort.must_equal ["makkak ", "malpa ", "orangutan ", "--hairy ", "--weight ", "--height ", "-h ", "--help "].sort
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
context "option value completion" do
|
141
|
+
it "should complete option values" do
|
142
|
+
completer.complete("ape --height ").sort.must_equal ["small ", "tall "].sort
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should complete option values" do
|
146
|
+
completer.complete("ape --height s").must_equal ["small "]
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
context "subcommand completion" do
|
152
|
+
it "should filter subcommands by first letter" do
|
153
|
+
completer.complete("ape m").sort.must_equal ["makkak ", "malpa "].sort
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should offer nothing when the line does not match any subcommand" do
|
157
|
+
completer.complete("ape x").must_equal []
|
158
|
+
end
|
159
|
+
|
160
|
+
it "should ignore flags specified before the last command" do
|
161
|
+
completer.complete("ape --hairy m").sort.must_equal ["makkak ", "malpa "].sort
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should ignore options specified before the last command" do
|
165
|
+
completer.complete("ape --weight 12kg m").sort.must_equal ["makkak ", "malpa "].sort
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should ignore equal sign separated options specified before the last command" do
|
169
|
+
completer.complete("ape --weight=12kg m").sort.must_equal ["makkak ", "malpa "].sort
|
170
|
+
end
|
171
|
+
|
172
|
+
it "should filter subcommands by first three letters" do
|
173
|
+
completer.complete("ape mak").must_equal ["makkak "]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
|
@@ -11,6 +11,7 @@ describe HammerCLI::ExceptionHandler do
|
|
11
11
|
let(:output) { HammerCLI::Output::Output.new }
|
12
12
|
let(:handler) { HammerCLI::ExceptionHandler.new(:output => output)}
|
13
13
|
let(:heading) { "Something went wrong" }
|
14
|
+
let(:cmd) { Class.new(HammerCLI::AbstractCommand).new("command_name") }
|
14
15
|
|
15
16
|
it "should handle unauthorized" do
|
16
17
|
output.expects(:print_error).with(heading, "Invalid username or password")
|
@@ -28,6 +29,18 @@ describe HammerCLI::ExceptionHandler do
|
|
28
29
|
handler.handle_exception(MyException.new('message'), :heading => heading)
|
29
30
|
end
|
30
31
|
|
32
|
+
it "should handle help request" do
|
33
|
+
output.expects(:print_message).with(cmd.help)
|
34
|
+
handler.handle_exception(Clamp::HelpWanted.new(cmd), :heading => heading)
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should handle usage error" do
|
39
|
+
output.expects(:print_error).with(heading, "Error: wrong_usage\n\nSee: 'command_name --help'")
|
40
|
+
handler.handle_exception(Clamp::UsageError.new('wrong_usage', cmd), :heading => heading)
|
41
|
+
|
42
|
+
end
|
43
|
+
|
31
44
|
it "should handle resource not found" do
|
32
45
|
ex = RestClient::ResourceNotFound.new
|
33
46
|
output.expects(:print_error).with(heading, ex.message)
|
@@ -14,6 +14,8 @@ describe HammerCLI::Options::OptionDefinition do
|
|
14
14
|
option "--test-format", "TEST_FORMAT", "Test option with a formatter",
|
15
15
|
:format => FakeFormatter.new,
|
16
16
|
:default => "A"
|
17
|
+
option "--test-context", "CONTEXT", "Option saved into context",
|
18
|
+
:context_target => :test_option
|
17
19
|
end
|
18
20
|
|
19
21
|
describe "formatters" do
|
@@ -30,7 +32,7 @@ describe HammerCLI::Options::OptionDefinition do
|
|
30
32
|
|
31
33
|
opt_instance = opt.of(TestOptionFormattersCmd.new([]))
|
32
34
|
# clamp api changed in 0.6.2
|
33
|
-
if opt_instance.respond_to? :write
|
35
|
+
if opt_instance.respond_to? :write
|
34
36
|
opt_instance.write('B')
|
35
37
|
else
|
36
38
|
opt_instance.take('B')
|
@@ -39,5 +41,13 @@ describe HammerCLI::Options::OptionDefinition do
|
|
39
41
|
end
|
40
42
|
end
|
41
43
|
|
44
|
+
describe "context" do
|
45
|
+
it "should save option to context" do
|
46
|
+
context = {}
|
47
|
+
cmd = TestOptionFormattersCmd.new("", context)
|
48
|
+
cmd.run(["--test-context=VALUE"])
|
49
|
+
context[:test_option].must_equal "VALUE"
|
50
|
+
end
|
51
|
+
end
|
42
52
|
end
|
43
53
|
|
@@ -53,6 +53,25 @@ describe HammerCLI::Output::Adapter::Table do
|
|
53
53
|
out.must_match(/.*-DOT-.*/)
|
54
54
|
end
|
55
55
|
end
|
56
|
+
|
57
|
+
context "sort_columns" do
|
58
|
+
let(:field_firstname) { Fields::DataField.new(:path => [:firstname], :label => "Firstname") }
|
59
|
+
let(:field_lastname) { Fields::DataField.new(:path => [:lastname], :label => "Lastname") }
|
60
|
+
let(:fields) {
|
61
|
+
[field_firstname, field_lastname]
|
62
|
+
}
|
63
|
+
let(:data) { HammerCLI::Output::RecordCollection.new [{
|
64
|
+
:firstname => "John",
|
65
|
+
:lastname => "Doe"
|
66
|
+
}]}
|
67
|
+
|
68
|
+
it "should sort output" do
|
69
|
+
TablePrint::Printer.any_instance.stubs(:table_print).returns(
|
70
|
+
"LASTNAME | FIRSTNAME\n---------|----------\nDoe | John \n")
|
71
|
+
proc { adapter.print_collection(fields, data) }.must_output(
|
72
|
+
"----------|---------\nFIRSTNAME | LASTNAME\n----------|---------\nJohn | Doe \n----------|---------\n")
|
73
|
+
end
|
74
|
+
end
|
56
75
|
end
|
57
76
|
|
58
77
|
end
|
@@ -5,7 +5,7 @@ describe HammerCLI::Output::Output do
|
|
5
5
|
let(:adapter) { HammerCLI::Output::Adapter::Silent }
|
6
6
|
let(:definition) { HammerCLI::Output::Definition.new }
|
7
7
|
|
8
|
-
let(:context) { { :adapter => :silent } }
|
8
|
+
let(:context) { { :adapter => :silent, :interactive => false } }
|
9
9
|
let(:out_class) { HammerCLI::Output::Output }
|
10
10
|
let(:out) { out_class.new(context) }
|
11
11
|
|