]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/LEI.pm
lei: start working on bash completion
[public-inbox.git] / lib / PublicInbox / LEI.pm
index 95b48095ef6af108d67aad827d5acb6583f2482f..7004e9d72ed634dddf3dff99fd1b9b8024870c61 100644 (file)
@@ -12,7 +12,7 @@ use parent qw(PublicInbox::DS);
 use Getopt::Long ();
 use Socket qw(AF_UNIX SOCK_STREAM pack_sockaddr_un);
 use Errno qw(EAGAIN ECONNREFUSED ENOENT);
-use POSIX qw(setsid);
+use POSIX ();
 use IO::Handle ();
 use Sys::Syslog qw(syslog openlog);
 use PublicInbox::Config;
@@ -132,7 +132,11 @@ our %CMD = ( # sorted in order of importance/use:
 
 'reorder-local-store-and-break-history' => [ '[REFNAME]',
        'rewrite git history in an attempt to improve compression',
-       'gc!' ]
+       'gc!' ],
+
+# internal commands are prefixed with '_'
+'_complete' => [ '[...]', 'internal shell completion helper',
+               pass_through('everything') ],
 ); # @CMD
 
 # switch descriptions, try to keep consistent across commands
@@ -209,6 +213,10 @@ my %OPTDESC = (
        'unset matching NAME, may be specified multiple times'],
 ); # %OPTDESC
 
+my %CONFIG_KEYS = (
+       'leistore.dir' => 'top-level storage location',
+);
+
 sub x_it ($$) { # pronounced "exit"
        my ($self, $code) = @_;
        if (my $sig = ($code & 127)) {
@@ -223,6 +231,8 @@ sub x_it ($$) { # pronounced "exit"
        }
 }
 
+sub puts ($;@) { print { shift->{1} } map { "$_\n" } @_ }
+
 sub emit {
        my ($self, $channel) = @_; # $buf = $_[2]
        print { $self->{$channel} } $_[2] or die "print FD[$channel]: $!";
@@ -522,6 +532,55 @@ sub lei_daemon_env {
 
 sub lei_help { _help($_[0]) }
 
+# Shell completion helper.  Used by lei-completion.bash and hopefully
+# other shells.  Try to do as much here as possible to avoid redundancy
+# and improve maintainability.
+sub lei__complete {
+       my ($self, @argv) = @_; # argv = qw(lei and any other args...)
+       shift @argv; # ignore "lei", the entire command is sent
+       @argv or return puts $self, grep(!/^_/, keys %CMD);
+       my $cmd = shift @argv;
+       my $info = $CMD{$cmd} // do { # filter matching commands
+               @argv or puts $self, grep(/\A\Q$cmd\E/, keys %CMD);
+               return;
+       };
+       my ($proto, undef, @spec) = @$info;
+       my $cur = pop @argv;
+       my $re = defined($cur) ? qr/\A\Q$cur\E/ : qr/./;
+       if (substr($cur // '-', 0, 1) eq '-') { # --switches
+               # gross special case since the only git-config options
+               # Consider moving to a table if we need more special cases
+               # we use Getopt::Long for are the ones we reject, so these
+               # are the ones we don't reject:
+               if ($cmd eq 'config') {
+                       puts $self, grep(/$re/, keys %CONFIG_KEYS);
+                       @spec = qw(add z|null get get-all unset unset-all
+                               replace-all get-urlmatch
+                               remove-section rename-section
+                               name-only list|l edit|e
+                               get-color-name get-colorbool);
+                       # fall-through
+               }
+               # TODO: arg support
+               puts $self, grep(/$re/, map { # generate short/long names
+                       my $eq = '';
+                       if (s/=.+\z//) { # required arg, e.g. output|o=i
+                               $eq = '=';
+                       } elsif (s/:.+\z//) { # optional arg, e.g. mid:s
+                       } else { # negation: solve! => no-solve|solve
+                               s/\A(.+)!\z/no-$1|$1/;
+                       }
+                       map {
+                               length > 1 ? "--$_$eq" : "-$_"
+                       } split(/\|/, $_, -1) # help|h
+               } grep { !ref } @spec); # filter out $GLP_PASS ref
+       } elsif ($cmd eq 'config' && !@argv && !$CONFIG_KEYS{$cur}) {
+               puts $self, grep(/$re/, keys %CONFIG_KEYS);
+       }
+       # TODO: URLs, pathnames, OIDs, MIDs, etc...  See optparse() for
+       # proto parsing.
+}
+
 sub reap_exec { # dwaitpid callback
        my ($self, $pid) = @_;
        x_it($self, $?);
@@ -584,60 +643,44 @@ sub noop {}
 
 # lei(1) calls this when it can't connect
 sub lazy_start {
-       my ($path, $err) = @_;
-       require IO::FDPass; # require this early so caller sees it
-       if ($err == ECONNREFUSED) {
+       my ($path, $errno) = @_;
+       if ($errno == ECONNREFUSED) {
                unlink($path) or die "unlink($path): $!";
-       } elsif ($err != ENOENT) {
-               $! = $err; # allow interpolation to stringify in die
+       } elsif ($errno != ENOENT) {
+               $! = $errno; # allow interpolation to stringify in die
                die "connect($path): $!";
        }
        umask(077) // die("umask(077): $!");
        socket(my $l, AF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
        bind($l, pack_sockaddr_un($path)) or die "bind($path): $!";
-       listen($l, 1024) or die "listen $!";
+       listen($l, 1024) or die "listen: $!";
        my @st = stat($path) or die "stat($path): $!";
        my $dev_ino_expect = pack('dd', $st[0], $st[1]); # dev+ino
        pipe(my ($eof_r, $eof_w)) or die "pipe: $!";
        my $oldset = PublicInbox::Sigfd::block_signals();
-       my $pid = fork // die "fork: $!";
-       return if $pid;
+       require IO::FDPass;
        require PublicInbox::Listener;
        require PublicInbox::EOFpipe;
-       openlog($path, 'pid', 'user');
-       local $SIG{__DIE__} = sub {
-               syslog('crit', "@_");
-               die; # calls the default __DIE__ handler
-       };
-       local $SIG{__WARN__} = sub { syslog('warning', "@_") };
-       open(STDIN, '+<', '/dev/null') or die "redirect stdin failed: $!\n";
-       open STDOUT, '>&STDIN' or die "redirect stdout failed: $!\n";
-       open STDERR, '>&STDIN' or die "redirect stderr failed: $!\n";
-       setsid();
-       $pid = fork // die "fork: $!";
+       (-p STDOUT && -p STDERR) or die "E: stdout+stderr must be pipes\n";
+       open(STDIN, '+<', '/dev/null') or die "redirect stdin failed: $!";
+       POSIX::setsid() > 0 or die "setsid: $!";
+       my $pid = fork // die "fork: $!";
        return if $pid;
-       $SIG{__DIE__} = 'DEFAULT';
-       my $on_destroy = PublicInbox::OnDestroy->new(sub {
-               my ($owner_pid) = @_;
-               syslog('crit', "$@") if $@ && $$ == $owner_pid;
-       }, $$);
        $0 = "lei-daemon $path";
        local %PATH2CFG;
-       $l->blocking(0);
-       $eof_w->blocking(0);
-       $eof_r->blocking(0);
-       my $listener = PublicInbox::Listener->new($l, \&accept_dispatch, $l);
+       $_->blocking(0) for ($l, $eof_r, $eof_w);
+       $l = PublicInbox::Listener->new($l, \&accept_dispatch, $l);
        my $exit_code;
        local $quit = sub {
                $exit_code //= shift;
-               my $tmp = $listener or exit($exit_code);
+               my $listener = $l or exit($exit_code);
                unlink($path) if defined($path);
-               syswrite($eof_w, '.');
-               $l = $listener = $path = undef;
-               $tmp->close if $tmp; # DS::close
+               # closing eof_w triggers \&noop wakeup
+               $eof_w = $l = $path = undef;
+               $listener->close; # DS::close
                PublicInbox::DS->SetLoopTimeout(1000);
        };
-       PublicInbox::EOFpipe->new($eof_r, sub {}, undef);
+       PublicInbox::EOFpipe->new($eof_r, \&noop, undef);
        my $sig = {
                CHLD => \&PublicInbox::DS::enqueue_reap,
                QUIT => $quit,
@@ -682,8 +725,21 @@ sub lazy_start {
                }
                $n; # true: continue, false: stop
        });
+
+       # STDIN was redirected to /dev/null above, closing STDOUT and
+       # STDERR will cause the calling `lei' client process to finish
+       # reading <$daemon> pipe.
+       open STDOUT, '>&STDIN' or die "redirect stdout failed: $!";
+       openlog($path, 'pid', 'user');
+       local $SIG{__WARN__} = sub { syslog('warning', "@_") };
+       my $owner_pid = $$;
+       my $on_destroy = PublicInbox::OnDestroy->new(sub {
+               syslog('crit', "$@") if $@ && $$ == $owner_pid;
+       });
+       open STDERR, '>&STDIN' or die "redirect stderr failed: $!";
+       # $daemon pipe to `lei' closed, main loop begins:
        PublicInbox::DS->EventLoop;
-       $@ = undef if $on_destroy; # quiet OnDestroy if we got here
+       @$on_destroy = (); # cancel on_destroy if we get here
        exit($exit_code // 0);
 }