简体   繁体   English

如何使用制表符完成替换命令行参数?

[英]How can I replace a command line argument with tab completion?

I'm wondering if this is possible in Bash, but I'd like to use tab completion to completely replace the current argument that's being expanded. 我想知道这在Bash中是否可行,但我想使用制表符完全替换正在扩展的当前参数。 I'll give an example: I'd like to have a function that moves up an arbitrary number of levels in the tree, so I can call up 2 And that would cd me 2 directories up. 我将给出一个例子:我想要一个在树中向上移动任意数量级别的函数,所以我可以调用2并且这会打开我2个目录。 However, I would like to make it so that if at the number 2, I press tab, it will expand that number to being the path (either relative or absolute, either is fine). 但是,我想这样做,如果在数字2,我按Tab键,它会将该数字扩展为路径(相对或绝对,或者很好)。 I have this almost working using the complete builtin except it will only append the text, so it will be something like up 2/Volumes/Dev/ 我有几乎使用完整的内置工作,除了它只会附加文本,所以它将是像2 / Volumes / Dev /

Is it possible to replace the completed symbol? 是否可以更换完整的符号?

Thanks in advance :) 提前致谢 :)

Update: 更新:

So a big thanks to chepner, because actually checking my code revealed where my bug was. 所以非常感谢chepner,因为实际检查我的代码揭示了我的bug所在。 I was comparing against the wrong var, and the debug code I had was causing the value to not replace. 我正在与错误的var进行比较,而我的调试代码导致该值无法替换。

For anyone interested, here's the code (and there could be a much better way to accomplish this): 对于任何感兴趣的人,这里是代码(可能有更好的方法来实现这一点):

# Move up N levels of the directory tree
# Or by typing in some dir in the PWD
# eg. Assuming your PWD is "/Volumes/Users/natecavanaugh/Documents/stuff"
#     `up 2` moves up 2 directories to "/Volumes/Users/natecavanaugh"
#     `up 2/` and pressing tab will autocomplete the dirs in "/Volumes/Users/natecavanaugh"
#     `up Users` navigate to "/Volumes/Users"
#     `up us` and pressing tab will autocomplete to "/Volumes/Users"
function up {
    dir="../"
    if [ -n "$1" ]; then
        if [[ $1 =~ ^[0-9]+$ ]]; then
            strpath=$( printf "%${1}s" );
            dir=" ${strpath// /$dir}"
        else
            dir=${PWD%/$1/*}/$1
        fi
    fi

    cd $dir
}

function _get_up {
    local cur
    local dir
    local results
    COMPREPLY=()
    #Variable to hold the current word
    cur="${COMP_WORDS[COMP_CWORD]}"

    local lower_cur=`echo ${cur##*/} | tr [:upper:] [:lower:]`

    # Is the arg a number or number followed by a slash
    if [[ $cur =~ ^[0-9]+/? ]]; then
        dir="../"
        strpath=$( printf "%${cur%%/*}s" );
        dir=" ${strpath// /$dir}"

        # Is the arg just a number?
        if [[ $cur =~ ^[0-9]+$ ]]; then
            COMPREPLY=($(compgen -W "${dir}"))
        else
            if [[ $cur =~ /.*$ ]]; then
                cur="${cur##*/}"
            fi

            results=$(for t in `cd $dir && ls -d */`; do if [[ `echo $t | tr [:upper:] [:lower:]` == "$lower_cur"* ]]; then echo "${t}"; fi done)

            COMPREPLY=($(compgen -P "$dir" -W "${results}"))
        fi
    else
        # Is the arg a word that we can look for in the PWD
        results=$(for t in `echo $PWD | tr "/" "\n"`; do if [[ `echo $t | tr [:upper:] [:lower:]` == "$lower_cur"* ]]; then echo "${t}"; fi; done)

        COMPREPLY=($(compgen -W "${results}"))
    fi  
}

#Assign the auto-completion function _get for our command get.
complete -F _get_up up

The following builds on the OP's own code in the question and: 以下内容基于问题中OP自己的代码构建:

  • improves robustness : handles directory names that need \\ -escaping - eg, names with embedded spaces - correctly; 提高健壮性 :处理需要\\ -escaping的目录名称 - 例如,带有嵌入空格的名称 - 正确; reports an error if an invalid directory name is specified (without completion) 如果指定了无效的目录名,则报告错误(未完成)
  • modifies the command-completion approach : on command completion, the level number/name prefix expands to the corresponding absolute path with a terminating / , so that further completion based on subdirectories can be performed right away, if desired. 修改命令完成方法 :在命令完成时,级别编号/名称前缀扩展为具有终止/的相应绝对路径 ,以便可以根据需要立即执行基于子目录的进一步完成。

The modified examples are: 修改后的例子是:

# Assume that the working directory is '/Users/jdoe/Documents/Projects/stuff'.
#  `up 2` moves 2 levels up to '/Users/jdoe/Documents'
#  `up 2<tab>` completes to `up /Users/jdoe/Documents/`
#     Hit enter to change to that path or [type additional characters and]
#     press tab again to complete based on subdirectories.
#  `up Documents` or `up documents` changes to '/Users/jdoe/Documents'
#  `up Doc<tab>` or `up doc<tab>` completes to `up /Users/jdoe/Documents/`
#     Hit enter to change to that path or [type additional characters and]
#     press tab again to complete based on subdirectories.
#     Note: Case-insensitive completion is only performed if it is turned on
#     globally via the completion-ignore-case Readline option
#     (configured, for instance, via ~/.inputrc or /etc/inputrc).

Here's the complete code (note that the syntax coloring suggests malformed code, but that's not the case): 这是完整的代码(注意语法着色表示格式错误的代码,但事实并非如此):

# Convenience function for moving up levels in the path to the current working directory.
# Synopsis:
#     `up [n]` moves n levels up in the directory hierarchy; default is 1.
#     `up dirname` changes to the closest ancestral directory by that name, regardless of case.
#     `up absolutepath` changes to the specified absolute path; primarily used with command completion (see below).
# Additionally, if command completion via _complete_up() is in effect (<tab> represents pressing the tab key):
#      `up [n]<tab>` replaces n with the absolute path of the directory n levels up (default is 1).
#      `up dirnameprefix<tab>` replaces dirnameprefix with the absolute path of the closest ancestral directory whose name starts with the specified name prefix, terminated with '/'.
#         Whether dirnameprefix is matched case-insensitively or not depends on whether case-insensitive command completion is turned on globally via ~/.inputrc or /etc/inputrc.
#       In both cases the completed absolute path ends in '/', allowing you to optionally continue completion based on that path's subdirectories.
# Notes:
#   - Directory names with characters that need escaping when unquoted (such as spaces) are handled correctly.
#   - For command completion, to specify names that need escaping when unquoted, specify them escaped rather than quoted;
#     e.g., `up my \di<tab>' to match 'my dir' in the ancestral path.
function up {

    local dir='../'   # By default, go up 1 level.

    [[ "$1" == '-h' || "$1" == '--help' ]] && { echo -e "usage:\n\t$FUNCNAME [n]\n\t$FUNCNAME dirname\n  Moves up N levels in the path to the current working directory, 1 by default.\n  If DIRNAME is given, it must be the full name of an ancestral directory (case does not matter).\n  If there are multiple matches, the one *lowest* in the hierarchy is changed to." && return 0; }

    if [[ -n "$1" ]]; then
        if [[ $1 =~ ^[0-9]+$ ]]; then   # A number, specifying the number of levels to go up.            
            local strpath=$( printf "%${1}s" ) # This creates a string with as many spaces as levels were specified.
            dir=${strpath// /$dir}  # Create the go-up-multiple-levels cd expression by replacing each space with '../'
        elif [[ $1 =~ ^/ ]]; then  # Already an absolute path? Use as is. (Typically, this happens as a result of command-line completion invoked via _complete_up().)
            dir=$1
        else # Assumed to be the full name of an ancestral directory (regardless of level), though the case needn't match.
            # Note: On case-insensitive HFS+ volumes on a Mac (the default), you can actually use case-insensitive names with 'cd' and the resulting working directory will be reported in that case(!).
            #       This behavior is NOT related to whether case-insensitivity is turned on for command completion or not.
            # !! Strangely, the 'nocasematch' shopt setting has no effect on variable substitution, so we need to roll our own case-insensitive substitution logic here.
            local wdLower=$(echo -n "$PWD" | tr '[:upper:]' '[:lower:]')
            local tokenLower=$(echo -n "$1" | tr '[:upper:]' '[:lower:]')
            local newParentDirLower=${wdLower%/$tokenLower/*}   # If the specified token is a full ancestral directory name (irrespective of case), this substitution will give us its parent path.
            [[ "$newParentDirLower" == "$wdLower"  ]] && { echo "$FUNCNAME: No ancestral directory named '$1' found." 1>&2; return 1; }
            local targetDirPathLength=$(( ${#newParentDirLower} + 1 + ${#tokenLower} ))
            # Get the target directory's name in the exact case it's defined.
            dir=${PWD:0:$targetDirPathLength}
        fi
    fi

    # Change to target directory; use of 'pushd' allows use of 'popd' to return to previous working directory.
    pushd "$dir" 1>/dev/null
}

# Companion function to up(), used for command completion.
# To install it, run (typically in your bash profile):
# `complete -o filenames -F _complete_up up`
# Note: The '-o filenames' option ensures that:
#   (a) paths of directories returned via $COMPREPLY leave the cursor at the terminating "/" for potential further completion
#   (b) paths with embeddes spaces and other characters requiring \-escaping are properly escaped.
function _complete_up {

    COMPREPLY=() # Initialize the array variable through which completions must be passed out.

    # Retrieve the current command-line token, i.e., the one on which completion is being invoked.
    local curToken=${COMP_WORDS[COMP_CWORD]}
    # Remove \ chars., presumed to be escape characters in the current token, which is presumed to be *unquoted*. This allows invoking completion on a token with embedded space, e.g., '$FUNCNAME some\ directory'
    # !! Strictly speaking, we'd have to investigate whether the token was specified with quotes on the command line and, if quoted,  NOT unescape. Given that the purpose of this function is expedience, we
    # !! assume that the token is NOT quoted and that all backslashes are therefore escape characters to be removed.
    curToken=${curToken//'\'}

    if [[ $curToken =~ ^/ ]]; then # Token is an absolute path (typically as a result of a previous completion) -> complete with directory names, similar to 'cd' (although the latter, curiously, also completes *file* names).

        local IFS=$'\n' # Make sure that the output of compgen below is only split along lines, not also along spaces (which the default $IFS would do).
        COMPREPLY=($(compgen -o dirnames -- "$curToken"))

    elif [[ $curToken =~ ^[0-9]+/? ]]; then # Token is a number (optionally followed by a slash) -> replace the token to be completed with the absolute path of the directory N levels above, where N is the number specified.

        # Create a go-up-multiple-levels cd expression that corresponds to the number of levels specified.
        local strpath=$( printf "%${curToken%%/*}s" ) # This creates a string with as many spaces as levels were specified.
        local upDirSpec=${strpath// /../}  # Create the go-up-multiple-levels cd expression by replacing each space with '../'        

        # Expand to absolute path (ending in '/' to facilitate optional further completion) and return.
        local dir=$(cd "$upDirSpec"; echo -n "$PWD/")
        if [[ "$dir" == '//' ]]; then dir='/'; fi # In case the target dir turns out to be the root dir, we've accidentally created '//' in the previous statement; fix it.
        # !! Note that the path will appear *unquoted* on the command line and must therefore be properly \-escaped (e.g., a ' '  as '\ ').
        # !! Escaping is performed automatially by virtue of defining the compspec with '-o filenames' (passed to 'complete').
        COMPREPLY=("$dir") 

    else # Token is a name -> look for a prefix match among all the ancestral path components; use the first match found (i.e., the next match up in the hierarchy).

        # Determine if we should do case-insensitive matching or not, depending on whether cases-insensitive completion was turned on globally via ~/.inputrc or /etc/inputrc.
        # We do this to be consistent with the default command completion behavior.
        local caseInsensitive=0        
        bind -v | egrep -i '\bcompletion-ignore-case[[:space:]]+on\b' &>/dev/null && caseInsensitive=1

        # If we need to do case-INsensitive matching in this function, we need to make sure the 'nocasematch' shell option is (temporarily) turned on.
        local nocasematchWasOff=0
        if (( caseInsensitive )); then
            nocasematchWasOff=1
            shopt nocasematch >/dev/null && nocasematchWasOff=0
            (( nocasematchWasOff )) && shopt -s nocasematch >/dev/null
        fi

        local pathSoFar=''
        local matchingPath=''
        # Note: By letting the loop iterate over ALL components starting at the root, we end up with the *last* match, i.e. the one *lowest* in the hierarchy (closed to the current working folder).
        # !! We COULD try to return multiple matches, if applicable, but in practice we assume that there'll rarely be paths whose components have identical names or prefixes.
        # !! Thus, should there be multiple matches, the user can reinvoke the same command to change to the next-higher match (though the command must be typed again), and so forth.
        local parentPath=${PWD%/*}
        local IFS='/' # This will break our parent path into components in the 'for' loop below.
        local name
        for name in ${parentPath:1}; do
            pathSoFar+=/$name
            if [[ "$name" == "$curToken"* ]]; then
                matchingPath="$pathSoFar/"
            fi
        done

        # Restore the state of 'nocasematch', if necessary.
        (( caseInsensitive && nocasematchWasOff )) && shopt -u nocasematch >/dev/null

        # If match was found, return its absolute path (ending in / to facilitate optional further completion).
        # !! Note that the path will appear *unquoted* on the command line and must therefore be properly \-escaped (e.g., a ' '  as '\ ').
        # !! Escaping is performed automatially by virtue of defining the compspec with '-o filenames' (passed to 'complete').
        [[ -n "$matchingPath" ]] && COMPREPLY=("$matchingPath")

    fi  
}

# Assign the auto-completion function for up().
complete -o filenames -F _complete_up up

It is possible to completely replace the current word with a single new word. 可以用一个新单词完全替换当前单词。 With my bash 4.2.29, I can do this: 用我的bash 4.2.29,我可以这样做:

_xxx() { COMPREPLY=( foo ); }
complete -F _xxx x
x bar # pressing tab turns this into x foo

You encounter problems, however, if there is more than one possible completion, and you want to get partial completion of the common prefix. 但是,如果有多个可能的完成,并且您希望部分完成公共前缀,则会遇到问题。 Then my experiments indicate that bash will try to match the available completions to the prefix you entered. 然后我的实验表明bash会尝试将可用的完成次数与您输入的前缀相匹配。

So in general you should probably only replace the current argument with something completely different if that something is uniquely defined. 所以一般来说,如果某些内容是唯一定义的,那么你应该只用一些完全不同的东西替换当前的参数。 Otherwise, you should generate completions which match the current prefix, to have the user select from those. 否则,您应该生成与当前前缀匹配的完成,以便用户从中选择。 In your case you could replace the COMPREPLY=($(compgen -P "$dir" -W "${results}")) with something along these lines: 在你的情况下,你可以用以下COMPREPLY=($(compgen -P "$dir" -W "${results}"))替换COMPREPLY=($(compgen -P "$dir" -W "${results}"))

local IFS=$'\n'
COMPREPLY=( $(find "${dir}" -maxdepth 1 -type d -iname "${cur#*/}*" -printf "%P\n") )
if [[ ${#COMPREPLY[@]} -eq 1 ]]; then
    COMPREPLY=( "${dir}${COMPREPLY[0]}" )
fi

However, in this specific case it might be better to only replace the prefix digit by the appropriate path, and leave everything else to the default bash completion: 但是,在这种特定情况下,最好只用适当的路径替换前缀数字,并将其他所有内容保留为默认的bash完成:

_up_prefix() {
    local dir cur="${COMP_WORDS[COMP_CWORD]}"
    COMPREPLY=()

    if [[ ${cur} =~ ^[0-9]+/? ]]; then
        # Starting with a number, possibly followed by a slash
        dir=$( printf "%${cur%%/*}s" );
        dir="${dir// /../}"
        if [[ ${cur} == */* ]]; then
            dir="${dir}${cur#*/}"
        fi
        COMPREPLY=( "${dir}" "${dir}." ) # hack to suppress trailing space
    elif [[ ${cur} != */* ]]; then
        # Not a digit, and no slash either, so search parent directories
        COMPREPLY=( $(IFS='/'; compgen -W "${PWD}" "${cur}") )
        if [[ ${#COMPREPLY[@]} -eq 1 ]]; then
            dir="${PWD%${COMPREPLY[0]}/*}${COMPREPLY[0]}/"
            COMPREPLY=( "${dir}" "${dir}." ) # hack as above
        fi
    fi
}

complete -F _up_prefix -o dirnames up

The code becomes a lot easier to read and maintain, plus more efficient to boot. 代码变得更容易阅读和维护,而且启动效率更高。 The only drawback is that in some cases you'd have to press tab one more time than you used to: once to substitute the prefix, and twice more to actually see the list of possible completions. 唯一的缺点是,在某些情况下,您必须再次按Tab键一次:一次替换前缀,再多两次以实际查看可能的完成列表。 Your choice whether that's acceptable. 您的选择是否可以接受。

One more thing: the completion will turn the argument into a regular path, but your up function as it is does not accept those. 还有一件事:完成将把参数转换为常规路径,但是你的up函数不接受它们。 So perhaps you should start that function with a [[ -d $1 ]] check, and simply cd to that directory if it exists. 所以也许你应该用[[ -d $1 ]]检查启动那个函数,如果它存在,只需cd到那个目录。 Otherwise your completion will generate arguments which are unacceptable to the called function. 否则,您的完成将生成对被调用函数不可接受的参数。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何在bash中启用命令行开关的tab-completion? - How to enable tab-completion of command line switches in bash? 如何使Git别名的制表符完成行为像现有命令一样? - How can I make tab completion for my Git alias behave like an existing command? 如何“查看”bash命令行参数扩展? - How can I “look behind” the bash command line argument expansion? 如何在一行中用字符串替换命令的结果? - How can I string replace results of a command in one line? 如何使 bash 选项卡完成的行为类似于 vim 选项卡完成并循环匹配匹配项? - How can I make bash tab completion behave like vim tab completion and cycle through matching matches? 如何为命令行开关启用Bash可编程完成功能? - How Do I Enable Bash Programmable Completion for Command Line Switches? Bash命令行选项卡完成冒号字符 - Bash Command-Line Tab Completion Colon Character 如何配置git bash命令行补全? - How to configure git bash command line completion? 如何将我的bash脚本的所有命令行参数作为一个参数传递给另一个程序? - How can I pass all command line arguments of my bash script as one argument to another program? 如何通过除一个(不是第一/最后)命令行参数以外的所有参数,而不会丢失引号? - How can I pass through all but one (not first/last) command line argument, w/o losing quoting?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM