Esempio n. 1
0
    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("};");
    }
Esempio n. 2
0
 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();
 }
Esempio n. 3
0
 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);
             }
         }
     }
 }