]> Sergey Matveev's repositories - public-inbox.git/commitdiff
lei ls-search: command to list saved searches
authorEric Wong <e@80x24.org>
Sun, 18 Apr 2021 08:40:14 +0000 (08:40 +0000)
committerEric Wong <e@80x24.org>
Sun, 18 Apr 2021 23:04:42 +0000 (19:04 -0400)
Going forward, we'll probably support JSON for all the "ls-*"
subcommands.  This also provides the basis for "lei up" shell
completion.

MANIFEST
lib/PublicInbox/LEI.pm
lib/PublicInbox/LeiExternal.pm
lib/PublicInbox/LeiLsSearch.pm [new file with mode: 0644]
lib/PublicInbox/LeiSavedSearch.pm
lib/PublicInbox/LeiUp.pm
t/lei-q-save.t

index 1b7d16ee045869bf49eb83ad1a7d17347c156462..f35c514ced5544d1788514b9c2f9e454a4889e2a 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -196,6 +196,7 @@ lib/PublicInbox/LeiImport.pm
 lib/PublicInbox/LeiInit.pm
 lib/PublicInbox/LeiInput.pm
 lib/PublicInbox/LeiLsLabel.pm
+lib/PublicInbox/LeiLsSearch.pm
 lib/PublicInbox/LeiMirror.pm
 lib/PublicInbox/LeiOverview.pm
 lib/PublicInbox/LeiP2q.pm
index f223b3deb41386a9676b44a607fcbe30b93d5094..56640be12358cb0a3f01f68bf6aed20876c39ec1 100644 (file)
@@ -157,8 +157,8 @@ our %CMD = ( # sorted in order of importance/use:
        'exclude further results from a publicinbox|extindex',
        qw(prune), @c_opt ],
 
-'ls-query' => [ '[FILTER...]', 'list saved search queries',
-               qw(name-only format|f=s), @c_opt ],
+'ls-search' => [ '[PREFIX]', 'list saved search queries',
+               qw(format|f=s pretty l ascii z|0), @c_opt ],
 'rm-query' => [ 'QUERY_NAME', 'remove a saved search', @c_opt ],
 'mv-query' => [ qw(OLD_NAME NEW_NAME), 'rename a saved search', @c_opt ],
 
@@ -312,7 +312,9 @@ my %OPTDESC = (
 'jobs|j=i      add-external' => 'set parallelism when indexing after --mirror',
 
 'in-format|F=s' => $stdin_formats,
-'format|f=s    ls-query' => $ls_format,
+'format|f=s    ls-search' => ['OUT|json|jsonl|concatjson',
+                       'listing output format' ],
+'l     ls-search' => 'long listing format',
 'format|f=s    ls-external' => $ls_format,
 
 'limit|n=i@' => ['NUM', 'limit on number of matches (default: 10000)' ],
@@ -353,7 +355,7 @@ my %CONFIG_KEYS = (
        'leistore.dir' => 'top-level storage location',
 );
 
-my @WQ_KEYS = qw(lxs l2m imp mrr cnv p2q tag sol); # internal workers
+my @WQ_KEYS = qw(lxs l2m imp mrr cnv p2q tag sol lsss); # internal workers
 
 sub _drop_wq {
        my ($self) = @_;
index 5e8dc71a224e854e8d547bd3671b1fee6f75e4e9..b0ebe9479b6254294fada399d746933484a7d6e2 100644 (file)
@@ -215,8 +215,8 @@ sub lei_forget_external {
        }
 }
 
-sub _complete_url_common ($) {
-       my ($argv) = @_;
+sub complete_url_common {
+       my $argv = $_[-1];
        # Workaround bash word-splitting URLs to ['https', ':', '//' ...]
        # Maybe there's a better way to go about this in
        # contrib/completion/lei-completion.bash
@@ -228,7 +228,8 @@ sub _complete_url_common ($) {
                        push @x, $cur;
                        $cur = '';
                }
-               while (@x > 2 && $x[0] !~ /\Ahttps?\z/ && $x[1] ne ':') {
+               while (@x > 2 && $x[0] !~ /\A(?:http|nntp|imap)s?\z/i &&
+                               $x[1] ne ':') {
                        shift @x;
                }
                if (@x >= 2) { # qw(https : hostname : 443) or qw(http :)
@@ -245,7 +246,7 @@ sub _complete_url_common ($) {
 sub _complete_forget_external {
        my ($self, @argv) = @_;
        my $cfg = $self->_lei_cfg;
-       my ($cur, $re) = _complete_url_common(\@argv);
+       my ($cur, $re) = complete_url_common(\@argv);
        # FIXME: bash completion off "http:" or "https:" when the last
        # character is a colon doesn't work properly even if we're
        # returning "//$HTTP_HOST/$PATH_INFO/", not sure why, could
@@ -261,7 +262,7 @@ sub _complete_forget_external {
 sub _complete_add_external { # for bash, this relies on "compopt -o nospace"
        my ($self, @argv) = @_;
        my $cfg = $self->_lei_cfg;
-       my ($cur, $re) = _complete_url_common(\@argv);
+       my ($cur, $re) = complete_url_common(\@argv);
        require URI;
        map {
                my $u = URI->new(substr($_, length('external.')));
diff --git a/lib/PublicInbox/LeiLsSearch.pm b/lib/PublicInbox/LeiLsSearch.pm
new file mode 100644 (file)
index 0000000..2aa457c
--- /dev/null
@@ -0,0 +1,109 @@
+# Copyright (C) 2021 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# "lei ls-search" to display results saved via "lei q --save"
+package PublicInbox::LeiLsSearch;
+use strict;
+use v5.10.1;
+use PublicInbox::LeiSavedSearch;
+use parent qw(PublicInbox::IPC);
+
+sub do_ls_search_long {
+       my ($self, $pfx) = @_;
+       # TODO: share common JSON output code with LeiOverview
+       my $json = $self->{json}->new->utf8->canonical;
+       my $lei = $self->{lei};
+       $json->ascii(1) if $lei->{opt}->{ascii};
+       my $fmt = $lei->{opt}->{'format'};
+       $lei->{1}->autoflush(0);
+       my $ORS = "\n";
+       my $pretty = $lei->{opt}->{pretty};
+       my $EOR;  # TODO: compact pretty like "lei q"
+       if ($fmt =~ /\A(concat)?json\z/ && $pretty) {
+               $EOR = ($1//'') eq 'concat' ? "\n}" : "\n},";
+       }
+       if ($fmt eq 'json') {
+               $lei->out('[');
+               $ORS = ",\n";
+       }
+       my @x = sort(grep(/\A\Q$pfx/, PublicInbox::LeiSavedSearch::list($lei)));
+       while (my $x = shift @x) {
+               $ORS = '' if !scalar(@x);
+               my $lss = PublicInbox::LeiSavedSearch->new($lei, $x) or next;
+               my $cfg = $lss->{-cfg};
+               my $ent = {
+                       q => $cfg->get_all('lei.q'),
+                       output => $cfg->{'lei.q.output'},
+               };
+               for my $k ($lss->ARRAY_FIELDS) {
+                       my $ary = $cfg->get_all("lei.q.$k") // next;
+                       $ent->{$k} = $ary;
+               }
+               for my $k ($lss->BOOL_FIELDS) {
+                       my $val = $cfg->{"lei.q.$k"} // next;
+                       $ent->{$k} = $val;
+               }
+               if (defined $EOR) { # pretty, but compact
+                       $EOR = "\n}" if !scalar(@x);
+                       my $buf = "{\n";
+                       $buf .= join(",\n", map {;
+                               my $f = $_;
+                               if (my $v = $ent->{$f}) {
+                                       $v = $json->encode([$v]);
+                                       qq{  "$f": }.substr($v, 1, -1);
+                               } else {
+                                       ();
+                               }
+                       # key order by importance
+                       } (qw(output q), $lss->ARRAY_FIELDS,
+                               $lss->BOOL_FIELDS) );
+                       $lei->out($buf .= $EOR);
+               } else {
+                       $lei->out($json->encode($ent), $ORS);
+               }
+       }
+       if ($fmt eq 'json') {
+               $lei->out("]\n");
+       } elsif ($fmt eq 'concatjson') {
+               $lei->out("\n");
+       }
+}
+
+sub bg_worker ($$$) {
+       my ($lei, $pfx, $json) = @_;
+       my $self = bless { -wq_nr_workers => 1, json => $json }, __PACKAGE__;
+       my ($op_c, $ops) = $lei->workers_start($self, 'ls-search', 1);
+       $lei->{lsss} = $self;
+       $self->wq_io_do('do_ls_search_long', [], $pfx);
+       $self->wq_close(1);
+       $op_c->op_wait_event($ops);
+}
+
+sub lei_ls_search {
+       my ($lei, $pfx) = @_;
+       my $fmt = $lei->{opt}->{'format'} // '';
+       if ($lei->{opt}->{l}) {
+               $lei->{opt}->{'format'} //= $fmt = 'json';
+       }
+       my $json;
+       my $tty = -t $lei->{1};
+       $lei->start_pager if $tty;
+       if ($fmt =~ /\A(ldjson|ndjson|jsonl|(?:concat)?json)\z/) {
+               $lei->{opt}->{pretty} //= $tty;
+               $json = ref(PublicInbox::Config->json);
+       } elsif ($fmt ne '') {
+               return $lei->fail("unknown format: $fmt");
+       }
+       my $ORS = "\n";
+       if ($lei->{opt}->{z}) {
+               return $lei->fail('-z and --format do not mix') if $json;
+               $ORS = "\0";
+       }
+       $pfx //= '';
+       return bg_worker($lei, $pfx, $json) if $json;
+       for (sort(grep(/\A\Q$pfx/, PublicInbox::LeiSavedSearch::list($lei)))) {
+               $lei->out($_, $ORS);
+       }
+}
+
+1;
index 3076d14c1b4ae3959ddc363e08b2e69325503ebf..d67622c9b08ce6bfc30fa75cef75cb9206ba06ea 100644 (file)
@@ -21,6 +21,11 @@ sub cquote_val ($) { # cf. git-config(1)
        $val;
 }
 
+sub ARRAY_FIELDS () { qw(only include exclude) }
+sub BOOL_FIELDS () {
+       qw(external local remote import-remote import-before threads)
+}
+
 sub lss_dir_for ($$) {
        my ($lei, $dstref) = @_;
        my @n;
@@ -39,6 +44,31 @@ sub lss_dir_for ($$) {
        $lei->share_path . '/saved-searches/' . join('-', @n);
 }
 
+sub list {
+       my ($lei, $pfx) = @_;
+       my $lss_dir = $lei->share_path.'/saved-searches/';
+       return () unless -d $lss_dir;
+       # TODO: persist the cache?  Use another format?
+       my $f = $lei->cache_dir."/saved-tmp.$$.".time.'.config';
+       open my $fh, '>', $f or die "open $f: $!";
+       print $fh "[include]\n";
+       for my $p (glob("$lss_dir/*/lei.saved-search")) {
+               print $fh "\tpath = ", cquote_val($p), "\n";
+       }
+       close $fh or die "close $f: $!";
+       my $cfg = PublicInbox::Config::git_config_dump($f);
+       unlink($f);
+       bless $cfg, 'PublicInbox::Config';
+       my $out = $cfg->get_all('lei.q.output') or return ();
+       map {;
+               if (s!\A(?:maildir|mh|mbox.+|mmdf):!!i) {
+                       -e $_ ? $_ : (); # TODO auto-prune somewhere?
+               } else { # IMAP, maybe JMAP
+                       $_;
+               }
+       } @$out
+}
+
 sub new {
        my ($cls, $lei, $dst) = @_;
        my $self = bless { ale => $lei->ale }, $cls;
@@ -74,16 +104,15 @@ $q
 [lei "q"]
        output = $dst
 EOM
-               for my $k (qw(only include exclude)) {
+               for my $k (ARRAY_FIELDS) {
                        my $ary = $lei->{opt}->{$k} // next;
                        for my $x (@$ary) {
                                print $fh "\t$k = ".cquote_val($x)."\n";
                        }
                }
-               for my $k (qw(external local remote import-remote
-                               import-before threads)) {
+               for my $k (BOOL_FIELDS) {
                        my $val = $lei->{opt}->{$k} // next;
-                       print $fh "\t$k = ".cquote_val($val)."\n";
+                       print $fh "\t$k = ".($val ? 1 : 0)."\n";
                }
                close($fh) or return $lei->fail("close $f: $!");
        }
index 9fe4901bc6127a44bffe73a0fdc063aa38587e2b..73286ea2f20f48eea9844490223efa502930fbf7 100644 (file)
@@ -42,4 +42,10 @@ sub lei_up {
        $lei->_start_query;
 }
 
+sub _complete_up {
+       my ($lei, @argv) = @_;
+       my ($cur, $re) = $lei->complete_url_common(\@argv);
+       grep(/\A$re\Q$cur/, PublicInbox::LeiSavedSearch::list($lei));
+}
+
 1;
index a8eda41ebc0479a525a0fc3db585c1f7b7c8bb1a..761814b4ebbe2c6a2c836ed7a01b4a99898c0308 100644 (file)
@@ -55,5 +55,17 @@ test_lei(sub {
        ok(-s "$home/mbcl2" > $size, 'size increased after up');
 
        ok(!lei(qw(up -q), $home), 'up fails w/o --save');
+
+       lei_ok qw(ls-search); my @d = split(/\n/, $lei_out);
+       lei_ok qw(ls-search -z); my @z = split(/\0/, $lei_out);
+       is_deeply(\@d, \@z, '-z output matches non-z');
+       is_deeply(\@d, [ "$home/mbcl2", "$home/md/" ],
+               'ls-search output alphabetically sorted');
+       lei_ok qw(ls-search -l);
+       my $json = PublicInbox::Config->json->decode($lei_out);
+       ok($json && $json->[0]->{output}, 'JSON has output');
+       lei_ok qw(_complete lei up);
+       like($lei_out, qr!^\Q$home/mbcl2\E$!sm, 'complete got mbcl2 output');
+       like($lei_out, qr!^\Q$home/md/\E$!sm, 'complete got maildir output');
 });
 done_testing;