File Coverage

blib/lib/App/redisp.pm
Criterion Covered Total %
statement 19 22 86.3
branch n/a
condition n/a
subroutine 7 8 87.5
pod n/a
total 26 30 86.6


line stmt bran cond sub pod time code
1             package App::redisp;
2             {
3             $App::redisp::VERSION = '0.13';
4             }
5             package # hide
6             main;
7 0     0     sub eval_ctx { eval "sub { $_[0] }" } # Here to avoid any closures
8              
9             package App::redisp;
10             # ABSTRACT: Perl redis shell
11              
12 1     1   1089 use B qw(svref_2object);
  1         2  
  1         76  
13 1     1   858 use Data::Dump qw(pp);
  1         6865  
  1         77  
14 1     1   969 use Moo;
  1         20121  
  1         7  
15 1     1   3086 use Pod::Usage qw(pod2usage);
  1         100177  
  1         137  
16 1     1   19420 use Term::ANSIColor qw(colored);
  1         28934  
  1         625  
17 1     1   2397 use Term::ReadLine;
  1         6129  
  1         37  
18 1     1   998 use Tie::Redis;
  0            
  0            
19              
20             use constant HAVE_READKEY => eval { require Term::ReadKey };
21              
22             use App::redisp::Commands qw(@COMMANDS);
23             use Eval::WithLexicals;
24              
25             has eval_with_lexicals => (
26             is => 'ro',
27             default => sub { Eval::WithLexicals->new(
28             in_package => 'main'
29             );
30             }
31             );
32              
33             has host => (
34             is => 'rw',
35             default => sub { 'localhost' }
36             );
37              
38             has port => (
39             is => 'rw',
40             default => sub { 6379 }
41             );
42              
43             has serialize => (
44             is => 'rw',
45             default => sub { '' }
46             );
47              
48             has redis => (
49             is => 'rw',
50             lazy => 1,
51             default => sub {
52             my($self) = @_;
53             tie my %h, 'Tie::Redis',
54             host => $self->host, port => $self->port, use_recv => 1;
55             tied %h;
56             }
57             );
58              
59             sub debug {
60             $ENV{DEBUG} && print "-- ", colored(['green'], @_), "\n";
61             }
62              
63             # Special handling code
64             my %special = (
65             keyword => sub {
66             my($param) = @_;
67             if($param =~ /^\s*(\w+)\s+([^\%\@\$].*)/) {
68             $param = "redis(q{$1}, $2)";
69             debug "Replaced with '$param'\n";
70             }
71             return $param;
72             }
73             );
74              
75             # Handle these commands specially
76             my %redis_special_commands = (
77             keys => $special{keyword},
78             exists => $special{keyword},
79             );
80              
81             my %util_cmds = (
82             encoding => sub {
83              
84             },
85             );
86              
87             sub BUILD {
88             my($self) = @_;
89              
90             $self->_install_commands;
91             }
92              
93             sub usage {
94             my($class, $verbosity) = @_;
95              
96             pod2usage(
97             -verbose => $verbosity == 1 ? (99, -sections => 'USAGE') : $verbosity,
98             -input => __FILE__
99             );
100             }
101              
102             sub run {
103             my($self) = @_;
104              
105             my $short_server = $self->host =~ /[0-9]$/
106             ? $self->host # IP address
107             : ($self->host =~ /^([^.]+)/)[0];
108              
109             my $read = Term::ReadLine->new($short_server);
110             my $prompt = "$short_server> ";
111              
112             while(1) {
113             my $line = $read->readline($prompt);
114             exit unless defined $line;
115             $read->addhistory($line) if $line =~ /\S/;
116              
117             if($line =~ /^(?:\?|help)$/) {
118             print ;
119             next;
120             } elsif($line =~ /^\.(\w+)(?:\s+(.*))?/) {
121             ($util_cmds{$1} || sub { warn "Unknown command\n" })->($2);
122             next;
123             } elsif($line =~ /^\s*(\w+)/ && exists $redis_special_commands{$1}) {
124             $line = $redis_special_commands{$1}->($line);
125             }
126              
127             # TODO: Consider Parse::Perl, but I like no-non-core XS deps for now.
128             my $code = ::eval_ctx $line;
129             unless(ref $code eq 'CODE') {
130             chomp $@;
131             print colored(['red'], $@), "\n";
132             next;
133             }
134              
135             $self->_setup_ties_for_code($code);
136              
137             $self->eval($line);
138             }
139             }
140              
141             sub eval {
142             my($self, $line) = @_;
143              
144             Term::ReadKey::ReadMode(0) if HAVE_READKEY;
145             my @ret;
146             eval {
147             local $SIG{INT} = sub { die "Interrupt\n" };
148             @ret = $self->eval_with_lexicals->eval($line);
149             1;
150             } or do {
151             chomp $@;
152             print colored(['red'], $@), "\n";
153             return;
154             };
155             Term::ReadKey::ReadMode(1) if HAVE_READKEY;
156             pp @ret;
157             }
158              
159             sub _setup_ties_for_code {
160             my($self, $code) = @_;
161             no strict 'refs';
162              
163             for my $var(_find_referenced($code)) {
164             # Avoid special variables
165             next if $var->[0] =~ /^(?:.*::|[\x01-\x1f].*|\W|[0-9]+|_|ENV|SIG)$/;
166              
167             if($var->[1] eq 'sv') {
168             next if tied ${"::" . $var->[0]};
169             debug qq{Tie \${\"$var->[0]\"}};
170             tie ${"::" . $var->[0]}, 'Tie::Redis::Scalar',
171             redis => $self->redis, key => $var->[0];
172              
173             } elsif($var->[1] eq 'hv') {
174             next if tied %{"::" . $var->[0]};
175             debug qq{Tie \%{\"$var->[0]\"}};
176             tie %{"::" . $var->[0]}, 'Tie::Redis::Hash',
177             redis => $self->redis, key => $var->[0];
178              
179             } elsif($var->[1] eq 'av') {
180             next if tied @{"::" . $var->[0]};
181             debug qq{Tie \@{\"$var->[0]\"}};
182             tie @{"::" . $var->[0]}, 'Tie::Redis::List',
183             redis => $self->redis, key => $var->[0];
184             }
185             }
186             }
187              
188             sub _install_commands {
189             my($self) = @_;
190              
191             no strict 'refs';
192             no warnings 'redefine';
193              
194             for my $cmd(@COMMANDS) {
195             next if exists $redis_special_commands{$cmd};
196             *{"main::$cmd"} = sub(@) {
197             my @items = $self->redis->{_conn}->$cmd(@_);
198              
199             if(@items == 1 && ref $items[0] eq 'ARRAY') {
200             return @{$items[0]};
201             } else {
202             return @items;
203             }
204             };
205             }
206              
207             *{"main::redis"} = sub(@) {
208             my($cmd, @args) = @_;
209             my @items = $self->redis->{_conn}->$cmd(@args);
210              
211             if(@items == 1 && ref $items[0] eq 'ARRAY') {
212             return @{$items[0]};
213             } else {
214             return @items;
215             }
216             };
217             }
218              
219             sub _find_referenced {
220             my($code) = @_;
221              
222             # Muahah!
223             my @vars;
224             my $cv = svref_2object($code);
225             my $op = $cv->START;
226             do {
227             if($op->name =~ /^(?:gv|gvsv|aelemfast|const)$/) {
228             my $type = $op->name eq 'gvsv' ? 'sv' :
229             $op->name eq 'aelemfast' ? 'av' :
230             ($op->next->name =~ /2(.*)/)[0];
231              
232             # B::Concise::concise_op was helpful here
233             if($type) {
234             my $idx = $op->isa("B::SVOP") ? $op->targ : $op->padix;
235              
236             my $sv;
237             if($op->isa("B::PADOP") || !${$op->sv}) {
238             $sv = (($cv->PADLIST->ARRAY)[1]->ARRAY)[$idx];
239             } else {
240             $sv = $op->sv;
241             }
242             my $gv_name = $sv->can("NAME") ? $sv->NAME : $sv->PV;
243             push @vars, [$gv_name, $type] if $gv_name;
244             }
245             }
246             } while $op = $op->next and $op->isa("B::OP");
247              
248             return @vars;
249             }
250              
251              
252              
253              
254             =pod
255              
256             =head1 NAME
257              
258             App::redisp - Perl redis shell
259              
260             =head1 VERSION
261              
262             version 0.13
263              
264             =head1 SYNOPSIS
265              
266             $ redisp
267             localhost> keys "foo*"
268             "foobar", "food"
269             localhost> set foobarbaz, 12
270             "OK"
271              
272             # Or in perl style
273             localhost> $foobar
274             10
275              
276             # Actually these next ones aren't implemented yet...
277             localhost> .encoding utf-8
278             localhost> .server xxx
279             localhost> .reconnect
280             localhost> .output json
281              
282             =head1 DESCRIPTION
283              
284             Redis and Perl share similar data types, therefore I thought it would be useful
285             to have a Redis shell interface that appears to behave as Perl. This is a Perl
286             Read-Eval-Print Loop (REPL) that happens to understand Redis.
287              
288             The use of Redis aims to be transparent, you just use a variable like C<$foo>
289             and it will be read or saved to Redis. For a temporary variable that is only visible to Perl use C.
290              
291             =for Pod::Coverage eval_with_lexicals host port debug eval BUILD run serialize usage
292              
293             =head1 USAGE
294              
295             redisp [--help] [--server=host] [--port=port] [--encoding=encoding]
296             [--serialize=serializer]
297              
298             =head1 OPTIONS
299              
300             =over 4
301              
302             =item * B<--help>
303              
304             This document.
305              
306             =item * B<--server>
307              
308             Host to connect to Redis on.
309              
310             =item * B<--port>
311              
312             Port to connect to Redis on.
313              
314             =item * B<--encoding>
315              
316             Encoding to use with Redis, B is recommended (but the default is none).
317              
318             =item * B<--serialize>
319              
320             Serializer to use, see the L documentation for details on supported
321             serializers and the limitations.
322              
323             =back
324              
325             =head1 LIMITATIONS
326              
327             The main noticable thing is common key naming styles in Redis such as
328             C<"foo-bar"> or C<"foo:bar"> require quoting on the Perl side. For example to
329             access a top level key of foo:bar you need to access C<${"foo:bar"}>.
330              
331             In Redis a key has one type; in Perl a glob reference may have HASH, ARRAY,
332             SCALAR, etc values. This application makes Perl match the Redis behaviour, it's
333             invalid to use more than one type at a particular name. The error will be:
334             C.
335              
336             Due to the way this works it's impossible to use symbolic references (e.g.
337             C<${"foo$a"}>), your code needs to reference top level keys it uses at compile
338             time.
339              
340             =head1 EXAMPLES
341              
342             Yet more examples, because the synopsis section was getting sort of big.
343              
344             C is a command that returns a hash, so to grab something like the version
345             you can do this:
346              
347             localhost> info
348             [returns big hash]
349              
350             localhost> info->{redis_version}
351             "2.1.10"
352              
353             Due to some commands clashing with Perl keywords you can't use them as
354             functions. C and C is something notable for this.
355              
356             localhost> keys "foo*" # Special cased
357              
358             localhost> sort keys "foo*" # doesn't work as you'd expect
359              
360             localhost> sort redis qw(keys foo*) # does what you wanted
361              
362             Pub/sub can be used, but you need to write some code yourself:
363             XXX: This doesn't work at all yet!
364              
365             localhost> subscribe foo, sub { print "@_\n" }
366             [prints messages, ^C stops, but you'll need to unsubscribe manually]
367             localhost> unsubscribe foo
368              
369             =head1 BUGS
370              
371             This goes I close to the internals of Perl so there may be issues with
372             constructs I haven't thought of. Raise bugs via L.
373              
374             The output produced by:
375              
376             ANYEVENT_REDIS_DEBUG=1 DEBUG=1 redisp
377              
378             for your issue would be helpful.
379              
380             =head1 SEE ALSO
381              
382             L, L, L,
383             L (I recommend you install this or ::Gnu).
384              
385             =head1 AUTHOR
386              
387             David Leadbeater
388              
389             =head1 COPYRIGHT AND LICENSE
390              
391             This software is copyright (c) 2011 by David Leadbeater.
392              
393             This program is free software. It comes without any warranty, to the extent
394             permitted by applicable law. You can redistribute it and/or modify it under the
395             terms of the Beer-ware license revision 42.
396              
397             =cut
398              
399              
400             __DATA__