our
$VERSION
=
'v0.14.1'
;
sub
generate_completion {
my
(
$self
,
%args
) =
@_
;
my
$spec
=
$self
->spec;
my
$appname
=
$spec
->name;
my
$appspec_version
= App::Spec->VERSION;
my
$functions
= [];
my
$completion_outer
=
$self
->completion_commands(
commands
=>
$spec
->subcommands,
options
=>
$spec
->options,
parameters
=>
$spec
->parameters,
level
=> 1,
functions
=>
$functions
,
);
my
$global_options
=
$spec
->options;
my
(
$flags_string
,
$options_string
) =
$self
->flags_options(
$global_options
);
my
$body
=
<<"EOM";
#!/usr/bin/env bash
# Generated with perl module App::Spec v$appspec_version
_$appname() \{
COMPREPLY=()
local program=$appname
local cur prev words cword
_init_completion -n : || return
declare -a FLAGS
declare -a OPTIONS
declare -a MYWORDS
local INDEX=`expr \$cword - 1`
MYWORDS=("\$\{words[@]:1:\$cword\}")
FLAGS=($flags_string)
OPTIONS=($options_string)
__${appname}_handle_options_flags
$completion_outer
\}
_${appname}_compreply() \{
local prefix=""
cur="\$(printf '%q' "\$cur")"
IFS=\$'\\n' COMPREPLY=(\$(compgen -P "\$prefix" -W "\$*" -- "\$cur"))
__ltrim_colon_completions "\$prefix\$cur"
if [[ \$\{#COMPREPLY[*]\} -eq 1 ]]; then # Only one completion
COMPREPLY=( "\$\{COMPREPLY[0]%% -- *\}" ) # Remove ' -- ' and everything after
COMPREPLY=( "\$\{COMPREPLY[0]%%+( )\}" ) # Remove trailing spaces
fi
\}
@{[ join '', @$functions ]}
EOM
my
$static_functions
=
$self
->_functions;
$body
.=
<<"EOM";
$static_functions
complete -o default -F _$appname $appname
EOM
return
$body
;
}
sub
flags_options {
my
(
$self
,
$options
) =
@_
;
my
@flags
;
my
@opt
;
for
my
$o
(
@$options
) {
my
$name
=
$o
->name;
my
$aliases
=
$o
->aliases;
my
$summary
=
$o
->summary;
my
@names
= (
$name
,
@$aliases
);
(
$summary
,
@names
) =
$self
->escape_singlequote(
$summary
,
@names
);
@names
=
map
{
length
$_
> 1 ?
"--$_"
:
"-$_"
}
@names
;
my
@items
=
map
{
(
"'$_'"
,
"'$summary'"
)
}
@names
;
if
(
$o
->type eq
'flag'
) {
push
@flags
,
@items
;
}
else
{
push
@opt
,
@items
;
}
}
return
(
"@flags"
,
"@opt"
);
}
sub
escape_singlequote {
my
(
$self
,
@strings
) =
@_
;
my
@result
;
for
my
$string
(
@strings
) {
no
warnings
'uninitialized'
;
$string
=~ s/[
']/'
"\\\\'"
'/g;
push
@result
,
$string
;
}
return
wantarray
?
@result
:
$result
[0];
}
sub
completion_commands {
my
(
$self
,
%args
) =
@_
;
my
$spec
=
$self
->spec;
my
$appname
=
$spec
->name;
my
$functions
=
$args
{functions};
my
$previous
=
$args
{previous} || [];
my
$commands
=
$args
{commands};
my
$options
=
$args
{options};
my
$parameters
=
$args
{parameters};
my
$level
=
$args
{level};
my
$indent
=
" "
x
$level
;
my
@commands
=
map
{
my
$name
=
$_
;
my
$summary
=
$commands
->{
$_
}->summary;
for
(
$name
,
$summary
) {
no
warnings
'uninitialized'
;
s/[
'`]/'
"'"
'/g;
s/\$/\\\$/g;
}
"'$name'"
. (
length
$summary
?
q{$'\t'}
.
"'$summary'"
:
''
)
}
sort
grep
{ not m/^_/ }
keys
%$commands
;
my
$cmds
=
join
q{$'\\n'}
,
@commands
;
my
$index
=
$level
- 1;
my
$subc
=
''
;
if
(
keys
%$commands
) {
$subc
=
<<"EOM";
$indent# subcmds
${indent}case \$\{MYWORDS\[$index\]\} in
EOM
}
for
my
$name
(
sort
keys
%$commands
) {
my
$cmd_spec
=
$commands
->{
$name
};
my
(
$flags_string
,
$options_string
) =
$self
->flags_options(
$cmd_spec
->options);
$subc
.=
<<"EOM";
${indent} $name)
EOM
$subc
.=
$indent
.
" FLAGS+=($flags_string)\n"
if
$flags_string
;
$subc
.=
$indent
.
" OPTIONS+=($options_string)\n"
if
$options_string
;
$subc
.=
<<"EOM";
${indent} __${appname}_handle_options_flags
EOM
my
$subcommands
=
$cmd_spec
->subcommands;
my
$parameters
=
$cmd_spec
->parameters;
my
$cmd_options
=
$cmd_spec
->options;
if
(
keys
%$subcommands
or
@$cmd_options
or
@$parameters
) {
my
$comp
=
$self
->completion_commands(
commands
=>
$subcommands
,
options
=> [
@$options
,
@$cmd_options
],
parameters
=>
$parameters
,
level
=>
$level
+ 1,
previous
=> [
@$previous
,
$name
],
functions
=>
$functions
,
);
$subc
.=
$comp
;
}
else
{
$subc
.=
$indent
.
" __comp_current_options true || return # no subcmds, no params/opts\n"
;
}
$subc
.=
<<"EOM";
${indent} ;;
EOM
}
my
$option_comp
;
my
$param_comp
=
''
;
my
$subc_comp
=
''
;
if
(
@$options
) {
(
$option_comp
) =
$self
->completion_options(
options
=>
$options
,
level
=>
$level
,
functions
=>
$args
{functions},
previous
=>
$previous
,
);
}
if
(
@$parameters
) {
$param_comp
=
$self
->completion_parameters(
parameters
=>
$parameters
,
level
=>
$level
,
previous
=>
$previous
,
functions
=>
$functions
,
);
$param_comp
=
<<"EOM";
$param_comp
EOM
}
if
(
keys
%$commands
) {
$subc
.=
<<"EOM";
${indent}esac
EOM
$subc_comp
=
<<"EOM";
${indent}case \$INDEX in
${indent}$index)
${indent} __comp_current_options || return
${indent} __${appname}_dynamic_comp 'commands' $cmds
${indent};;
${indent}*)
$subc
${indent};;
${indent}esac
EOM
return
$subc_comp
;
}
my
$completion
=
<<"EOM";
${indent}case \$\{MYWORDS[\$INDEX-1]\} in
$option_comp
${indent}esac
${indent}case \$INDEX in
$param_comp
${indent}*)
${indent} __comp_current_options || return
${indent};;
${indent}esac
EOM
return
$completion
;
}
sub
completion_parameters {
my
(
$self
,
%args
) =
@_
;
my
$spec
=
$self
->spec;
my
$appname
=
$spec
->name;
my
$parameters
=
$args
{parameters};
my
$level
=
$args
{level};
my
$indent
=
" "
x
$level
;
my
$comp
=
''
;
for
my
$i
(0 ..
$#$parameters
) {
my
$param
=
$parameters
->[
$i
];
my
$name
=
$param
->name;
my
$num
=
$level
+
$i
- 1;
$comp
.=
$indent
.
" $num)\n"
;
$comp
.=
$indent
.
" __comp_current_options || return\n"
;
$comp
.=
$self
->completion_parameter(
parameter
=>
$param
,
level
=>
$level
+ 1,
functions
=>
$args
{functions},
previous
=>
$args
{previous},
);
$comp
.=
$indent
.
" ;;\n"
;
}
return
$comp
;
}
sub
completion_options {
my
(
$self
,
%args
) =
@_
;
my
$appname
=
$self
->spec->name;
my
$options
=
$args
{options};
my
$level
=
$args
{level};
my
$indent
=
" "
x
$level
;
my
@comp_options
;
my
@comp_values
;
my
$comp_value
=
''
;
my
$maxlength
= 0;
for
my
$opt
(
@$options
) {
my
$name
=
$opt
->name;
my
$aliases
=
$opt
->aliases;
my
@names
= (
$name
,
@$aliases
);
for
my
$n
(
@names
) {
my
$length
=
length
$n
;
$length
=
$length
> 1 ?
$length
+2 :
$length
+1;
$maxlength
=
$length
if
$length
>
$maxlength
;
}
}
for
my
$i
(0 ..
$#$options
) {
my
$opt
=
$options
->[
$i
];
my
$name
=
$opt
->name;
my
$type
=
$opt
->type;
next
if
$type
eq
"flag"
;
my
$enum
=
$opt
->enum;
my
$summary
=
$opt
->summary;
$summary
=~ s/[
'`]/'
"'"
'/g;
$summary
=~ s/\$/\\\$/g;
my
$aliases
=
$opt
->aliases;
my
@names
= (
$name
,
@$aliases
);
my
@option_strings
;
for
my
$n
(
@names
) {
my
$dash
=
length
$n
> 1 ?
"--"
:
"-"
;
my
$option_string
=
"$dash$n"
;
push
@option_strings
,
$option_string
;
my
$length
=
length
$option_string
;
$option_string
.=
" "
x (
$maxlength
-
$length
);
my
$string
=
length
$summary
?
qq{'$option_string -- $summary'}
:
qq{'$option_string'}
;
push
@comp_options
,
$string
;
}
$comp_value
.=
<<"EOM";
${indent} @{[ join '|', @option_strings ]})
EOM
if
(
$enum
) {
my
@list
=
@$enum
;
for
(
@list
) {
s/[
'`]/'
"'"
'/g;
s/\\/\\\\/g;
s/ /\\\\\\\\ /g;
s/\$/\\\$/g;
$_
=
qq{"$_"}
;
}
$comp_value
.=
<<"EOM";
${indent} _${appname}_compreply @list
${indent} return
EOM
}
elsif
(
$type
=~ m/^file(name)?\z/) {
$comp_value
.=
<<"EOM";
${indent} compopt -o filenames
${indent} return
EOM
}
elsif
(
$type
=~ m/^dir(name)?\z/) {
$comp_value
.=
<<"EOM";
${indent} compopt -o dirnames
${indent} return
EOM
}
elsif
(
$opt
->completion) {
my
$function_name
=
$self
->dynamic_completion(
option
=>
$opt
,
level
=>
$level
,
previous
=>
$args
{previous},
functions
=>
$args
{functions},
);
$comp_value
.=
<<"EOM";
${indent} $function_name
EOM
}
$comp_value
.=
$indent
.
" ;;\n"
;
}
return
(
$comp_value
);
}
sub
dynamic_completion {
my
(
$self
,
%args
) =
@_
;
my
$functions
=
$args
{functions};
my
$previous
=
$args
{previous} || [];
my
$p
=
$args
{option};
my
$level
=
$args
{level};
my
$indent
=
' '
x
$level
;
my
$name
=
$p
->name;
my
$shell_name
=
$name
;
$name
=~
tr
/^A-Za-z0-9_:-/_/c;
$shell_name
=~
tr
/^A-Za-z0-9_/_/c;
my
$def
=
$p
->completion;
my
(
$op
,
$command
,
$command_string
);
if
(not
ref
$def
and
$def
== 1) {
my
$possible_values
=
$p
->
values
or
die
"Error for '$name': completion: 1 but 'values' not defined"
;
$op
=
$possible_values
->{op} or
die
"Error for '$name': 'values' needs an 'op'"
;
}
elsif
(
ref
$def
) {
$op
=
$def
->{op};
$command
=
$def
->{command};
$command_string
=
$def
->{command_string};
}
else
{
die
"Error for '$name': invalid value for 'completion'"
;
}
my
$appname
=
$self
->spec->name;
my
$function_name
=
"_${appname}_"
.
join
(
"_"
,
@$previous
)
.
"_"
. (
$p
->isa(
"App::Spec::Option"
) ?
"option"
:
"param"
)
.
"_"
.
$shell_name
.
"_completion"
;
my
$function
;
if
(
$op
) {
$function
=
<<"EOM";
$function_name() \{
local __dynamic_completion
__dynamic_completion=\$(PERL5_APPSPECRUN_SHELL=bash PERL5_APPSPECRUN_COMPLETION_PARAMETER='$name' \${words[@]})
__${appname}_dynamic_comp '$name' "\$__dynamic_completion"
\}
EOM
}
elsif
(
$command
or
$command_string
) {
my
$string
=
''
;
if
(
$command
) {
my
@args
;
for
my
$arg
(
@$command
) {
unless
(
ref
$arg
) {
push
@args
,
"'$arg'"
;
next
;
}
if
(
my
$replace
=
$arg
->{replace}) {
if
(
ref
$replace
eq
'ARRAY'
) {
my
@repl
=
@$replace
;
if
(
$replace
->[0] eq
'SHELL_WORDS'
) {
my
$num
=
$replace
->[1];
my
$index
=
"\$cword"
;
if
(
$num
ne
'CURRENT'
) {
if
(
$num
=~ m/^-/) {
$index
.=
$num
;
}
else
{
$index
=
$num
- 1;
}
}
my
$string
=
qq{"\$\{words\[$index\]\}
"};
push
@args
,
$string
;
}
}
else
{
if
(
$replace
eq
"SELF"
) {
push
@args
,
"\$program"
;
}
}
}
}
$string
=
"@args"
;
}
elsif
(
defined
$command_string
) {
$string
=
$command_string
;
}
my
$varname
=
"__${name}_completion"
;
chomp
$string
;
$function
=
<<"EOM";
$function_name() \{
local CURRENT_WORD="\${words\[\$cword\]\}"
local param_$shell_name="\$($string)"
_${appname}_compreply "\$param_$shell_name"
\}
EOM
}
push
@$functions
,
$function
;
return
$function_name
;
}
sub
completion_parameter {
my
(
$self
,
%args
) =
@_
;
my
$spec
=
$self
->spec;
my
$appname
=
$spec
->name;
my
$param
=
$args
{parameter};
my
$name
=
$param
->name;
my
$level
=
$args
{level};
my
$indent
=
" "
x
$level
;
my
$comp
=
''
;
my
$type
=
$param
->type;
my
$enum
=
$param
->enum;
if
(
$enum
) {
my
@list
=
@$enum
;
for
(
@list
) {
s/[
'`]/'
"'"
'/g;
s/\\/\\\\/g;
s/ /\\\\ /g;
s/\$/\\\$/g;
$_
=
qq{"$_"}
;
}
$comp
=
<<"EOM";
${indent} _${appname}_compreply @list
EOM
}
elsif
(
$type
=~ m/^file(name)?\z/) {
$comp
=
<<"EOM";
${indent} compopt -o filenames
EOM
}
elsif
(
$type
=~ m/^dir(name)?\z/) {
$comp
=
<<"EOM";
${indent} compopt -o dirnames
EOM
}
elsif
(
$param
->completion) {
my
$function_name
=
$self
->dynamic_completion(
option
=>
$param
,
level
=>
$level
,
previous
=>
$args
{previous},
functions
=>
$args
{functions},
);
$comp
.=
<<"EOM";
${indent} $function_name
EOM
}
return
$comp
;
}
sub
_functions {
my
(
$self
) =
@_
;
my
$string
=
<<'EOM';
__APPNAME_dynamic_comp() {
local argname="$1"
local arg="$2"
local name desc cols desclength formatted
local comp=()
local max=0
while read -r line; do
name="$line"
desc="$line"
name="${name%$'\t'*}"
if [[ "${#name}" -gt "$max" ]]; then
max="${#name}"
fi
done <<< "$arg"
while read -r line; do
name="$line"
desc="$line"
name="${name%$'\t'*}"
desc="${desc/*$'\t'}"
if [[ -n "$desc" && "$desc" != "$name" ]]; then
# TODO portable?
cols=`tput cols`
[[ -z $cols ]] && cols=80
desclength=`expr $cols - 4 - $max`
formatted=`printf "%-*s -- %-*s" "$max" "$name" "$desclength" "$desc"`
comp+=("$formatted")
else
comp+=("'$name'")
fi
done <<< "$arg"
_APPNAME_compreply ${comp[@]}
}
function __APPNAME_handle_options() {
local i j
declare -a copy
local last="${MYWORDS[$INDEX]}"
local max=`expr ${#MYWORDS[@]} - 1`
for ((i=0; i<$max; i++))
do
local word="${MYWORDS[$i]}"
local found=
for ((j=0; j<${#OPTIONS[@]}; j+=2))
do
local option="${OPTIONS[$j]}"
if [[ "$word" == "$option" ]]; then
found=1
i=`expr $i + 1`
break
fi
done
if [[ -n $found && $i -lt $max ]]; then
INDEX=`expr $INDEX - 2`
else
copy+=("$word")
fi
done
MYWORDS=("${copy[@]}" "$last")
}
function __APPNAME_handle_flags() {
local i j
declare -a copy
local last="${MYWORDS[$INDEX]}"
local max=`expr ${#MYWORDS[@]} - 1`
for ((i=0; i<$max; i++))
do
local word="${MYWORDS[$i]}"
local found=
for ((j=0; j<${#FLAGS[@]}; j+=2))
do
local flag="${FLAGS[$j]}"
if [[ "$word" == "$flag" ]]; then
found=1
break
fi
done
if [[ -n $found ]]; then
INDEX=`expr $INDEX - 1`
else
copy+=("$word")
fi
done
MYWORDS=("${copy[@]}" "$last")
}
__APPNAME_handle_options_flags() {
__APPNAME_handle_options
__APPNAME_handle_flags
}
__comp_current_options() {
local always="$1"
if [[ -n $always || ${MYWORDS[$INDEX]} =~ ^- ]]; then
local options_spec=''
local j=
for ((j=0; j<${#FLAGS[@]}; j+=2))
do
local name="${FLAGS[$j]}"
local desc="${FLAGS[$j+1]}"
options_spec+="$name"$'\t'"$desc"$'\n'
done
for ((j=0; j<${#OPTIONS[@]}; j+=2))
do
local name="${OPTIONS[$j]}"
local desc="${OPTIONS[$j+1]}"
options_spec+="$name"$'\t'"$desc"$'\n'
done
__APPNAME_dynamic_comp 'options' "$options_spec"
return 1
else
return 0
fi
}
EOM
my
$appname
=
$self
->spec->name;
$string
=~ s/APPNAME/
$appname
/g;
return
$string
;
}
1;