| 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 |  |  |  |  |  |  |  |