2012-05-11 32 views
5

我想知道這是否可能在Bash中,但我想使用Tab完成來完全替換當前正在擴展的參數。 我舉一個例子: 我想有一個函數可以在樹中向上移動任意數量的級別,所以我可以打電話給 up 2 而這會將我的2個目錄up。 但是,我想這樣做,所以如果在數字2處,我按Tab,它會將該數字擴展爲路徑(相對或絕對,或者很好)。 我幾乎正在使用完整的內置工作,除了它只會追加文本,所以它會像 up 2/Volumes/Dev/如何使用製表符完成替換命令行參數?

是否可以替換已完成的符號?

感謝提前:)

更新:

於是一個大感謝chepner,因爲實際上,籤我的代碼顯示在我的錯誤了。我與錯誤的var比較,並且我有調試代碼導致值不能取代。

任何有興趣,這裏的代碼(也有可能是一個更好的方法來做到這一點):

# 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 
+0

將您到目前爲止的代碼添加到您的問題中。 – chepner

+0

我不知道這是否是有意的,但'up Us /'顯示每個祖先目錄,儘管你實際上不能選擇它們。整齊! – chepner

+0

這是一個整潔的想法和一個聰明的方法。你的代碼教會了我幾件事。但是,它在處理未加引號時不能正確處理需要使用'\' - 轉義(例如嵌入空格)的目錄名稱。我的回答解決了這個問題,但也稍微改變了你的方法。 – mklement0

回答

1

以下基礎上@ NateCanaugh的代碼和

  • 提高穩健性 :處理需要的目錄名稱\ - 正確地轉義 - 例如帶有嵌入空格的名稱 - 如果指定了無效的目錄名(不完全)
  • 修改命令完成方法報告一個錯誤:在命令完成時,將電平數/名稱前綴擴展到對應絕對路徑與終止「/」,如果需要,可以立即執行基於子目錄的進一步完成。

修改的例子是:

# 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). 

下面是完整的代碼(注意語法着色表明畸形的代碼,但事實並非如此):

# 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 
1

是可能的用一個新單詞完全替換當前單詞。隨着我的bash 4.2.29,我可以這樣做:

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

您遇到的問題,但是,如果有多於一個可能完成的,你想要得到的公共前綴的部分完成。然後我的實驗表明bash會嘗試將可用填充與您輸入的前綴進行匹配。

因此,一般來說,如果某件事物是唯一定義的,那麼可能應該只用完全不同的東西替代當前的參數。否則,您應該生成與當前前綴匹配的完成項,以便用戶從中選擇。你的情況,你可以用這些方針的東西替換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 

然而,在這種特定的情況下,它可能會更好只通過相應的路徑替換前綴數字,並保留一切爲默認的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 

的代碼變得更容易了很多閱讀和維護,以及更有效的引導。唯一的缺點是,在某些情況下,您必須再按一次Tab鍵,而不是以前的時間:一次替換前綴,另外兩次實際查看可能的完成列表。您的選擇是否可以接受。

一兩件事:完成將會把爭論變成常規路徑,但你起來的功能,因爲它是不接受的。所以也許你應該用[[ -d $1 ]]檢查來啓動該功能,如果它存在,只需簡單地cd到那個目錄。否則,你的完成將產生被調用函數無法接受的參數。