| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Net::CLI::Interact::Phrasebook; |
|
2
|
|
|
|
|
|
|
{ $Net::CLI::Interact::Phrasebook::VERSION = '2.300005' } |
|
3
|
|
|
|
|
|
|
|
|
4
|
1
|
|
|
1
|
|
8
|
use Moo; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
8
|
|
|
5
|
1
|
|
|
1
|
|
333
|
use MooX::Types::MooseLike::Base qw(InstanceOf Str Any HashRef); |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
87
|
|
|
6
|
|
|
|
|
|
|
|
|
7
|
1
|
|
|
1
|
|
490
|
use Path::Class; |
|
|
1
|
|
|
|
|
38350
|
|
|
|
1
|
|
|
|
|
68
|
|
|
8
|
1
|
|
|
1
|
|
545
|
use File::ShareDir 'dist_dir'; |
|
|
1
|
|
|
|
|
21554
|
|
|
|
1
|
|
|
|
|
69
|
|
|
9
|
1
|
|
|
1
|
|
500
|
use Net::CLI::Interact::ActionSet; |
|
|
1
|
|
|
|
|
4
|
|
|
|
1
|
|
|
|
|
2177
|
|
|
10
|
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
has 'logger' => ( |
|
12
|
|
|
|
|
|
|
is => 'ro', |
|
13
|
|
|
|
|
|
|
isa => InstanceOf['Net::CLI::Interact::Logger'], |
|
14
|
|
|
|
|
|
|
required => 1, |
|
15
|
|
|
|
|
|
|
); |
|
16
|
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
has 'personality' => ( |
|
18
|
|
|
|
|
|
|
is => 'rw', |
|
19
|
|
|
|
|
|
|
isa => Str, |
|
20
|
|
|
|
|
|
|
required => 1, |
|
21
|
|
|
|
|
|
|
); |
|
22
|
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
has 'library' => ( |
|
24
|
|
|
|
|
|
|
is => 'lazy', |
|
25
|
|
|
|
|
|
|
isa => Any, # FIXME 'Str|ArrayRef[Str]', |
|
26
|
|
|
|
|
|
|
); |
|
27
|
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
sub _build_library { |
|
29
|
0
|
|
|
0
|
|
|
return [ Path::Class::Dir->new( dist_dir('Net-CLI-Interact') ) |
|
30
|
|
|
|
|
|
|
->subdir('phrasebook')->stringify ]; |
|
31
|
|
|
|
|
|
|
} |
|
32
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
has 'add_library' => ( |
|
34
|
|
|
|
|
|
|
is => 'rw', |
|
35
|
|
|
|
|
|
|
isa => Any, # FIXME 'Str|ArrayRef[Str]', |
|
36
|
|
|
|
|
|
|
default => sub { [] }, |
|
37
|
|
|
|
|
|
|
); |
|
38
|
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
has '_prompt' => ( |
|
40
|
|
|
|
|
|
|
is => 'ro', |
|
41
|
|
|
|
|
|
|
isa => HashRef[InstanceOf['Net::CLI::Interact::ActionSet']], |
|
42
|
|
|
|
|
|
|
default => sub { {} }, |
|
43
|
|
|
|
|
|
|
); |
|
44
|
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
sub prompt { |
|
46
|
0
|
|
|
0
|
1
|
|
my ($self, $name) = @_; |
|
47
|
0
|
0
|
|
|
|
|
die "unknown prompt [$name]" unless $self->has_prompt($name); |
|
48
|
0
|
|
|
|
|
|
return $self->_prompt->{$name}; |
|
49
|
|
|
|
|
|
|
} |
|
50
|
|
|
|
|
|
|
|
|
51
|
0
|
|
|
0
|
1
|
|
sub prompt_names { return keys %{ (shift)->_prompt } } |
|
|
0
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
sub has_prompt { |
|
54
|
0
|
|
|
0
|
1
|
|
my ($self, $name) = @_; |
|
55
|
0
|
0
|
0
|
|
|
|
die "missing prompt name!" |
|
56
|
|
|
|
|
|
|
unless defined $name and length $name; |
|
57
|
0
|
|
|
|
|
|
return exists $self->_prompt->{$name}; |
|
58
|
|
|
|
|
|
|
} |
|
59
|
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
has '_macro' => ( |
|
61
|
|
|
|
|
|
|
is => 'ro', |
|
62
|
|
|
|
|
|
|
isa => HashRef[InstanceOf['Net::CLI::Interact::ActionSet']], |
|
63
|
|
|
|
|
|
|
default => sub { {} }, |
|
64
|
|
|
|
|
|
|
); |
|
65
|
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
sub macro { |
|
67
|
0
|
|
|
0
|
1
|
|
my ($self, $name) = @_; |
|
68
|
0
|
0
|
|
|
|
|
die "unknown macro [$name]" unless $self->has_macro($name); |
|
69
|
0
|
|
|
|
|
|
return $self->_macro->{$name}; |
|
70
|
|
|
|
|
|
|
} |
|
71
|
|
|
|
|
|
|
|
|
72
|
0
|
|
|
0
|
1
|
|
sub macro_names { return keys %{ (shift)->_macro } } |
|
|
0
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
sub has_macro { |
|
75
|
0
|
|
|
0
|
1
|
|
my ($self, $name) = @_; |
|
76
|
0
|
0
|
0
|
|
|
|
die "missing macro name!" |
|
77
|
|
|
|
|
|
|
unless defined $name and length $name; |
|
78
|
0
|
|
|
|
|
|
return exists $self->_macro->{$name}; |
|
79
|
|
|
|
|
|
|
} |
|
80
|
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
# matches which are prompt names are resolved to RegexpRefs |
|
82
|
|
|
|
|
|
|
# and regexp provided by the user are inflated into RegexpRefs |
|
83
|
|
|
|
|
|
|
sub _resolve_matches { |
|
84
|
0
|
|
|
0
|
|
|
my ($self, $actions) = @_; |
|
85
|
|
|
|
|
|
|
|
|
86
|
0
|
|
|
|
|
|
foreach my $a (@$actions) { |
|
87
|
0
|
0
|
|
|
|
|
next unless $a->{type} eq 'match'; |
|
88
|
0
|
0
|
|
|
|
|
next unless ref $a->{value} eq ref []; |
|
89
|
|
|
|
|
|
|
|
|
90
|
0
|
|
|
|
|
|
my @newvals = (); |
|
91
|
0
|
|
|
|
|
|
foreach my $v (@{ $a->{value} }) { |
|
|
0
|
|
|
|
|
|
|
|
92
|
0
|
0
|
0
|
|
|
|
if ($v =~ m{^/} and $v =~ m{/$}) { |
|
93
|
0
|
|
|
|
|
|
$v =~ s{^/}{}; $v =~ s{/$}{}; |
|
|
0
|
|
|
|
|
|
|
|
94
|
0
|
|
|
|
|
|
push @newvals, qr/$v/; |
|
95
|
|
|
|
|
|
|
} |
|
96
|
|
|
|
|
|
|
else { |
|
97
|
0
|
|
|
|
|
|
push @newvals, @{ $self->prompt($v)->first->value }; |
|
|
0
|
|
|
|
|
|
|
|
98
|
|
|
|
|
|
|
} |
|
99
|
|
|
|
|
|
|
} |
|
100
|
|
|
|
|
|
|
|
|
101
|
0
|
|
|
|
|
|
$a->{value} = \@newvals; |
|
102
|
|
|
|
|
|
|
} |
|
103
|
|
|
|
|
|
|
|
|
104
|
0
|
|
|
|
|
|
return $actions; |
|
105
|
|
|
|
|
|
|
} |
|
106
|
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
# inflate the hashref into action objects |
|
108
|
|
|
|
|
|
|
sub _bake { |
|
109
|
0
|
|
|
0
|
|
|
my ($self, $data) = @_; |
|
110
|
|
|
|
|
|
|
|
|
111
|
0
|
0
|
0
|
|
|
|
return unless ref $data eq ref {} and keys %$data; |
|
112
|
0
|
|
|
|
|
|
$self->logger->log('phrasebook', 'debug', 'storing', $data->{type}, $data->{name}); |
|
113
|
|
|
|
|
|
|
|
|
114
|
0
|
|
|
|
|
|
my $slot = '_'. lc $data->{type}; |
|
115
|
|
|
|
|
|
|
$self->$slot->{$data->{name}} |
|
116
|
|
|
|
|
|
|
= Net::CLI::Interact::ActionSet->new({ |
|
117
|
|
|
|
|
|
|
actions => $self->_resolve_matches($data->{actions}) |
|
118
|
0
|
|
|
|
|
|
}); |
|
119
|
|
|
|
|
|
|
} |
|
120
|
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
sub BUILD { |
|
122
|
0
|
|
|
0
|
0
|
|
my $self = shift; |
|
123
|
0
|
|
|
|
|
|
$self->load_phrasebooks; |
|
124
|
|
|
|
|
|
|
} |
|
125
|
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
# parse phrasebook files and load action objects |
|
127
|
|
|
|
|
|
|
sub load_phrasebooks { |
|
128
|
0
|
|
|
0
|
0
|
|
my $self = shift; |
|
129
|
0
|
|
|
|
|
|
my $data = {}; |
|
130
|
0
|
|
|
|
|
|
my $stash = { prompt => [], macro => [] }; |
|
131
|
|
|
|
|
|
|
|
|
132
|
0
|
|
|
|
|
|
foreach my $file ($self->_find_phrasebooks) { |
|
133
|
0
|
|
|
|
|
|
$self->logger->log('phrasebook', 'info', 'reading phrasebook', $file); |
|
134
|
0
|
|
|
|
|
|
my @lines = $file->slurp; |
|
135
|
0
|
|
|
|
|
|
while ($_ = shift @lines) { |
|
136
|
|
|
|
|
|
|
# Skip comments and empty lines |
|
137
|
0
|
0
|
|
|
|
|
next if m/^(?:#|\s*$)/; |
|
138
|
|
|
|
|
|
|
|
|
139
|
0
|
0
|
|
|
|
|
if (m{^(prompt|macro)\s+(\w+)\s*$}) { |
|
|
|
0
|
|
|
|
|
|
|
140
|
0
|
0
|
|
|
|
|
if (scalar keys %$data) { |
|
141
|
0
|
|
|
|
|
|
push @{ $stash->{$data->{type}} }, $data; |
|
|
0
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
} |
|
143
|
0
|
|
|
|
|
|
$data = {type => $1, name => $2}; |
|
144
|
0
|
|
|
|
|
|
next; |
|
145
|
|
|
|
|
|
|
} |
|
146
|
|
|
|
|
|
|
# skip new sections we don't yet understand |
|
147
|
|
|
|
|
|
|
elsif (m{^\w}) { |
|
148
|
0
|
|
|
|
|
|
$_ = shift @lines until m{^(?:prompt|macro)}; |
|
149
|
0
|
|
|
|
|
|
unshift @lines, $_; |
|
150
|
0
|
|
|
|
|
|
next; |
|
151
|
|
|
|
|
|
|
} |
|
152
|
|
|
|
|
|
|
|
|
153
|
0
|
0
|
|
|
|
|
if (m{^\s+send\s+(.+)$}) { |
|
154
|
0
|
|
|
|
|
|
my $value = $1; |
|
155
|
0
|
|
|
|
|
|
$value =~ s/^["']//; $value =~ s/["']$//; |
|
|
0
|
|
|
|
|
|
|
|
156
|
0
|
|
|
|
|
|
push @{ $data->{actions} }, { |
|
|
0
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
type => 'send', value => $value, |
|
158
|
|
|
|
|
|
|
}; |
|
159
|
0
|
|
|
|
|
|
next; |
|
160
|
|
|
|
|
|
|
} |
|
161
|
|
|
|
|
|
|
|
|
162
|
0
|
0
|
|
|
|
|
if (m{^\s+put\s+(.+)$}) { |
|
163
|
0
|
|
|
|
|
|
my $value = $1; |
|
164
|
0
|
|
|
|
|
|
$value =~ s/^["']//; $value =~ s/["']$//; |
|
|
0
|
|
|
|
|
|
|
|
165
|
0
|
|
|
|
|
|
push @{ $data->{actions} }, { |
|
|
0
|
|
|
|
|
|
|
|
166
|
|
|
|
|
|
|
type => 'send', value => $value, no_ors => 1, |
|
167
|
|
|
|
|
|
|
}; |
|
168
|
0
|
|
|
|
|
|
next; |
|
169
|
|
|
|
|
|
|
} |
|
170
|
|
|
|
|
|
|
|
|
171
|
0
|
0
|
|
|
|
|
if (m{^\s+match\s+(.+)\s*$}) { |
|
172
|
0
|
|
|
|
|
|
my @vals = split m/\s+or\s+/, $1; |
|
173
|
0
|
0
|
|
|
|
|
if (scalar @vals) { |
|
174
|
0
|
|
|
|
|
|
push @{ $data->{actions} }, |
|
|
0
|
|
|
|
|
|
|
|
175
|
|
|
|
|
|
|
{type => 'match', value => \@vals}; |
|
176
|
0
|
|
|
|
|
|
next; |
|
177
|
|
|
|
|
|
|
} |
|
178
|
|
|
|
|
|
|
} |
|
179
|
|
|
|
|
|
|
|
|
180
|
0
|
0
|
|
|
|
|
if (m{^\s+follow\s+/(.+)/\s+with\s+(.+)\s*$}) { |
|
181
|
0
|
|
|
|
|
|
my ($match, $send) = ($1, $2); |
|
182
|
0
|
|
|
|
|
|
$send =~ s/^["']//; $send =~ s/["']$//; |
|
|
0
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
$data->{actions}->[-1]->{continuation} = [ |
|
184
|
0
|
|
|
|
|
|
{type => 'match', value => [qr/$match/]}, |
|
185
|
|
|
|
|
|
|
{type => 'send', value => eval "qq{$send}", no_ors => 1} |
|
186
|
|
|
|
|
|
|
]; |
|
187
|
0
|
|
|
|
|
|
next; |
|
188
|
|
|
|
|
|
|
} |
|
189
|
|
|
|
|
|
|
|
|
190
|
0
|
|
|
|
|
|
die "don't know what to do with this phrasebook line:\n", $_; |
|
191
|
|
|
|
|
|
|
} |
|
192
|
|
|
|
|
|
|
# last entry in the file needs baking |
|
193
|
0
|
|
|
|
|
|
push @{ $stash->{$data->{type}} }, $data; |
|
|
0
|
|
|
|
|
|
|
|
194
|
0
|
|
|
|
|
|
$data = {}; |
|
195
|
|
|
|
|
|
|
} |
|
196
|
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
# bake the prompts before the macros, to allow macros to reference |
|
198
|
|
|
|
|
|
|
# prompts which appear later in the same file. |
|
199
|
0
|
|
|
|
|
|
foreach my $t (qw/prompt macro/) { |
|
200
|
0
|
|
|
|
|
|
foreach my $d (@{ $stash->{$t} }) { |
|
|
0
|
|
|
|
|
|
|
|
201
|
0
|
|
|
|
|
|
$self->_bake($d); |
|
202
|
|
|
|
|
|
|
} |
|
203
|
|
|
|
|
|
|
} |
|
204
|
|
|
|
|
|
|
} |
|
205
|
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
# finds the path of Phrasebooks within the Library leading to Personality |
|
207
|
|
|
|
|
|
|
sub _find_phrasebooks { |
|
208
|
0
|
|
|
0
|
|
|
my $self = shift; |
|
209
|
0
|
0
|
|
|
|
|
my @libs = (ref $self->library ? @{$self->library} : ($self->library)); |
|
|
0
|
|
|
|
|
|
|
|
210
|
0
|
0
|
|
|
|
|
my @alib = (ref $self->add_library ? @{$self->add_library} : ($self->add_library)); |
|
|
0
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
# first find the (relative) path for the requested personality |
|
213
|
|
|
|
|
|
|
# then within each of @libs gather the files along that path |
|
214
|
|
|
|
|
|
|
|
|
215
|
0
|
|
|
|
|
|
my $target = $self->_find_personality_in( @libs, @alib ); |
|
216
|
0
|
0
|
|
|
|
|
die (sprintf "error: unknown personality: '%s'\n", |
|
217
|
|
|
|
|
|
|
$self->personality) unless $target; |
|
218
|
|
|
|
|
|
|
|
|
219
|
0
|
|
|
|
|
|
my @files = $self->_gather_pb_from( $target, @libs, @alib ); |
|
220
|
0
|
0
|
|
|
|
|
die (sprintf "error: personality '%s' contains no phrasebook files!\n", |
|
221
|
|
|
|
|
|
|
$self->personality) unless scalar @files; |
|
222
|
|
|
|
|
|
|
|
|
223
|
0
|
|
|
|
|
|
return @files; |
|
224
|
|
|
|
|
|
|
} |
|
225
|
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
sub _find_personality_in { |
|
227
|
0
|
|
|
0
|
|
|
my ($self, @libs) = @_; |
|
228
|
0
|
|
|
|
|
|
my $target = undef; |
|
229
|
|
|
|
|
|
|
|
|
230
|
0
|
|
|
|
|
|
foreach my $lib (@libs) { |
|
231
|
|
|
|
|
|
|
Path::Class::Dir->new($lib)->recurse(callback => sub { |
|
232
|
0
|
0
|
|
0
|
|
|
return unless $_[0]->is_dir; |
|
233
|
0
|
0
|
|
|
|
|
$target = Path::Class::Dir->new($_[0])->relative($lib) |
|
234
|
|
|
|
|
|
|
if $_[0]->dir_list(-1) eq $self->personality |
|
235
|
0
|
|
|
|
|
|
}); |
|
236
|
0
|
0
|
|
|
|
|
last if defined $target; |
|
237
|
|
|
|
|
|
|
} |
|
238
|
0
|
|
|
|
|
|
return $target; |
|
239
|
|
|
|
|
|
|
} |
|
240
|
|
|
|
|
|
|
|
|
241
|
|
|
|
|
|
|
sub _gather_pb_from { |
|
242
|
0
|
|
|
0
|
|
|
my ($self, $target, @libs) = @_; |
|
243
|
0
|
|
|
|
|
|
my @files = (); |
|
244
|
|
|
|
|
|
|
|
|
245
|
0
|
0
|
0
|
|
|
|
return () unless $target->isa('Path::Class::Dir') and $target->is_relative; |
|
246
|
|
|
|
|
|
|
|
|
247
|
0
|
|
|
|
|
|
foreach my $lib (@libs) { |
|
248
|
0
|
|
|
|
|
|
my $root = Path::Class::Dir->new($lib); |
|
249
|
|
|
|
|
|
|
|
|
250
|
0
|
|
|
|
|
|
foreach my $part ($target->dir_list) { |
|
251
|
0
|
|
|
|
|
|
$root = $root->subdir($part); |
|
252
|
|
|
|
|
|
|
# $self->logger->log('phrasebook', 'debug', sprintf 'searching in [%s]', $root); |
|
253
|
0
|
0
|
|
|
|
|
last if not -d $root->stringify; |
|
254
|
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
push @files, |
|
256
|
0
|
|
|
|
|
|
sort {$a->basename cmp $b->basename} |
|
257
|
0
|
|
|
|
|
|
grep { not $_->is_dir } $root->children(no_hidden => 1); |
|
|
0
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
} |
|
259
|
|
|
|
|
|
|
} |
|
260
|
0
|
|
|
|
|
|
return @files; |
|
261
|
|
|
|
|
|
|
} |
|
262
|
|
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
1; |
|
264
|
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
=pod |
|
266
|
|
|
|
|
|
|
|
|
267
|
|
|
|
|
|
|
=head1 NAME |
|
268
|
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
Net::CLI::Interact::Phrasebook - Load command phrasebooks from a Library |
|
270
|
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
=head1 DESCRIPTION |
|
272
|
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
A command phrasebook is where you store the repeatable sequences of commands |
|
274
|
|
|
|
|
|
|
which can be sent to connected network devices. An example would be a command |
|
275
|
|
|
|
|
|
|
to show the configuration of a device: storing this in a phrasebook (sometimes |
|
276
|
|
|
|
|
|
|
known as a dictionary) saves time and effort. |
|
277
|
|
|
|
|
|
|
|
|
278
|
|
|
|
|
|
|
This module implements the loading and preparing of phrasebooks from an |
|
279
|
|
|
|
|
|
|
on-disk file-based hierarchical library, and makes them available to the |
|
280
|
|
|
|
|
|
|
application as smart objects for use in L<Net::CLI::Interact> sessions. |
|
281
|
|
|
|
|
|
|
Entries in the phrasebook will be one of the following types: |
|
282
|
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
=over 4 |
|
284
|
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
=item Prompt |
|
286
|
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
Named regular expressions that match the content of a single line of text in |
|
288
|
|
|
|
|
|
|
the output returned from a connected device. They are a demarcation between |
|
289
|
|
|
|
|
|
|
commands sent and responses returned. |
|
290
|
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
=item Macro |
|
292
|
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
Alternating sequences of command statements sent to the device, and regular |
|
294
|
|
|
|
|
|
|
expressions to match the response. There are different kinds of Macro, |
|
295
|
|
|
|
|
|
|
explained below. |
|
296
|
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
=back |
|
298
|
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
The named regular expressions used in Prompts and Macros are known as I<Match> |
|
300
|
|
|
|
|
|
|
statements. The command statements in Macros which are sent to the device are |
|
301
|
|
|
|
|
|
|
known as I<Send> statements. That is, Prompts and Macros are built from one or |
|
302
|
|
|
|
|
|
|
more Match and Send statements. |
|
303
|
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
Each Send or Match statement becomes an instance of the |
|
305
|
|
|
|
|
|
|
L<Net::CLI::Interact::Action> class. These are built up into Prompts and |
|
306
|
|
|
|
|
|
|
Macros, which become instances of the L<Net::CLI::Interact::ActionSet> class. |
|
307
|
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
=head1 USAGE |
|
309
|
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
A phrasebook is a plain text file containing named Prompts or Macros. Each |
|
311
|
|
|
|
|
|
|
file exists in a directory hierarchy, such that files "deeper" in the |
|
312
|
|
|
|
|
|
|
hierarchy have their entries override the similarly named entries higher up. |
|
313
|
|
|
|
|
|
|
For example: |
|
314
|
|
|
|
|
|
|
|
|
315
|
|
|
|
|
|
|
/dir1/file1 |
|
316
|
|
|
|
|
|
|
/dir1/file2 |
|
317
|
|
|
|
|
|
|
/dir1/dir2/file3 |
|
318
|
|
|
|
|
|
|
|
|
319
|
|
|
|
|
|
|
Entries in C<file3> sharing a name with any entries from C<file1> or C<file2> |
|
320
|
|
|
|
|
|
|
will take precedence. Those in C<file2> will also override entries in |
|
321
|
|
|
|
|
|
|
C<file1>, because asciibetical sorting places the files in that order, and |
|
322
|
|
|
|
|
|
|
later definitions with the same name and type override earlier ones. |
|
323
|
|
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
When this module is loaded, a I<personality> key is required. This locates a |
|
325
|
|
|
|
|
|
|
directory on disk, and then the files in that directory and all its ancestors |
|
326
|
|
|
|
|
|
|
in the hierarchy are loaded. The directories to search are specified by two |
|
327
|
|
|
|
|
|
|
I<Library> options (see below). All phrasebooks matching the given |
|
328
|
|
|
|
|
|
|
I<personality> are loaded, allowing a user to override or augment the default, |
|
329
|
|
|
|
|
|
|
shipped phrasebooks. |
|
330
|
|
|
|
|
|
|
|
|
331
|
|
|
|
|
|
|
=head1 INTERFACE |
|
332
|
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
=head2 new( \%options ) |
|
334
|
|
|
|
|
|
|
|
|
335
|
|
|
|
|
|
|
This takes the following options, and returns a loaded phrasebook object: |
|
336
|
|
|
|
|
|
|
|
|
337
|
|
|
|
|
|
|
=over 4 |
|
338
|
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
=item C<< personality => $directory >> (required) |
|
340
|
|
|
|
|
|
|
|
|
341
|
|
|
|
|
|
|
The name of a directory component on disk. Any files higher in the libraries |
|
342
|
|
|
|
|
|
|
hierarchy are also loaded, but entries in files contained within this |
|
343
|
|
|
|
|
|
|
directory, or "closer" to it, will take precedence. |
|
344
|
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
=item C<< library => $directory | \@directories >> |
|
346
|
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
First library hierarchy, specified either as a single directory or a list of |
|
348
|
|
|
|
|
|
|
directories that are searched in order. The idea is that this option be set in |
|
349
|
|
|
|
|
|
|
your application code, perhaps specifying some directory of phrasebooks |
|
350
|
|
|
|
|
|
|
shipped with the distribution. |
|
351
|
|
|
|
|
|
|
|
|
352
|
|
|
|
|
|
|
=item C<< add_library => $directory | \@directories >> |
|
353
|
|
|
|
|
|
|
|
|
354
|
|
|
|
|
|
|
Second library hierarchy, specified either as a single directory or a list of |
|
355
|
|
|
|
|
|
|
directories that are searched in order. This parameter is for the end-user to |
|
356
|
|
|
|
|
|
|
provide the location(s) of their own phrasebook(s). Any entries found via this |
|
357
|
|
|
|
|
|
|
path will override those found via the first C<library> path. |
|
358
|
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
=back |
|
360
|
|
|
|
|
|
|
|
|
361
|
|
|
|
|
|
|
=head2 prompt( $name ) |
|
362
|
|
|
|
|
|
|
|
|
363
|
|
|
|
|
|
|
Returns the Prompt associated to the given C<$name>, or throws an exception if |
|
364
|
|
|
|
|
|
|
no such prompt can be found. The returned object is an instance of |
|
365
|
|
|
|
|
|
|
L<Net::CLI::Interact::ActionSet>. |
|
366
|
|
|
|
|
|
|
|
|
367
|
|
|
|
|
|
|
=head2 has_prompt( $name ) |
|
368
|
|
|
|
|
|
|
|
|
369
|
|
|
|
|
|
|
Returns true if a prompt of the given C<$name> exists in the loaded phrasebooks. |
|
370
|
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
=head2 prompt_names |
|
372
|
|
|
|
|
|
|
|
|
373
|
|
|
|
|
|
|
Returns a list of the names of the current loaded Prompts. |
|
374
|
|
|
|
|
|
|
|
|
375
|
|
|
|
|
|
|
=head2 macro( $name ) |
|
376
|
|
|
|
|
|
|
|
|
377
|
|
|
|
|
|
|
Returns the Macro associated to the given C<$name>, or throws an exception if |
|
378
|
|
|
|
|
|
|
no such macro can be found. The returned object is an instance of |
|
379
|
|
|
|
|
|
|
L<Net::CLI::Interact::ActionSet>. |
|
380
|
|
|
|
|
|
|
|
|
381
|
|
|
|
|
|
|
=head2 has_macro( $name ) |
|
382
|
|
|
|
|
|
|
|
|
383
|
|
|
|
|
|
|
Returns true if a macro of the given C<$name> exists in the loaded phrasebooks. |
|
384
|
|
|
|
|
|
|
|
|
385
|
|
|
|
|
|
|
=head2 macro_names |
|
386
|
|
|
|
|
|
|
|
|
387
|
|
|
|
|
|
|
Returns a list of the names of the current loaded Macros. |
|
388
|
|
|
|
|
|
|
|
|
389
|
|
|
|
|
|
|
=head1 PHRASEBOOK FORMAT |
|
390
|
|
|
|
|
|
|
|
|
391
|
|
|
|
|
|
|
=head2 Prompt |
|
392
|
|
|
|
|
|
|
|
|
393
|
|
|
|
|
|
|
A Prompt is a named regular expression which matches the content of a single |
|
394
|
|
|
|
|
|
|
line of text. Here is an example: |
|
395
|
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
prompt configure |
|
397
|
|
|
|
|
|
|
match /\(config[^)]*\)# ?$/ |
|
398
|
|
|
|
|
|
|
|
|
399
|
|
|
|
|
|
|
On the first line is the keyword C<prompt> followed by the name of the Prompt, |
|
400
|
|
|
|
|
|
|
which must be a valid Perl identifier (letters, numbers, underscores only). |
|
401
|
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
On the immediately following line is the keyword C<match> followed by a |
|
403
|
|
|
|
|
|
|
regular expression, enclosed in two forward-slash characters. Currently, no |
|
404
|
|
|
|
|
|
|
alternate bookend characters are supported, nor are regular expression |
|
405
|
|
|
|
|
|
|
modifiers (such as C<xism>) outside of the match, but you can of course |
|
406
|
|
|
|
|
|
|
include them within. |
|
407
|
|
|
|
|
|
|
|
|
408
|
|
|
|
|
|
|
The Prompt is used to find out when the connected CLI has emitted all of the |
|
409
|
|
|
|
|
|
|
response to a command. Try to make the Prompt as specific as possible, |
|
410
|
|
|
|
|
|
|
including line-end anchors. Remember that it will be matched against one line |
|
411
|
|
|
|
|
|
|
of text, only. |
|
412
|
|
|
|
|
|
|
|
|
413
|
|
|
|
|
|
|
=head2 Macro |
|
414
|
|
|
|
|
|
|
|
|
415
|
|
|
|
|
|
|
In general, Macros are alternating sequences of commands to send to the |
|
416
|
|
|
|
|
|
|
connected CLI, and regular expressions to match the end of the returned |
|
417
|
|
|
|
|
|
|
response. Macros are useful for issuing commands which have intermediate |
|
418
|
|
|
|
|
|
|
prompts, or confirmation steps. They also support the I<slurping> of |
|
419
|
|
|
|
|
|
|
additional output when the connected CLI has split the response into pages. |
|
420
|
|
|
|
|
|
|
|
|
421
|
|
|
|
|
|
|
At its simplest a Macro can be just one command: |
|
422
|
|
|
|
|
|
|
|
|
423
|
|
|
|
|
|
|
macro show_int_br |
|
424
|
|
|
|
|
|
|
send show ip int br |
|
425
|
|
|
|
|
|
|
match /> ?$/ |
|
426
|
|
|
|
|
|
|
|
|
427
|
|
|
|
|
|
|
On the first line is the keyword C<macro> followed by the name of the Macro, |
|
428
|
|
|
|
|
|
|
which must be a valid Perl identifier (letters, numbers, underscores only). |
|
429
|
|
|
|
|
|
|
|
|
430
|
|
|
|
|
|
|
On the immediately following line is the keyword C<send> followed by a space |
|
431
|
|
|
|
|
|
|
and then any text up until the end of the line, and if you want to include |
|
432
|
|
|
|
|
|
|
whitespace at the beginning or end of the command, use quotes. This text is |
|
433
|
|
|
|
|
|
|
sent to the connected CLI as a single command statement. The next line |
|
434
|
|
|
|
|
|
|
contains the keyword C<match> followed by the Prompt (regular expression) |
|
435
|
|
|
|
|
|
|
which will terminate gathering of returned output from the sent command. |
|
436
|
|
|
|
|
|
|
|
|
437
|
|
|
|
|
|
|
Macros support the following features: |
|
438
|
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
=over 4 |
|
440
|
|
|
|
|
|
|
|
|
441
|
|
|
|
|
|
|
=item Automatic Matching |
|
442
|
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
Normally, you ought always to specify C<send> statements along with a |
|
444
|
|
|
|
|
|
|
following C<match> statement so that the module can tell when the output from |
|
445
|
|
|
|
|
|
|
your command has ended. However you can omit any Match and the module will |
|
446
|
|
|
|
|
|
|
insert either the current C<prompt> value if set by the user, or the last |
|
447
|
|
|
|
|
|
|
Prompt from the last Macro. So the previous example could be re-written as: |
|
448
|
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
macro show_int_br |
|
450
|
|
|
|
|
|
|
send show ip int br |
|
451
|
|
|
|
|
|
|
|
|
452
|
|
|
|
|
|
|
You can have as many C<send> statements as you like, and the Match statements |
|
453
|
|
|
|
|
|
|
will be inserted for you: |
|
454
|
|
|
|
|
|
|
|
|
455
|
|
|
|
|
|
|
macro show_int_br_and_timestamp |
|
456
|
|
|
|
|
|
|
send show ip int br |
|
457
|
|
|
|
|
|
|
send show clock |
|
458
|
|
|
|
|
|
|
|
|
459
|
|
|
|
|
|
|
However it is recommended that this type of sequence be implemented as |
|
460
|
|
|
|
|
|
|
individual commands (or separate Macros) rather than a single Macro, as it |
|
461
|
|
|
|
|
|
|
will be easier for you to retrieve the command response(s). Normally the |
|
462
|
|
|
|
|
|
|
Automatic Matching is used just to allow missing off of the final Match |
|
463
|
|
|
|
|
|
|
statement when it's the same as the current Prompt. |
|
464
|
|
|
|
|
|
|
|
|
465
|
|
|
|
|
|
|
=item Format Interpolation |
|
466
|
|
|
|
|
|
|
|
|
467
|
|
|
|
|
|
|
Each C<send> statement is in fact run through Perl's C<sprintf> command, so |
|
468
|
|
|
|
|
|
|
variables may be interpolated into the statement using standard C<"%"> fields. |
|
469
|
|
|
|
|
|
|
For example: |
|
470
|
|
|
|
|
|
|
|
|
471
|
|
|
|
|
|
|
macro show_int_x |
|
472
|
|
|
|
|
|
|
send show interface %s |
|
473
|
|
|
|
|
|
|
|
|
474
|
|
|
|
|
|
|
The method for passing variables into the module upon execution of this Macro |
|
475
|
|
|
|
|
|
|
is documented in L<Net::CLI::Interact::Role::Engine>. This feature is useful |
|
476
|
|
|
|
|
|
|
for username/password prompts. |
|
477
|
|
|
|
|
|
|
|
|
478
|
|
|
|
|
|
|
=item Named Match References |
|
479
|
|
|
|
|
|
|
|
|
480
|
|
|
|
|
|
|
If you're going to use the same Match (regular expression) in a number of |
|
481
|
|
|
|
|
|
|
Macros, then set it up as a Prompt (see above) and refer to it by name, |
|
482
|
|
|
|
|
|
|
instead: |
|
483
|
|
|
|
|
|
|
|
|
484
|
|
|
|
|
|
|
prompt priv_exec |
|
485
|
|
|
|
|
|
|
match /# ?$/ |
|
486
|
|
|
|
|
|
|
|
|
487
|
|
|
|
|
|
|
macro to_priv_exec |
|
488
|
|
|
|
|
|
|
send enable |
|
489
|
|
|
|
|
|
|
match /[Pp]assword: ?$/ |
|
490
|
|
|
|
|
|
|
send %s |
|
491
|
|
|
|
|
|
|
match priv_exec |
|
492
|
|
|
|
|
|
|
|
|
493
|
|
|
|
|
|
|
As you can see, in the case of the last Match, we have the keyword C<match> |
|
494
|
|
|
|
|
|
|
followed by the name of a defined Prompt. To match multiple defined Prompts |
|
495
|
|
|
|
|
|
|
use this syntax (with as many named references as you like): |
|
496
|
|
|
|
|
|
|
|
|
497
|
|
|
|
|
|
|
macro to_privileged |
|
498
|
|
|
|
|
|
|
send enable |
|
499
|
|
|
|
|
|
|
match username_prompt or priv_exec |
|
500
|
|
|
|
|
|
|
|
|
501
|
|
|
|
|
|
|
=item Continuations |
|
502
|
|
|
|
|
|
|
|
|
503
|
|
|
|
|
|
|
Sometimes the connected CLI will not know it's talking to a program and so |
|
504
|
|
|
|
|
|
|
paginate the output (that is, split it into pages). There is usually a |
|
505
|
|
|
|
|
|
|
keypress required between each page. This is supported via the following |
|
506
|
|
|
|
|
|
|
syntax: |
|
507
|
|
|
|
|
|
|
|
|
508
|
|
|
|
|
|
|
macro show_run |
|
509
|
|
|
|
|
|
|
send show running-config |
|
510
|
|
|
|
|
|
|
follow / --More-- / with ' ' |
|
511
|
|
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
On the line following the C<send> statement is the keyword C<follow> and a |
|
513
|
|
|
|
|
|
|
regular expression enclosed in forward-slashes. This is the Match which will, |
|
514
|
|
|
|
|
|
|
if seen in the command output, trigger the continuation. On the line you then |
|
515
|
|
|
|
|
|
|
have the keyword C<with> followed by a space and some text, until the end of |
|
516
|
|
|
|
|
|
|
the line. If you need to enclose whitespace use quotes, as in the example. |
|
517
|
|
|
|
|
|
|
|
|
518
|
|
|
|
|
|
|
The module will send the continuation text and gobble the matched prompt from |
|
519
|
|
|
|
|
|
|
the emitted output so you only have one complete piece of text returned, even |
|
520
|
|
|
|
|
|
|
if split over many pages. The sent text can contain metacharacters such as |
|
521
|
|
|
|
|
|
|
C<\n> for a newline. |
|
522
|
|
|
|
|
|
|
|
|
523
|
|
|
|
|
|
|
Note that in the above example the C<follow> statement should be seen as an |
|
524
|
|
|
|
|
|
|
extension of the C<send> statement. There is still an implicit Match prompt |
|
525
|
|
|
|
|
|
|
added at the end of this Macro, as per Automatic Matching, above. |
|
526
|
|
|
|
|
|
|
|
|
527
|
|
|
|
|
|
|
=item Line Endings |
|
528
|
|
|
|
|
|
|
|
|
529
|
|
|
|
|
|
|
Normally all sent command statements are appended with a newline (or the value |
|
530
|
|
|
|
|
|
|
of C<ors>, if set). To suppress that feature, use the keyword C<put> instead |
|
531
|
|
|
|
|
|
|
of C<send>. However this does not prevent the Format Interpolation via |
|
532
|
|
|
|
|
|
|
C<sprintf> as described above (simply use C<"%%"> to get a literal C<"%">). |
|
533
|
|
|
|
|
|
|
|
|
534
|
|
|
|
|
|
|
=back |
|
535
|
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
=cut |
|
537
|
|
|
|
|
|
|
|