line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Net::CLI::Interact::Phrasebook; |
2
|
|
|
|
|
|
|
{ $Net::CLI::Interact::Phrasebook::VERSION = '2.400000' } |
3
|
|
|
|
|
|
|
|
4
|
1
|
|
|
1
|
|
8
|
use Moo; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
7
|
|
5
|
1
|
|
|
1
|
|
311
|
use MooX::Types::MooseLike::Base qw(InstanceOf Str Any HashRef); |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
82
|
|
6
|
|
|
|
|
|
|
|
7
|
1
|
|
|
1
|
|
471
|
use Path::Class; |
|
1
|
|
|
|
|
38915
|
|
|
1
|
|
|
|
|
99
|
|
8
|
1
|
|
|
1
|
|
606
|
use File::ShareDir 'dist_dir'; |
|
1
|
|
|
|
|
24050
|
|
|
1
|
|
|
|
|
74
|
|
9
|
1
|
|
|
1
|
|
583
|
use Net::CLI::Interact::ActionSet; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
2337
|
|
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
|
|
|
|
|
|
|
|