Question

I have a program that can accept a large number of valid arguments. Many of these have common prefixes and are in a directory tree structure. The options often do not exist and some directories can be used with or without the slash but some can only be used without the slash.

If I already have bash completion that shows me this list of alternatives and changes the commandline as I press and partially complete an argument as below:

$ myprog <TAB><TAB>
foo
somedirectory/a
somedirectory/b
somedirectory/c
zibble
zibble/a
zibble/b
zibble/c
$ myprog so<TAB>
$ myprog somedirectory<TAB><TAB>
somedirectory/a
somedirectory/b
somedirectory/c
$ myprog somedirectory/<TAB><TAB>
a
b
c

How do I change it for bash completion that does this like regular file/dir completion:

$ myprog <TAB><TAB>
foo
somedirectory/
zibble
zibble/
$ myprog so<TAB>
$ myprog somedirectory/<TAB><TAB>
a
b
c

and

$ myprog <TAB><TAB>
foo
somedirectory/
zibble
zibble/
$ myprog zi<TAB>
$ myprog zibble<TAB><TAB>
zibble
zibble/
$ myprog zibble/<TAB><TAB>
a
b
c
Was it helpful?

Solution

The key is to add this line just before returning completions for the the word that should behave like directory completion: [[ $COMPREPLY == */ ]] && compopt -o nospace

By running this after compgen the only suggestions in the COMPREPLY array match the current word being completed. If there is more than one suggestion it will only be partially completed and no space will be added (standard behaviour). If there is only one suggestion then it is used to decide whether to add a space.

Now if you trim all the filenames under zibble/ bash will not add a space separator when it figures out that zibble/ is the only match for the word being completed.

Here's a patch against the bash completion of debian http://anonscm.debian.org/gitweb/?p=bash-completion/bash-completion.git - so there is some weird stuff in the patch context.

It applies cleanly against this version using the git apply command of the "git" version control program: http://anonscm.debian.org/gitweb/?p=bash-completion/bash-completion.git;a=commit;h=2897e62fe7e535eb048f7e08f03ac3fbc3a84fa5

diff --git a/completions/make b/completions/make
index aa19b24..345deea 100644
--- a/completions/make
+++ b/completions/make
@@ -1,11 +1,66 @@
 # bash completion for GNU make                             -*- shell-script -*-

+function _make_target_extract_script()
+{
+    local prefix=$(printf "%s\n" "$1" | sed 's/[][\.*^$(){}?+|/]/\\&/g')
+
+    cat <<EOF
+    /^# Make data base/,/^# Files/d             # skip until files section
+    /^# Not a target/,/^$/        d             # skip not target blocks
+    /^${prefix}/,/^$/!            d             # skip anything user dont want
+
+    # The stuff above here describes lines that are not
+    #  explicit targets or not targets other than special ones
+    # The stuff below here decides whether an explicit target
+    #  should be output.
+
+    /^# File is an intermediate prerequisite/ {
+      s/^.*$//;x                                # unhold target
+      d                                         # delete line
+    }
+
+    /^$/ {                                      # end of target block
+      x                                         # unhold target
+      s/^(${prefix}[^:/]*\/).*:.*$/\1/p         # write targets for subdirs
+      s/:.*$/ /p                                 # write complete targets
+      d                                         # hide any bugs
+    }
+
+    /^[^#\t:%]+:/ {         # found target block
+
+      /^\.PHONY/                  d             # special target
+      /^\.SUFFIXES/               d             # special target
+      /^\.DEFAULT/                d             # special target
+      /^\.PRECIOUS/               d             # special target
+      /^\.INTERMEDIATE/           d             # special target
+      /^\.SECONDARY/              d             # special target
+      /^\.SECONDEXPANSION/        d             # special target
+      /^\.DELETE_ON_ERROR/        d             # special target
+      /^\.IGNORE/                 d             # special target
+      /^\.LOW_RESOLUTION_TIME/    d             # special target
+      /^\.SILENT/                 d             # special target
+      /^\.EXPORT_ALL_VARIABLES/   d             # special target
+      /^\.NOTPARALLEL/            d             # special target
+      /^\.ONESHELL/               d             # special target
+      /^\.POSIX/                  d             # special target
+      /^\.NOEXPORT/               d             # special target
+      /^\.MAKE/                   d             # special target
+
+      /^[^a-zA-Z0-9]/             d             # convention for hidden tgt
+
+      h                                         # hold target
+      d                                         # delete line
+    }
+
+EOF
+}
+
 _make()
 {
     local cur prev words cword split
     _init_completion -s || return

-    local file makef makef_dir="." makef_inc i
+    local file makef makef_dir=( "-C" "." ) makef_inc i

     case $prev in
         -f|--file|--makefile|-o|--old-file|--assume-old|-W|--what-if|\
@@ -49,7 +104,7 @@ _make()
         for (( i=0; i < ${#words[@]}; i++ )); do
             if [[ ${words[i]} == -@(C|-directory) ]]; then
                 # eval for tilde expansion
-                eval makef_dir=${words[i+1]}
+                eval makef_dir=( -C "${words[i+1]}" )
                 break
             fi
         done
@@ -59,18 +114,17 @@ _make()
         for (( i=0; i < ${#words[@]}; i++ )); do
             if [[ ${words[i]} == -@(f|-?(make)file) ]]; then
                 # eval for tilde expansion
-                eval makef=${words[i+1]}
+                eval makef=( -f "${words[i+1]}" )
                 break
             fi
         done

-        [[ -n $makef ]] && makef="-f ${makef}"
-        [[ -n $makef_dir ]] && makef_dir="-C ${makef_dir}"
+        COMPREPLY=( $( compgen -W "$( 
+            make -npq "${makef[@]}" "${makef_dir[@]}" .DEFAULT 2>/dev/null | \
+            sed -n -r -f <(_make_target_extract_script "$cur")
+       )" -- "$cur" ) )

-        COMPREPLY=( $( compgen -W "$( make -qp $makef $makef_dir 2>/dev/null | \
-            awk -F':' '/^[a-zA-Z0-9][^$#\/\t=]*:([^=]|$)/ \
-            {split($1,A,/ /);for(i in A)print A[i]}' )" \
-            -- "$cur" ) )
+        [[ $COMPREPLY == */ ]] && compopt -o nospace

     fi
 } &&
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top