hammer_cli 0.0.12 → 0.0.13
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|