Complete-Bash

 view release on metacpan or  search on metacpan

lib/Complete/Bash.pm  view on Meta::CPAN

    summary => 'Completion routines for bash shell',
};

sub _expand_tilde {
    my ($user, $slash) = @_;
    my @ent;
    if (length $user) {
        @ent = getpwnam($user);
    } else {
        @ent = getpwuid($>);
        $user = $ent[0];
    }
    return $ent[7] . $slash if @ent;
    "~$user$slash"; # return as-is when failed
}

sub _add_unquoted {
    no warnings 'uninitialized';

    my ($word, $is_cur_word, $after_ws) = @_;

    #say "D:add_unquoted word=$word is_cur_word=$is_cur_word after_ws=$after_ws";

    $word =~ s!^(~)(\w*)(/|\z) |  # 1) tilde  2) username  3) optional slash
               \\(.)           |  # 4) escaped char
               \$(\w+)            # 5) variable name
              !
                  $1 ? (not($after_ws) || $is_cur_word ? "$1$2$3" : _expand_tilde($2, $3)) :
                      $4 ? $4 :
                          ($is_cur_word ? "\$$5" : $ENV{$5})
                              !egx;
    $word;
}

sub _add_double_quoted {
    no warnings 'uninitialized';

    my ($word, $is_cur_word) = @_;

    $word =~ s!\\(.)           |  # 1) escaped char
               \$(\w+)            # 2) variable name
              !
                  $1 ? $1 :
                      ($is_cur_word ? "\$$2" : $ENV{$2})
                          !egx;
    $word;
}

sub _add_single_quoted {
    my $word = shift;
    $word =~ s/\\(.)/$1/g;
    $word;
}

$SPEC{point} = {
    v => 1.1,
    summary => 'Return line with point marked by a marker',
    description => <<'_',

This is a utility function useful for testing/debugging. `parse_cmdline()`
expects a command-line and a cursor position (`$line`, `$point`). This routine
expects `$line` with a marker character (by default it's the caret, `^`) and
return (`$line`, `$point`) to feed to `parse_cmdline()`.

Example:

    point("^foo") # => ("foo", 0)
    point("fo^o") # => ("foo", 2)

_
    args_as => 'array',
    args => {
        cmdline => {
            summary => 'Command-line which contains a marker character',
            schema => 'str*',
            pos => 0,
        },
        marker => {
            summary => 'Marker character',
            schema => ['str*', len=>1],
            default => '^',
            pos => 1,
        },
    },
    result_naked => 1,
};
sub point {
    my ($line, $marker) = @_;
    $marker //= '^';

    my $point = index($line, $marker);
    die "BUG: No marker '$marker' in line <$line>" unless $point >= 0;
    $line =~ s/\Q$marker\E//;
    ($line, $point);
}

$SPEC{parse_cmdline} = {
    v => 1.1,
    summary => 'Parse shell command-line for processing by completion routines',
    description => <<'_',

This function basically converts `COMP_LINE` (str) and `COMP_POINT` (int) into
something like (but not exactly the same as) `COMP_WORDS` (array) and
`COMP_CWORD` (int) that bash supplies to shell functions.

The differences with bash are (these differences are mostly for parsing
convenience for programs that use this routine; this comparison is made against
bash versions 4.2-4.3):

1) quotes and backslashes are stripped (bash's `COMP_WORDS` contains all the
   quotes and backslashes);

2) quoted phrase that contains spaces, or phrase that contains escaped spaces is
   parsed as a single word. For example:

    command "First argument" Second\ argument

   bash would split it as (represented as Perl):

    ["command", "\"First", "argument\"", "Second\\", "argument"]

lib/Complete/Bash.pm  view on Meta::CPAN

  then the parse result will be:

    ["command", "--foo", "=", "bar", "http", ":", "//example.com", ":", "80", "Foo", "::", "Bar"]

  which is annoying sometimes. But we follow bash here so we can more easily
  accept input from a joined `COMP_WORDS` if we write completion bash functions,
  e.g. (in the example, `foo` is a Perl script):

    _foo ()
    {
        local words=(${COMP_CWORDS[@]})
        # add things to words, etc
        local point=... # calculate the new point
        COMPREPLY=( `COMP_LINE="foo ${words[@]}" COMP_POINT=$point foo` )
    }

  To avoid these word-breaking characters to be split/grouped, we can escape
  them with backslash or quote them, e.g.:

    command "http://example.com:80" Foo\:\:Bar

  which bash will parse as:

    ["command", "\"http://example.com:80\"", "Foo\\:\\:Bar"]

  and we parse as:

    ["command", "http://example.com:80", "Foo::Bar"]

* Due to the way bash parses the command line (see above), the two below are
  equivalent:

    % cmd --foo=bar
    % cmd --foo = bar

Because they both expand to `['--foo', '=', 'bar']`. But obviously
<pm:Getopt::Long> does not regard the two as equivalent.

_
    args_as => 'array',
    args => {
        cmdline => {
            summary => 'Command-line, defaults to COMP_LINE environment',
            schema => 'str*',
            pos => 0,
        },
        point => {
            summary => 'Point/position to complete in command-line, '.
                'defaults to COMP_POINT',
            schema => 'int*',
            pos => 1,
        },
        opts => {
            summary => 'Options',
            schema => 'hash*',
            description => <<'_',

Optional. Known options:

* `truncate_current_word` (bool). If set to 1, will truncate current word to the
  position of cursor, for example (`^` marks the position of cursor):
  `--vers^oo` to `--vers` instead of `--versoo`. This is more convenient when
  doing tab completion.

_
            schema => 'hash*',
            pos => 2,
        },
    },
    result => {
        schema => ['array*', len=>2],
        description => <<'_',

Return a 2-element array: `[$words, $cword]`. `$words` is array of str,
equivalent to `COMP_WORDS` provided by bash to shell functions. `$cword` is an
integer, roughly equivalent to `COMP_CWORD` provided by bash to shell functions.
The word to be completed is at `$words->[$cword]`.

Note that COMP_LINE includes the command name. If you want the command-line
arguments only (like in `@ARGV`), you need to strip the first element from
`$words` and reduce `$cword` by 1.


_
    },
    result_naked => 1,
    links => [
    ],
};
sub parse_cmdline {
    no warnings 'uninitialized';
    my ($line, $point, $opts) = @_;

    $line  //= $ENV{COMP_LINE};
    $point //= $ENV{COMP_POINT} // 0;

    die "$0: COMP_LINE not set, make sure this script is run under ".
        "bash completion (e.g. through complete -C)\n" unless defined $line;

    log_trace "[compbash] parse_cmdline(): input: line=<$line> point=<$point>"
        if $ENV{COMPLETE_BASH_TRACE};

    my @words;
    my $cword;
    my $pos = 0;
    my $pos_min_ws = 0;
    my $after_ws = 1; # XXX what does this variable mean?
    my $chunk;
    my $add_blank;
    my $is_cur_word;
    $line =~ s!(                                                         # 1) everything
                  (")((?: \\\\|\\"|[^"])*)(?:"|\z)(\s*)               |  #  2) open "  3) content  4) space after
                  (')((?: \\\\|\\'|[^'])*)(?:'|\z)(\s*)               |  #  5) open '  6) content  7) space after
                  ((?: \\\\|\\"|\\'|\\=|\\\s|[^"'@><=|&\(:\s])+)(\s*) |  #  8) unquoted word  9) space after
                  ([\@><=|&\(:]+) |                                      #  10) non-whitespace word-breaking characters
                  \s+
              )!
                  $pos += length($1);
                  #say "D: \$1=<$1> \$2=<$3> \$3=<$3> \$4=<$4> \$5=<$5> \$6=<$6> \$7=<$7> \$8=<$8> \$9=<$9> \$10=<$10>";
                  #say "D:<$1> pos=$pos, point=$point, cword=$cword, after_ws=$after_ws";

lib/Complete/Bash.pm  view on Meta::CPAN

    my @summaries;
    my @res;
    my $has_summary;

    my $code_return_message = sub {
        # display a message instead of list of words. we send " " (ASCII space)
        # which bash does not display, so we can display a line of message while
        # the user does not get the message as the completion. I've also tried
        # \000 to \037 instead of space (\040) but nothing works better.
        my $msg = shift;
        if ($msg =~ /\A /) {
            $msg =~ s/\A +//;
            $msg = " (empty message)" unless length $msg;
        }
        return (sprintf("%-"._terminal_width()."s", $msg), " ");
    };

  FORMAT_MESSAGE:
    # display a message instead of list of words. we send " " (ASCII space)
    # which bash does not display, so we can display a line of message while the
    # user does not get the message as the completion. I've also tried \000 to
    # \037 instead of space (\040) but nothing works better.
    if (defined $hcomp->{message}) {
        @res = $code_return_message->($hcomp->{message});
        goto RETURN_RES;
    }

  WORKAROUND_PREVENT_BASH_FROM_INSERTING_SPACE:
    {
        last unless @$words == 1;
        if (defined $path_sep) {
            my $re = qr/\Q$path_sep\E\z/;
            my $word;
            if (ref $words->[0] eq 'HASH') {
                $words = [$words->[0], {word=>"$words->[0]{word} "}] if
                    $words->[0]{word} =~ $re;
            } else {
                $words = [$words->[0], "$words->[0] "]
                    if $words->[0] =~ $re;
            }
            last;
        }

        if ($hcomp->{is_partial} ||
                ref $words->[0] eq 'HASH' && $words->[0]{is_partial}) {
            if (ref $words->[0] eq 'HASH') {
                $words = [$words->[0], {word=>"$words->[0]{word} "}];
            } else {
                $words = [$words->[0], "$words->[0] "];
            }
            last;
        }
    }

  WORKAROUND_WITH_WORDBREAKS:
    # this is a workaround. since bash breaks words using characters in
    # $COMP_WORDBREAKS, which by default is "'@><=;|&(: this presents a problem
    # we often encounter: if we want to provide with a list of strings
    # containing say ':', most often Perl modules/packages, if user types e.g.
    # "Text::AN" and we provide completion ["Text::ANSI"] then bash will change
    # the word at cursor to become "Text::Text::ANSI" since it sees the current
    # word as "AN" and not "Text::AN". the workaround is to chop /^Text::/ from
    # completion answers. btw, we actually chop /^text::/i to handle
    # case-insensitive matching, although this does not have the ability to
    # replace the current word (e.g. if we type 'text::an' then bash can only
    # replace the current word 'an' with 'ANSI).
    {
        last unless $opts->{workaround_with_wordbreaks} // 1;
        last unless defined $opts->{word};

        if ($opts->{word} =~ s/(.+[\@><=;|&\(:])//) {
            my $prefix = $1;
            for (@$words) {
                if (ref($_) eq 'HASH') {
                    $_->{word} =~ s/\A\Q$prefix\E//i;
                } else {
                    s/\A\Q$prefix\E//i;
                }
            }
        }
    }

  ESCAPE_WORDS:
    for my $entry (@$words) {
        my $word    = ref($entry) eq 'HASH' ? $entry->{word}    : $entry;
        my $summary = (ref($entry) eq 'HASH' ? $entry->{summary} : undef) // '';
        if ($esc_mode eq 'shellvar') {
            # escape $ also
            $word =~ s!([^A-Za-z0-9,+._/:~-])!\\$1!g;
        } elsif ($esc_mode eq 'none') {
            # no escaping
        } else {
            # default
            $word =~ s!([^A-Za-z0-9,+._/:\$~-])!\\$1!g;
        }
        push @words, $word;
        push @summaries, $summary;
        $has_summary = 1 if length $summary;
    }

    my $summary_align = $ENV{COMPLETE_BASH_SUMMARY_ALIGN} // 'left';
    my $max_columns = $ENV{COMPLETE_BASH_MAX_COLUMNS} // 0;
    my $terminal_width = _terminal_width();
    my $column_width = _column_width($terminal_width, $max_columns);

    #warn "terminal_width=$terminal_width, column_width=".($column_width // 'undef')."\n";

  FORMAT_SUMMARIES: {
        @res = @words;
        last if @words <= 1;
        last unless $has_summary;
        last unless $opts->{show_summaries} //
            $ENV{COMPLETE_BASH_SHOW_SUMMARIES} // 1;
        my $max_entry_width   = 8;
        my $max_summ_width = 0;
        for (0..$#words) {
            $max_entry_width = length $words[$_]
                if $max_entry_width < length $words[$_];
            $max_summ_width = length $summaries[$_]
                if $max_summ_width < length $summaries[$_];
        }

lib/Complete/Bash.pm  view on Meta::CPAN

                goto RETURN_RES;
            };

        print CHLD_IN map { "$_:$res[$_]\n" } 0..$#res;
        close CHLD_IN;

        my @res_words;
        while (<CHLD_OUT>) {
            my ($index) = /\A([0-9]+)\:/ or next;
            push @res_words, $words[$index];
        }
        if (@res_words) {
            @res = join(" ", @res_words);
        } else {
            @res = ();
        }
        waitpid($pid, 0);
    }

  RETURN_RES:
    #use Data::Dump; warn Data::Dump::dump(\@res);
    if ($as eq 'array') {
        return \@res;
    } else {
        return join("", map {($_, "\n")} @res);
    }
}

1;
# ABSTRACT: Completion routines for bash shell

__END__

=pod

=encoding UTF-8

=head1 NAME

Complete::Bash - Completion routines for bash shell

=head1 VERSION

This document describes version 0.337 of Complete::Bash (from Perl distribution Complete-Bash), released on 2022-09-08.

=head1 DESCRIPTION

This module provides routines related to tab completion in bash shell.

=head2 About programmable completion in bash

Bash allows completion to come from various sources. The simplest is from a list
of words (C<-W>):

 % complete -W "one two three four" somecmd
 % somecmd t<Tab>
 two  three

Another source is from a bash function (C<-F>). The function will receive input
in two variables: C<COMP_WORDS> (array, command-line chopped into words) and
C<COMP_CWORD> (integer, index to the array of words indicating the cursor
position). It must set an array variable C<COMPREPLY> that contains the list of
possible completion:

 % _foo()
 {
   local cur
   COMPREPLY=()
   cur=${COMP_WORDS[COMP_CWORD]}
   COMPREPLY=($( compgen -W '--help --verbose --version' -- $cur ) )
 }
 % complete -F _foo foo
 % foo <Tab>
 --help  --verbose  --version

And yet another source is an external command (C<-C>) including, from a Perl
script. The command receives two environment variables: C<COMP_LINE> (string,
raw command-line) and C<COMP_POINT> (integer, cursor location). Program must
split C<COMP_LINE> into words, find the word to be completed, complete that, and
return the list of words one per-line to STDOUT. An example:

 % cat foo-complete
 #!/usr/bin/perl
 use Complete::Bash qw(parse_cmdline format_completion);
 use Complete::Util qw(complete_array_elem);
 my ($words, $cword) = @{ parse_cmdline() };
 my $res = complete_array_elem(array=>[qw/--help --verbose --version/], word=>$words->[$cword]);
 print format_completion($res);

 % complete -C foo-complete foo
 % foo --v<Tab>
 --verbose --version

=head2 About the routines in this module

First of all, C<parse_cmdline()> is the function to parse raw command-line (such
as what you get from bash in C<COMP_LINE> environment variable) into words. This
makes it easy for the other functions to generate completion answer. See the
documentation for that function for more details.

C<format_completion()> is what you use to format completion answer structure for
bash.

=head1 FUNCTIONS


=head2 format_completion

Usage:

 format_completion($completion, $opts) -> str|array

Format completion for output (for shell).

Bash accepts completion reply in the form of one entry per line to STDOUT. Some
characters will need to be escaped. This function helps you do the formatting,
with some options.

This function accepts completion answer structure as described in the C<Complete>
POD. Aside from C<words>, this function also recognizes these keys:

This function is not exported by default, but exportable.

Arguments ('*' denotes required arguments):

=over 4

=item * B<$completion>* => I<hash|array>

Completion answer structure.

Either an array or hash. See function description for more details.

=item * B<$opts> => I<hash>

Specify options.

Known options:

lib/Complete/Bash.pm  view on Meta::CPAN


then the parse result will be:

["command", "--foo", "=", "bar", "http", ":", "//example.com", ":", "80", "Foo", "::", "Bar"]

which is annoying sometimes. But we follow bash here so we can more easily
accept input from a joined C<COMP_WORDS> if we write completion bash functions,
e.g. (in the example, C<foo> is a Perl script):

I<foo ()
{
    local words=(${COMP>CWORDS[@]})
    # add things to words, etc
    local point=... # calculate the new point
    COMPREPLY=( C<COMP_LINE="foo ${words[@]}" COMP_POINT=$point foo> )
}

To avoid these word-breaking characters to be split/grouped, we can escape
them with backslash or quote them, e.g.:

command "http://example.com:80" Foo\:\:Bar

which bash will parse as:

["command", "\"http://example.com:80\"", "Foo\:\:Bar"]

and we parse as:

["command", "http://example.com:80", "Foo::Bar"]

=item * Due to the way bash parses the command line (see above), the two below are
equivalent:

% cmd --foo=bar
% cmd --foo = bar

=back

Because they both expand to C<['--foo', '=', 'bar']>. But obviously
L<Getopt::Long> does not regard the two as equivalent.

This function is not exported by default, but exportable.

Arguments ('*' denotes required arguments):

=over 4

=item * B<$cmdline> => I<str>

Command-line, defaults to COMP_LINE environment.

=item * B<$opts> => I<hash>

Options.

Optional. Known options:

=over

=item * C<truncate_current_word> (bool). If set to 1, will truncate current word to the
position of cursor, for example (C<^> marks the position of cursor):
C<--vers^oo> to C<--vers> instead of C<--versoo>. This is more convenient when
doing tab completion.

=back

=item * B<$point> => I<int>

PointE<sol>position to complete in command-line, defaults to COMP_POINT.


=back

Return value:  (array)


Return a 2-element array: C<[$words, $cword]>. C<$words> is array of str,
equivalent to C<COMP_WORDS> provided by bash to shell functions. C<$cword> is an
integer, roughly equivalent to C<COMP_CWORD> provided by bash to shell functions.
The word to be completed is at C<< $words-E<gt>[$cword] >>.

Note that COMP_LINE includes the command name. If you want the command-line
arguments only (like in C<@ARGV>), you need to strip the first element from
C<$words> and reduce C<$cword> by 1.



=head2 point

Usage:

 point($cmdline, $marker) -> any

Return line with point marked by a marker.

This is a utility function useful for testing/debugging. C<parse_cmdline()>
expects a command-line and a cursor position (C<$line>, C<$point>). This routine
expects C<$line> with a marker character (by default it's the caret, C<^>) and
return (C<$line>, C<$point>) to feed to C<parse_cmdline()>.

Example:

 point("^foo") # => ("foo", 0)
 point("fo^o") # => ("foo", 2)

This function is not exported by default, but exportable.

Arguments ('*' denotes required arguments):

=over 4

=item * B<$cmdline> => I<str>

Command-line which contains a marker character.

=item * B<$marker> => I<str> (default: "^")

Marker character.


=back

Return value:  (any)

=head1 ENVIRONMENT

=head2 COMPLETE_BASH_DEFAULT_ESC_MODE

Str. To provide default for the C<esc_mode> option in L</format_completion>.

=head2 COMPLETE_BASH_FZF

Bool. Whether to pass large completion answer to fzf instead of directly passing
it to bash and letting bash page it with a simpler more-like internal pager. By
default, large is defined as having at least 100 items (same bash's
C<completion-query-items> setting). This can be configured via
L</COMPLETE_BASH_FZF_ITEMS>.

Will not pass to fzf if inside emacs (C<INSIDE_EMACS> environment is true).

=head2 COMPLETE_BASH_FZF_ITEMS

Uint. Default 100. The minimum number of items to trigger passing completion
answer to C<fzf>.

A special value of -1 means to use terminal height. However, since terminal
height (and width) normally cannot be read during tab completion anyway, it's
better if you do something like this in your bash startup file:

 export COMPLETE_BASH_FZF_ITEMS=$LINES

because without passing to C<fzf>, as soon as the number of completion answers
exceeds C<$LINES>, C<bash> will start paging the answer to its internal pager,
which is limited like C<more>. If you set the above, then as soon as the number
of completion answers exceeds terminal height, you will avoid the bash internal
pager and use the nicer C<fzf>.



( run in 1.646 second using v1.01-cache-2.11-cpan-39bf76dae61 )