public function generateCompleteFunction(Buffer $buf, CommandBase $cmd, $compPrefix) { $funcSuffix = command_signature_suffix($cmd); $buf->appendLine("{$compPrefix}_complete_{$funcSuffix} ()"); $buf->appendLine("{"); $buf->appendLine(local_bash_var('comp_prefix', $compPrefix)); $buf->append(' local cur words cword prev _get_comp_words_by_ref -n =: cur words cword prev local command_signature=$1 local command_index=$2 ((command_index++)) # Output application command alias mapping # aliases[ alias ] = command declare -A subcommand_alias # Define the command names declare -A subcommands declare -A subcommand_signs # option names defines the available options of this command declare -A options # options_require_value: defines the required completion type for each # option that requires a value. declare -A options_require_value '); $subcommands = $cmd->getCommands(); $subcommandAliasMap = array(); $subcommandDescMap = array(); $commandOptionMap = array(); $commandOptionRequireValueMap = array(); $commandSignMap = array(); foreach ($subcommands as $subcommand) { foreach ($subcommand->aliases() as $alias) { $subcommandAliasMap[$alias] = $subcommand->getName(); } $subcommandDescMap[$subcommand->getName()] = $subcommand->brief(); $commandSignMap[$subcommand->getName()] = command_signature_suffix($subcommand); } // Command signature is used for fetching meta information from the meta command. // And a command description map // // subcommands=(["add"]="command to add" ["commit"]="command to commit") // subcommand_alias=(["a"]="add" ["c"]="commit") // $buf->appendLine(set_bash_array('subcommands', $subcommandDescMap)); $buf->appendLine(set_bash_array('subcommand_alias', $subcommandAliasMap)); $buf->appendLine(set_bash_array('subcommand_signs', $commandSignMap)); // Generate the bash array for command options // // options=(["--debug"]=1 ["--verbose"]=1 ["--log-dir"]=1) // $options = $cmd->getOptionCollection(); foreach ($options as $option) { if ($option->short) { $commandOptionMap['-' . $option->short] = 1; } if ($option->long) { $commandOptionMap['--' . $option->long] = 1; } if ($option->required || $option->multiple) { if ($option->short) { $commandOptionRequireValueMap['-' . $option->short] = 1; } if ($option->long) { $commandOptionRequireValueMap['--' . $option->long] = 1; } } } $buf->appendLine(set_bash_array('options', $commandOptionMap)); // options_require_value=(["--log-dir"]="__complete_directory") $buf->appendLine(set_bash_array('options_require_value', $commandOptionRequireValueMap)); // local argument_min_length=0 $argInfos = $cmd->getArgInfoList(); // $buf->appendLine("local argument_min_length=" . count($argInfos)); $buf->appendLine(local_bash_var('argument_min_length', count($argInfos))); $buf->append(' # Get the command name chain of the current input, e.g. # # app asset install [arg1] [arg2] [arg3] # app commit add # # The subcommand dispatch should be done in the command complete function, # not in the root completion function. # We should pass the argument index to the complete function. # command_index=1 start from the first argument, not the application name # Find the command position local argument_index=0 local i local command local found_options=0 # echo "[DEBUG] command_index: [$command_signature] [$command_index]" while [ $command_index -lt $cword ]; do i="${words[command_index]}" case "$i" in # Ignore options --=*) found_options=1 ;; --*) found_options=1 ;; -*) found_options=1 ;; *) # looks like my command, that\'s break the loop and dispatch to the next complete function if [[ -n "$i" && -n "${subcommands[$i]}" ]] ; then command="$i" break elif [[ -n "$i" && -n "${subcommand_alias[$i]}" ]] ; then command="$i" break elif [[ $command_index -gt 1 ]] ; then # If the command is not found, check if the previous argument is an option expecting a value # or it is an argument # the previous argument (might be) p="${words[command_index-1]}" # not an option value, push to the argument list if [[ -z "${options_require_value[$p]}" ]] ; then # echo "[DEBUG] argument_index++ because of [$i]" ((argument_index++)) fi fi ;; esac ((command_index++)) done '); $buf->append(' # If the first command name is not found, we do complete... if [[ -z "$command" ]] ; then case "$cur" in # If the current argument $cur looks like an option, then we should complete -*) __mycomp "${!options[*]}" return ;; *) # The argument here can be an option value. e.g. --output-dir /tmp # The the previous one... if [[ -n "$prev" && -n "${options_require_value[$prev]}" ]] ; then # TODO: local complete_type="${options_require_value[$prev]"} '); $buf->appendLine(' __complete_meta "$command_signature" "opt" "${prev##*(-)}" "valid-values"'); $buf->appendLine(' return'); $buf->appendLine(' fi'); $buf->appendLine(' # If the command requires at least $argument_min_length to run, we check the argument'); if (count($argInfos) > 0) { $buf->appendLine('if [[ $argument_min_length > 0 ]] ; then'); // $buf->appendLine('echo argument_index: [$argument_index]'); // $buf->appendLine('echo cur: [$cur]'); // expand the argument case $buf->appendLine(' case $argument_index in'); foreach ($argInfos as $index => $a) { // TODO: when $a->multiple is enabled, we will use "*" for the case pattern. $pattern = $index; if ($a->multiple) { $pattern = '*'; } $buf->appendLine(" {$pattern})"); // $buf->appendLine('echo argument_index matched: [$argument_index]'); if ($a->validValues || $a->suggestions) { $values = array(); if ($a->validValues) { if (is_callable($a->validValues)) { $buf->appendLine(" __complete_meta \"\$command_signature\" \"arg\" {$index} \"valid-values\""); $buf->appendLine(' return'); } elseif ($values = $a->getValidValues()) { $buf->appendLine(' COMPREPLY=( $(compgen -W "' . join("\n", $values) . '" -- $cur) )'); $buf->appendLine(' return'); } } elseif ($a->suggestions) { if (is_callable($a->suggestions)) { $buf->appendLine(" __complete_meta \"\$command_signature\" \"arg\" {$index} \"suggestions\""); $buf->appendLine(' return'); } elseif ($values = $a->getSuggestions()) { $buf->appendLine(' COMPREPLY=( $(compgen -W "' . join("\n", $values) . '" -- $cur) )'); $buf->appendLine(' return'); } } } elseif (in_array($a->isa, array('file', 'path', 'dir'))) { $compopt = ''; switch ($a->isa) { case "file": $compopt .= ' -A file'; // $buf->appendLine('COMPREPLY=($(compgen -A file -- $cur))'); break; case "path": $compopt .= ' -A file'; break; case "command": $compopt .= ' -A command'; break; case "user": $compopt .= ' -A user'; break; case "service": $compopt .= ' -A service'; break; case "hostname": $compopt .= ' -A hostname'; break; case "job": $compopt .= ' -A job'; break; case "dir": case "directory": $compopt .= ' -A directory'; break; } // If the glob is specified, bash does not support for both -A with -G if ($a->glob) { $compopt = " -G \"{$a->glob}\""; } $buf->appendLine("COMPREPLY=(\$(compgen {$compopt} -- \$cur))"); $buf->appendLine("return"); } $buf->appendLine(" ;;"); } $buf->appendLine(' esac'); $buf->appendLine(' fi'); } $buf->append(' # If there is no argument support, then user is supposed to give a subcommand name or an option __mycomp "${!options[*]} ${!subcommands[*]} ${!subcommand_alias[*]}" return ;; esac '); // Dispatch $buf->append(' else # We just found the first command, we are going to dispatch the completion handler to the next level... # Rewrite command alias to command name to get the correct response if [[ -n "${subcommand_alias[$command]}" ]] ; then command="${subcommand_alias[$command]}" fi if [[ -n "${subcommand_signs[$command]}" ]] ; then local suffix="${subcommand_signs[$command]}" local completion_func="${comp_prefix}_complete_${suffix//-/_}" # Declare the completion function name and dispatch rest arguments to the complete function command_signature="${command_signature}.${command}" declare -f $completion_func >/dev/null && \\ $completion_func $command_signature $command_index && return else echo "Command \'$command\' not found" fi fi '); // Epilog $buf->appendLine("};"); }
public function complete_with_subcommands(CommandBase $cmd, $level = 1) { $cmdSignature = $cmd->getSignature(); $buf = new Buffer(); $buf->setIndent($level); $subcmds = $this->visible_commands($cmd->getCommands()); $descsBuf = $this->describe_commands($subcmds, $level); $code = array(); // $code[] = 'echo $words[$CURRENT-1]'; $buf->appendLine("_arguments -C \\"); $buf->indent(); if ($args = $this->command_flags($cmd, $cmdSignature)) { foreach ($args as $arg) { $buf->appendLine($arg . " \\"); } } $buf->appendLine("': :->cmds' \\"); $buf->appendLine("'*:: :->option-or-argument' \\"); $buf->appendLine(" && return"); $buf->unindent(); $buf->appendLine("case \$state in"); $buf->indent(); $buf->appendLine("(cmds)"); $buf->appendBuffer($descsBuf); $buf->appendLine(";;"); $buf->appendLine("(option-or-argument)"); // $code[] = " curcontext=\${curcontext%:*:*}:$programName-\$words[1]:"; // $code[] = " case \$words[1] in"; $buf->indent(); $buf->appendLine("curcontext=\${curcontext%:*}-\$line[1]:"); $buf->appendLine("case \$line[1] in"); $buf->indent(); foreach ($subcmds as $k => $subcmd) { // TODO: support alias $buf->appendLine("({$k})"); if ($subcmd->hasCommands()) { $buf->appendBlock($this->complete_with_subcommands($subcmd, $level + 1)); } else { $buf->appendBlock($this->complete_command_options_arguments($subcmd, $level + 1)); } $buf->appendLine(";;"); } $buf->unindent(); $buf->appendLine("esac"); $buf->appendLine(";;"); $buf->unindent(); $buf->appendLine("esac"); return $buf->__toString(); }
public function outputValues($values, OptionResult $opts) { // indexed array if (is_array($values) && empty($values)) { return; } // encode complex data structure to shell if ($values instanceof ValueCollection) { // this output format works both in zsh & bash if ($opts->flat) { $buf = new Buffer(); $buf->appendLine("#flat"); foreach ($values as $groupId => $groupValues) { foreach ($groupValues as $val) { $buf->appendLine($val); } } $this->logger->write($buf); } elseif ($opts->zsh || $opts->bash) { $buf = new Buffer(); $buf->appendLine("#groups"); $buf->appendLine("declare -A groups"); $buf->appendLine("declare -A labels"); // zsh and bash only supports one dimensional array, so we can only output values in string and separate these values with space. foreach ($values as $groupId => $groupValues) { $buf->appendLine("groups[{$groupId}]=" . encode_array_as_shell_string($groupValues)); } foreach ($values->getGroupLabels() as $groupId => $label) { $buf->appendLine("labels[{$groupId}]=" . as_shell_string($label)); } $this->logger->write($buf); } elseif ($opts->json) { $this->logger->write($values->toJson()); } else { throw new UnsupportedShellException(); } return; } // for assoc array in indexed array if (is_array($values) && is_indexed_array($values) && is_array(end($values))) { $this->logger->writeln("#descriptions"); if ($opts->zsh) { // for zsh, we output the first line as the label foreach ($values as $value) { list($key, $val) = $value; $this->logger->writeln("{$key}:" . addcslashes($val, ":")); } } else { foreach ($values as $value) { $this->logger->writeln($value[0]); } } } elseif (is_array($values) && is_indexed_array($values)) { // indexed array is a list. $this->logger->writeln("#values"); $this->logger->writeln(join("\n", $values)); } else { // associative array $this->logger->writeln("#descriptions"); if ($opts->zsh) { foreach ($values as $key => $desc) { $this->logger->writeln("{$key}:" . addcslashes($desc, ":")); } } else { foreach ($values as $key => $desc) { $this->logger->writeln($key); } } } }