current commit
181201.1 · Remove the --lines option
#!/usr/bin/env perl

use strict;
use warnings;
use lib 'blib/lib';
use lib 't';

use Template;
use Util;

# XXX special completion for --type-add?
# XXX don't complete mutually exclusive options?
# XXX don't complete repeat options? (bash)
# XXX don't do anything post -- (bash)
# XXX handle option arguments (zsh)

my %ZSH_DESCRIPTIONS = (
    '--ackrc'                 => 'specify an alternative ackrc',
    '--after-context'         => 'print NUM lines of trailing context after matching lines',
    '--bar'                   => 'check with the admiral for traps',
    '--before-context'        => 'print NUM lines of leading context before matching lines',
    '--break'                 => 'print a break between results for different files',
    '--cathy'                 => 'chocolate, chocolate, chocolate!' ,
    '--color'                 => 'highlight matching text',
    '--color-filename'        => 'set color for filenames',
    '--color-lineno'          => 'set color for line numbers',
    '--color-match'           => 'set color for matches',
    '--column'                => 'show column number of first match',
    '--context'               => 'print NUM lines of context around matching lines',
    '--count'                 => 'print a count of matching lines for each input file',
    '--create-ackrc'          => 'dumps default ack options to standard output',
    '--dump'                  => 'writes the list of loaded options and where they came from',
    '--files-from'            => 'specifies the list of files to search within FILE',
    '--files-with-matches'    => 'only print filenames of matching files',
    '--files-without-matches' => 'only print filenames of non-matching files',
    '--filter'                => 'force ack to behave as if it were receiving input via a pipe',
    '--flush'                 => 'flushes output immediately',
    '--follow'                => 'follow symlinks',
    '--group'                 => 'group matches by filename',
    '--heading'               => q{print a filename heading above each file's result},
    '--help'                  => 'displays help',
    '--help-types'            => 'print all known types',
    '--ignore-ack-defaults'   => 'ignore default definitions provided with ack',
    '--ignore-case'           => 'ignore case in matches',
    '--ignore-directory'      => 'ignore directories (at any level) named DIRNAME',
    '--ignore-file'           => 'ignore files matching FILTERTYPE:FILTERARGS',
    '--invert-match'          => 'select non-matching lines',
    '--literal'               => 'quote all metacharacters in PATTERN',
    '--man'                   => 'displays the ack man page',
    '--match'                 => 'specify PATTERN explicitly',
    '--max-count'             => 'stop reading after NUM matches',
    '--no-filename'           => q{don't print filenames on output},
    '--noenv'                 => q{don't consider ackrc files/environment variables for configuration},
    '--output'                => 'output the evaluation of EXPR for each line',
    '--pager'                 => q{direct ack's output through PAGER},
    '--passthru'              => 'print all lines, whether or not they match the expression, highlighting matches',
    '--print0'                => 'separate output lines with NUL characters',
    '--recurse'               => 'recurse into subdirectories',
    '--show-types'            => 'outputs the filetypes that ack associates with each file',
    '--smart-case'            => 'ignores case in search strings if PATTERN contains no uppercase characters',
    '--sort-files'            => 'sorts found files lexicographically',
    '--thpppt'                => 'display the Bill The Cat logo.',
    '--type-add'              => 'add a type definition',
    '--type-del'              => 'removes a type definition',
    '--type-set'              => 'add a type definition',
    '--version'               => 'displays version and copyright information',
    '--with-filename'         => 'print the filename for each match',
    '--word-regexp'           => 'force PATTERN to match only whole words',
    '-1'                      => 'stops after reporting first match',
    '-f'                      => 'only print files that would be searched',
    '-g'                      => 'only print files whose names match PATTERN',
    '-o'                      => 'only print the part of each matching line that matches PATTERN',
    '-s'                      => 'suppress error messages about nonexistent or unreadable files',
    '-x'                      => 'search files specified on standard input',
);

my %OPTION_ALIASES = (
    '--after-context'         => ['-A'],
    '--before-context'        => ['-B'],
    '--context'               => ['-C'],
    '--count'                 => ['-c'],
    '--no-filename'           => ['-h'],
    '--with-filename'         => ['-H'],
    '--files-with-matches'    => ['-l'],
    '--files-without-matches' => ['-L'],
    '--max-count'             => ['-m'],
    '--no-recurse'            => ['-n'],
    '--help'                  => ['-?'],
    '--ignore-case'           => ['-i'],
    '--literal'               => ['-Q'],
    '--recurse'               => ['-r', '-R'],
    '--invert-match'          => ['-v'],
    '--word-regexp'           => ['-w'],
    '--color'                 => ['--colour'],
    '--ignore-directory'      => ['--ignore-dir'],
);

my %IS_AN_ALIAS = map { $_ => 1 } map { @$_ } values %OPTION_ALIASES;

my $BASH_TEMPLATE = <<'END_TEMPLATE';
declare -g -a _ack_options
declare -g -a _ack_types=()

_ack_options=(
[% FOREACH option IN options -%]
  "[% option -%]" \
[% END -%]
)

function __setup_ack() {
    local type

    while read LINE; do
        case $LINE in
            --*)
                type="${LINE%% *}"
                type=${type/--\[no\]/}
                _ack_options[ ${#_ack_options[@]} ]="--$type"
                _ack_options[ ${#_ack_options[@]} ]="--no$type"
                _ack_types[ ${#_ack_types[@]} ]="$type"
            ;;
        esac
    done < <(ack --help-types)
}
__setup_ack
unset -f __setup_ack

function _ack_complete() {
    local current_word
    local pattern

    current_word=${COMP_WORDS[$COMP_CWORD]}

    if [[ "$current_word" == -* ]]; then
        pattern="${current_word}*"
        for option in ${_ack_options[@]}; do
            if [[ "$option" == $pattern ]]; then
                COMPREPLY[ ${#COMPREPLY[@]} ]=$option
            fi
        done
    else
        local previous_word
        previous_word=${COMP_WORDS[$(( $COMP_CWORD - 1 ))]}
        if [[ "$previous_word" == "=" ]]; then
            previous_word=${COMP_WORDS[$(( $COMP_CWORD - 2 ))]}
        fi

        if [ "$previous_word" == '--type' -o "$previous_word" == '--notype' ]; then
            pattern="${current_word}*"
            for type in ${_ack_types[@]}; do
                if [[ "$type" == $pattern ]]; then
                    COMPREPLY[ ${#COMPREPLY[@]} ]=$type
                fi
            done
        fi
    fi
}

complete -o default -F _ack_complete ack ack2 ack-grep
END_TEMPLATE

my $ZSH_TEMPLATE = <<'END_TEMPLATE';
#compdef ack ack2 ack-grep

declare -a ack_types
declare -a arguments
local type

arguments=( \
[% FOREACH option IN options -%]
    [% IF option != '--type' && option != '--notype' && !is_alias.exists(option) -%]
        [% FILTER collapse -%]
            [%- IF aliases.exists(option) -%]
{[% option -%],[% aliases.$option.join(',') -%]}
            [%- ELSE -%]
"[% option -%]"
            [%- END -%]
            [%- IF descriptions.exists(option) -%]
"[[% descriptions.$option -%]]"
            [%- END -%]
        [% END -%]
        \
    [% END -%]
[% END -%]
)

while read LINE; do
    case $LINE in
        --*)
            type="${LINE%% *}"
            type=${type/--\[no\]/}
            arguments[$(( ${#arguments[@]} + 1 ))]="--${type}[restrict to files of type $type]"
            arguments[$(( ${#arguments[@]} + 1 ))]="--no${type}[restrict to files other than type $type]"
            ack_types[$(( ${#ack_types[@]} + 1 ))]="$type"
        ;;
    esac
done < <(ack --help-types)

arguments[$(( ${#arguments[@]} + 1 ))]="--type=[restrict to files of given type]:filetype:(${ack_types[@]})"
arguments[$(( ${#arguments[@]} + 1 ))]="--notype=[restrict to files other than given type]:notfiletype:(${ack_types[@]})"
arguments[$(( ${#arguments[@]} + 1 ))]="*:files:_files"

_arguments -S $arguments
END_TEMPLATE

my $tt = Template->new({});

# XXX do a sanity check on ZSH_DESCRIPTIONS vs get_expected_options, OPTION_ALIASES

my $vars = {
    options      => [ get_expected_options() ],
    descriptions => \%ZSH_DESCRIPTIONS,
    aliases      => \%OPTION_ALIASES,
    is_alias     => \%IS_AN_ALIAS,
};

my ( $filename ) = @ARGV;

if ( !$filename ) {
    die "usage: $0 (completion.bash|completion.zsh)\n";
}
elsif ( $filename eq 'completion.bash' ) {
    $tt->process(\$BASH_TEMPLATE, $vars, 'completion.bash') || die $tt->error;
}
elsif ( $filename eq 'completion.zsh' ) {
    $tt->process(\$ZSH_TEMPLATE, $vars, 'completion.zsh')   || die $tt->error;
}
else {
    die "I don't know how to generate $filename\n";
}