File Coverage

blib/lib/App/Memcached/CLI/Main.pm
Criterion Covered Total %
statement 53 324 16.3
branch 4 96 4.1
condition 2 41 4.8
subroutine 16 49 32.6
pod 0 27 0.0
total 75 537 13.9


line stmt bran cond sub pod time code
1             package App::Memcached::CLI::Main;
2              
3 2     2   15570 use strict;
  2         3  
  2         48  
4 2     2   7 use warnings;
  2         3  
  2         40  
5 2     2   33 use 5.008_001;
  2         6  
6              
7 2     2   7 use Carp;
  2         2  
  2         128  
8 2     2   13 use File::Basename 'basename';
  2         2  
  2         111  
9 2     2   1201 use Getopt::Long qw(:config posix_default no_ignore_case no_ignore_case_always);
  2         16313  
  2         10  
10 2     2   1310 use IO::Socket::INET;
  2         30074  
  2         12  
11 2     2   752 use List::Util qw(first);
  2         4  
  2         145  
12 2     2   926 use Term::ReadLine;
  2         3768  
  2         49  
13              
14 2     2   327 use App::Memcached::CLI;
  2         4  
  2         36  
15 2     2   661 use App::Memcached::CLI::DataSource;
  2         6  
  2         57  
16 2     2   619 use App::Memcached::CLI::Help;
  2         4  
  2         47  
17 2     2   568 use App::Memcached::CLI::Item;
  2         4  
  2         46  
18 2     2   7 use App::Memcached::CLI::Util ':all';
  2         3  
  2         170  
19              
20 2     2   7 use version; our $VERSION = 'v0.7.1';
  2         2  
  2         6  
21              
22             my $PROGRAM = basename $0;
23              
24             my %COMMAND2ALIASES = (
25             help => ['\h'],
26             version => ['\v'],
27             quit => [qw(\q exit)],
28             display => [qw(\d)],
29             stats => [qw(\s)],
30             settings => [qw(\c config)],
31             cachedump => [qw(\cd dump)],
32             detaildump => [qw(\dd)],
33             detail => [],
34             get => [],
35             gets => [],
36             set => [],
37             add => [],
38             replace => [],
39             append => [],
40             prepend => [],
41             cas => [],
42             incr => [],
43             decr => [],
44             touch => [],
45             delete => [],
46             flush_all => [qw(flush)],
47             );
48             my %COMMAND_OF;
49             while (my ($cmd, $aliases) = each %COMMAND2ALIASES) {
50             $COMMAND_OF{$cmd} = $cmd;
51             $COMMAND_OF{$_} = $cmd for @$aliases;
52             }
53              
54             my $DEFAULT_CACHEDUMP_SIZE = 20;
55              
56             sub new {
57 0     0 0 0 my $class = shift;
58 0         0 my %params = @_;
59              
60 0         0 eval {
61             $params{ds}
62             = App::Memcached::CLI::DataSource->connect(
63             $params{addr}, timeout => $params{timeout}
64 0         0 );
65             };
66 0 0       0 if ($@) {
67 0         0 warn "Can't connect to Memcached server! Addr=$params{addr}";
68 0         0 debug "ERROR: " . $@;
69 0         0 return;
70             }
71              
72 0         0 bless \%params, $class;
73             }
74              
75             sub parse_args {
76 6     6 0 4675 my $class = shift;
77              
78 6         7 my %params; # will be passed to new()
79 6 100 66     27 if (defined $ARGV[0] and looks_like_addr($ARGV[0])) {
80 4         14 $params{addr} = shift @ARGV;
81             }
82             GetOptions(
83 6 50       30 \my %opts, 'addr|a=s', 'timeout|t=i',
84             'debug|d', 'help|h', 'man',
85             ) or return +{};
86              
87 6 50       1473 if (defined $opts{debug}) {
88 0         0 $App::Memcached::CLI::DEBUG = 1;
89             }
90              
91 6         20 %params = (%opts, %params);
92 6         20 $params{addr} = create_addr($params{addr});
93              
94 6         18 return \%params;
95             }
96              
97             sub run {
98 0     0 0   my $self = shift;
99 0 0         if (@ARGV) {
100 0           $self->run_batch;
101             } else {
102 0           $self->run_interactive;
103             }
104             }
105              
106             sub run_batch {
107 0     0 0   my $self = shift;
108 0 0         debug "Run batch mode with @ARGV" if (@ARGV);
109 0           my ($_command, @args) = @ARGV;
110 0           my $command = $COMMAND_OF{$_command};
111 0 0         unless ($command) {
    0          
112 0           print "Unknown command - $_command\n";
113 0           return;
114             } elsif ($command eq 'quit') {
115 0           print "Nothing to do with $_command\n";
116 0           return;
117             }
118              
119 0           my $ret = $self->$command(@args);
120 0 0         unless ($ret) {
121 0           print qq[Command seems failed. Run \`$PROGRAM help\` or \`$PROGRAM help $command\` for usage.\n];
122             }
123             }
124              
125             sub run_interactive {
126 0     0 0   my $self = shift;
127 0           debug "Start interactive mode. $self->{addr}";
128 0   0       my $isa_tty = -t STDIN && (-t STDOUT || !(-f STDOUT || -c STDOUT));
129 0 0         unless ($isa_tty) {
130 0           croak "TTY Not Found! Quit.";
131             }
132 0           my $exit_loop = 0;
133             local $SIG{INT} = local $SIG{QUIT} = sub {
134 0     0     $exit_loop = 1;
135 0           warn "Caught INT or QUIT. Exiting...";
136 0           };
137              
138 0           $self->{term} = Term::ReadLine->new($PROGRAM);
139 0           print "Type '\\h' or 'help' to show help.\n\n";
140 0           while (! $exit_loop) {
141 0           my ($command, @args) = $self->prompt;
142 0 0         next unless $command;
143 0 0         if ($command eq 'quit') {
144 0           $exit_loop = 1;
145 0           next;
146             }
147              
148 0           my $ret = $self->$command(@args);
149 0 0         unless ($ret) {
150 0           print "Command seems failed. Type \\h $command for help.\n\n";
151             }
152             }
153 0           debug "Finish interactive mode. $self->{addr}";
154             }
155              
156             sub prompt {
157 0     0 0   my $self = shift;
158              
159 0           local $| = 1;
160 0           local $\;
161              
162 0           my $input = $self->{term}->readline("memcached\@$self->{addr}> ");
163 0           chomp($input);
164 0 0         return unless $input;
165 0 0         $self->{term}->addhistory($input) if ($input =~ m/\S/);
166              
167 0           my ($_command, @args) = split(m/\s+/, $input);
168 0           my $command = $COMMAND_OF{$_command};
169 0 0         print "Unknown command - $input\n" unless $command;
170              
171 0           return $command, @args;
172             }
173              
174             sub help {
175 0     0 0   my $self = shift;
176 0   0       my $command = shift || q{};
177              
178 0           my @command_info = @App::Memcached::CLI::Help::COMMANDS_INFO;
179              
180 0           my $body = q{};
181 0           my $space = ' ' x 4;
182              
183             # Help for specified command
184 0 0         if (my $function = $COMMAND_OF{$command}) {
    0          
185 0           my $aliases = join(q{, }, _sorted_aliases_of($function));
186 0           my $info = (grep { $_->{command} eq $function } @command_info)[0];
  0            
187 0           $body .= sprintf qq{\n[Command "%s"]\n\n}, $command;
188 0           $body .= "Summary:\n";
189 0           $body .= sprintf "%s%s\n\n", $space, $info->{summary};
190 0           $body .= "Aliases:\n";
191 0           $body .= sprintf "%s%s\n\n", $space, $aliases;
192 0 0         if ($info->{description}) {
193 0           $body .= $info->{description};
194 0           $body .= "\n";
195             }
196 0           print $body;
197 0           return 1;
198             }
199             # Command not found, but continue
200             elsif ($command) {
201 0           $body .= "Unknown command: $command\n";
202             }
203              
204             # General help
205 0           $body .= "\n[Available Commands]\n";
206 0           for my $info (@command_info) {
207 0           my $cmd = $info->{command};
208 0           my $commands = join(q{, }, _sorted_aliases_of($cmd));
209             $body .= sprintf "%s%-20s%s%s\n",
210 0           $space, $commands, $space x 2, $info->{summary};
211             }
212 0           $body .= "\nType \\h for each.\n\n";
213 0           print $body;
214 0           return 1;
215             }
216              
217             sub _sorted_aliases_of {
218 0     0     my $command = shift;
219 0           my @aliases = @{$COMMAND2ALIASES{$command}};
  0            
220 0 0         return (shift @aliases, $command, @aliases) if @aliases;
221 0           return ($command);
222             }
223              
224             sub get {
225 0     0 0   my $self = shift;
226 0           return $self->_retrieve('get', @_);
227             }
228              
229             sub gets {
230 0     0 0   my $self = shift;
231 0           return $self->_retrieve('gets', @_);
232             }
233              
234             sub _retrieve {
235 0     0     my $self = shift;
236 0           my ($command, @keys) = @_;
237 0 0         unless (@keys) {
238 0           print "No KEY specified.\n";
239 0           return;
240             }
241             my $items = App::Memcached::CLI::Item->find(
242 0           \@keys, $self->{ds}, command => $command,
243             );
244 0 0         unless (@$items) {
245 0           print "Not found - @keys\n";
246 0           return 1;
247             }
248 0           for (my $i=0; $i < scalar(@$items); $i++) {
249 0           my $item = $items->[$i];
250 0           print $item->output;
251 0 0         printf "%s\n", '-' x 24 if ($i < scalar(@$items) - 1);
252             }
253 0           return 1;
254             }
255              
256 0     0 0   sub set { return &_store(shift, 'set', @_); }
257 0     0 0   sub add { return &_store(shift, 'add', @_); }
258 0     0 0   sub replace { return &_store(shift, 'replace', @_); }
259 0     0 0   sub append { return &_store(shift, 'append', @_); }
260 0     0 0   sub prepend { return &_store(shift, 'prepend', @_); }
261              
262             sub _store {
263 0     0     my $self = shift;
264 0           my $command = shift;
265 0           my ($key, $value, $expire, $flags) = @_;
266 0 0 0       unless ($key and $value) {
267 0           print "KEY or VALUE not specified.\n";
268 0           return;
269             }
270 0           my $item = App::Memcached::CLI::Item->new(
271             key => $key,
272             value => $value,
273             expire => $expire,
274             flags => $flags,
275             );
276 0 0         unless ($item->save($self->{ds}, command => $command)) {
277 0           print "Failed to $command item. KEY $key, VALUE $value\n";
278 0           return 1;
279             }
280 0           print "OK\n";
281 0           return 1;
282             }
283              
284             sub cas {
285 0     0 0   my $self = shift;
286 0           my ($key, $value, $cas, $expire, $flags) = @_;
287 0 0 0       unless ($key and $value and $cas) {
      0        
288 0           print "KEY or VALUE or CAS not specified.\n";
289 0           return;
290             }
291 0           my $item = App::Memcached::CLI::Item->new(
292             key => $key,
293             value => $value,
294             expire => $expire,
295             flags => $flags,
296             cas => $cas,
297             );
298 0 0         unless ($item->save($self->{ds}, command => 'cas')) {
299 0           print "Failed to cas item. KEY $key, VALUE $value\n";
300 0           return 1;
301             }
302 0           print "OK\n";
303 0           return 1;
304             }
305              
306 0     0 0   sub incr { return &_incr_decr(shift, 'incr', @_); }
307 0     0 0   sub decr { return &_incr_decr(shift, 'decr', @_); }
308              
309             sub _incr_decr {
310 0     0     my $self = shift;
311 0           my $cmd = shift;
312 0           my $key = shift;
313 0           my $number = shift;
314 0 0 0       unless ($key and defined $number) {
315 0           print "No KEY or VALUE specified.\n";
316 0           return;
317             }
318 0 0         unless ($number =~ m/^\d+$/) {
319 0           print "Give numeric number for $cmd VALUE.\n";
320 0           return;
321             }
322 0           my $new_value = $self->{ds}->$cmd($key, $number);
323 0 0         unless (defined $new_value) {
324 0           print "FAILED - $cmd\n";
325 0           return;
326             }
327 0           print "OK. New VALUE is $new_value\n";
328 0           return 1;
329             }
330              
331             sub touch {
332 0     0 0   my $self = shift;
333 0           my $key = shift;
334 0           my $expire = shift;
335 0 0 0       unless ($key and defined $expire) {
336 0           print "No KEY or EXPIRE specified.\n";
337 0           return;
338             }
339 0 0         unless ($expire =~ m/^\d+$/) {
340 0           print "Give numeric number for EXPIRE.\n";
341 0           return;
342             }
343 0 0         unless ($self->{ds}->touch($key, $expire)) {
344 0           print "Failed to touch. KEY $key maybe missing\n";
345 0           return;
346             }
347 0           print "OK\n";
348 0           return 1;
349             }
350              
351             sub delete {
352 0     0 0   my $self = shift;
353 0           my $key = shift;
354 0 0         unless ($key) {
355 0           print "No KEY specified.\n";
356 0           return;
357             }
358 0           my $item = App::Memcached::CLI::Item->new(key => $key);
359 0 0         unless ($item->remove($self->{ds})) {
360 0           warn "Failed to delete item. KEY $key";
361 0           return;
362             }
363 0           print "OK\n";
364 0           return 1;
365             }
366              
367             sub version {
368 0     0 0   my $self = shift;
369 0           my $version = $self->{ds}->version;
370 0           print "$version\n";
371 0           return 1;
372             }
373              
374             sub cachedump {
375 0     0 0   my $self = shift;
376 0           my $class = shift;
377 0   0       my $num = shift || $DEFAULT_CACHEDUMP_SIZE;
378              
379 0 0         unless ($class) {
380 0           print "No slab class specified.\n";
381 0           return;
382             }
383 0           my $response = $self->{ds}->query("stats cachedump $class $num");
384 0           for my $line (@$response) {
385 0 0         if ($line !~ m/^ITEM (\S+) \[(\d+) b; (\d+) s\]/) {
386 0           warn "Unknown response: $line";
387 0           next;
388             }
389 0           my %data = (key => $1, length => $2, expire => $3);
390 0           my $item = App::Memcached::CLI::Item->new(%data);
391 0           $item->output_line;
392             }
393 0           return 1;
394             }
395              
396             sub display {
397 0     0 0   my $self = shift;
398              
399 0           my %stats;
400 0           my $max = 1;
401              
402 0           my $resp_items = $self->{ds}->query('stats items');
403 0           for my $line (@$resp_items) {
404 0 0         if ($line =~ m/^STAT items:(\d+):(\w+) (\d+)/) {
405 0           $stats{$1}{$2} = $3;
406             }
407             }
408              
409 0           my $resp_slabs = $self->{ds}->query('stats slabs');
410 0           for my $line (@$resp_slabs) {
411 0 0         if ($line =~ m/^STAT (\d+):(\w+) (\d+)/) {
412 0           $stats{$1}{$2} = $3;
413 0           $max = $1;
414             }
415             }
416              
417 0           print " # Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM\n";
418 0           for my $class (1..$max) {
419 0           my $slab = $stats{$class};
420 0 0         next unless $slab->{total_pages};
421              
422             my $size
423             = $slab->{chunk_size} < 1024 ? "$slab->{chunk_size}B"
424 0 0         : sprintf("%.1fK", $slab->{chunk_size} / 1024.0) ;
425              
426 0 0         my $full = ($slab->{free_chunks_end} == 0) ? 'yes' : 'no';
427             printf(
428             "%3d %8s %9ds %7d %7d %7s %8d %8d %4d\n",
429             $class, $size, $slab->{age} || 0, $slab->{total_pages},
430             $slab->{number} || 0, $full, $slab->{evicted} || 0,
431 0   0       $slab->{evicted_time} || 0, $slab->{outofmemory} || 0,
      0        
      0        
      0        
      0        
432             );
433             }
434              
435 0           return 1;
436             }
437              
438             sub stats {
439 0     0 0   my $self = shift;
440 0           my $filter = shift;
441 0           my $response = $self->{ds}->query('stats');
442 0           _print_stats_of_response('stats', $filter, @$response);
443 0           return 1;
444             }
445              
446             sub settings {
447 0     0 0   my $self = shift;
448 0           my $filter = shift;
449 0           my $response = $self->{ds}->query('stats settings');
450 0           _print_stats_of_response('stats settings', $filter, @$response);
451 0           return 1;
452             }
453              
454             sub _print_stats_of_response {
455 0     0     my $title = shift;
456 0           my $filter = shift;
457 0           my @lines = @_;
458              
459 0           my %stats;
460 0           my ($max_key_l, $max_val_l) = (0, 0);
461              
462 0           for my $line (@lines) {
463 0 0         next if ($line !~ m/^STAT\s+(\S*)\s+(.*)/);
464 0           my ($key, $value) = ($1, $2);
465 0 0         if (length $key > $max_key_l) { $max_key_l = length $key; }
  0            
466 0 0         if (length $value > $max_val_l) { $max_val_l = length $value; }
  0            
467 0 0 0       next if ($filter && $key !~ m/$filter/);
468 0           $stats{$key} = $value;
469             }
470              
471 0           print "# $title\n";
472 0           printf "#%${max_key_l}s %${max_val_l}s\n", 'Field', 'Value';
473 0           for my $field (sort {$a cmp $b} (keys %stats)) {
  0            
474 0           printf (" %${max_key_l}s %${max_val_l}s\n", $field, $stats{$field});
475             }
476             }
477              
478             sub detaildump {
479 0     0 0   my $self = shift;
480 0           my $response = $self->{ds}->query("stats detail dump");
481 0           print "$_\n" for @$response;
482 0           return 1;
483             }
484              
485             sub detail {
486 0     0 0   my $self = shift;
487 0   0       my $mode = shift || q{};
488 0 0   0     unless (first { $_ eq $mode } qw/on off/) {
  0            
489 0           print "Mode must be 'on' or 'off'!\n";
490 0           return;
491             }
492 0           my $response = $self->{ds}->query("stats detail $mode");
493 0           print "$_\n" for @$response;
494 0           my %result = (
495             on => 'Enabled',
496             off => 'Disabled',
497             );
498 0           print "$result{$mode} stats collection for detail dump.\n";
499 0           return 1;
500             }
501              
502             sub flush_all {
503 0     0 0   my $self = shift;
504 0           my $delay = shift;
505 0           my $query = 'flush_all';
506 0 0         if ($delay) { $query .= " $delay"; }
  0            
507 0           my $response = $self->{ds}->query($query);
508 0           print "OK\n";
509 0           return 1;
510             }
511              
512             1;
513             __END__