10

The Makefile is:

%.pdf: %.tex
    rubber -d $<

If there is a doc.tex in the directory, then make doc.pdf builds doc.pdf. The problem is that when I type make, the autocompletion gives nothing: it doesn't even allow to autocomplete to make doc.tex. What can be done about it?

4
  • Completion is handled by bash-completion, not by bash, nor by make. bash-completion needs a list of valid targets to show you. I don't think it's possible to convince make to produce such a list when you're using wildcard rules. Commented Oct 26, 2016 at 7:01
  • 1
    Even zsh's completion isn't that smart. Commented Oct 27, 2016 at 22:49
  • Possible duplicate: stackoverflow.com/q/516305/21348 Commented Aug 11, 2017 at 13:55
  • possible duplicate: stackoverflow.com/questions/4188324/… Commented Aug 22, 2022 at 22:40

1 Answer 1

6

The bash-completion package doesn't do this, it does some acrobatics to handle both command line options and extract a list of Makefile targets, but it does not try to generate matches by applying wildcards or otherwise handling any pattern rules.

It can be done however, here's a simple version with a few caveats.

function _mkcache() {
    local _file="$1"
    # add "-r" to omit defaults (60+ rules)
    ${MAKE:-make} ${_file:+-f "$_file"} -qp 2>/dev/null |
    gawk '/^# *Make data base/,/^# *Finished Make data base/{
      if (/^# Not a target/) { getline; next }
      ## handle "target: ..."
      if (match($0,/^([^.#% ][^:%=]+) *:($|[^=])(.*)/,bits)) {
          #if (bits[3]=="") next # OPT: skip phony
          printf("%s\n",bits[1])
      }
      ## handle "%.x [...]: %.y [| x]", split into distinct targets/prereqs
      else if (match($0,/^([^:]*%[^:]*) *(::?) *(.*%.*) *(\| *(.*))?/,bits)) {
          #if (bits[3]=="%") next # OPT: skip wildcard ones
          nb1=split(bits[1],bb1)
          nb3=split(bits[3],bb3)
          for (nn=1; nn<=nb1; nn++) 
            for (mm=1; mm<=nb3; mm++) 
              printf("%s : %s\n",bb1[nn],bb3[mm])
      }
      ## handle fixed (no %) deps
      else if (match($0,/^([^:]*%[^:]*) *(::?) *([^%]*)$/,bits)) {
          if (bits[3]=="") next # phony
          printf("%s : %s\n",bits[1],bits[3])
      }
      ## handle old form ".c.o:"  rewrite to new form "%.o: %.c"
      else if (match($0,/^\.([^.]+)\.([^.]+): *(.*)/,bits)) {
          printf("%%.%s : %%.%s\n", bits[2],bits[1])
      }
    }' > ".${_file:-Makefile}.targets"
}

function _bc_make() {
    local ctok=${COMP_WORDS[COMP_CWORD]}   # curr token
    local ptok=${COMP_WORDS[COMP_CWORD-1]} # prev token
    local -a mkrule maybe
    local try rr lhs rhs rdir pat makefile=Makefile

    ## check we're not doing any make options 
    [[ ${ctok:0:1} != "-" && ! $ptok =~ ^-[fCIjloW] ]] && {
        COMPREPLY=()
        [[ "$makefile" -nt .${makefile}.targets ]] && 
            _mkcache "$makefile"

        mapfile -t mkrule < ".${makefile}.targets"
        # mkrule+=( "%.o : %.c" )  # stuff in extra rules

        for rr in "${mkrule[@]}"; do
            IFS=": " read lhs rhs <<< $rr

            ## special "archive(member):"
            [[ "$lhs" =~ ^(.*)?\((.+)\) ]] && {
                continue # not handled
            }

            ## handle simple targets
            [[ "$rhs" == "" ]] && {
                COMPREPLY+=( $(compgen -W "$lhs" -- "$ctok" ) )
                continue
            }

            ## rules with a path, like "% : RCS/%,v" 
            rdir=""
            [[ "$rhs" == */* ]] && rdir="${rhs/%\/*/}/" 
            rhs=${rhs/#*\//}

            ## expand (glob) that matches RHS 
            ## if current token already ends in a "." strip it
            ## match by replacing "%" stem with "*"

            [[ $ctok == *. ]] && try="${rdir}${rhs/\%./$ctok*}" \
                              || try="${rdir}${rhs/\%/$ctok*}"

            maybe=( $(compgen -G "$try") )  # try must be quoted

            ## maybe[] is an array of filenames from expanded prereq globs
            (( ${#maybe[*]} )) && {

               [[ "$rhs" =~ % ]] && {
                   ## promote rhs glob to a regex: % -> (.*)
                   rhs="${rhs/./\\.}"
                   pat="${rdir}${rhs/\%/(.*)}"

                   ## use regex to extract stem from RHS, sub "%" on LHS
                   for nn in "${maybe[@]}"; do 
                       [[ $nn =~ $pat ]] && {
                           COMPREPLY+=( "${lhs/\%/${BASH_REMATCH[1]}}" )
                       }
                   done
               } || {
                   # fixed prereqs (no % on RHS)
                   COMPREPLY+=( "${lhs/\%/$ctok}" )   
               }
            }
        done
        return
    }
    COMPREPLY=() #default
}
complete -F _bc_make ${MAKE:-make}

There are two parts, a function _mkcache extracts all the rules and targets from a Makefile and caches these. It also does a bit of processing so the rules are simplified to a single "target : pre-req" form in that cache.

Then, a completion function _bc_make takes the token you attempt completion on and tries to match against targets, and uses the pattern rules to expand a glob based on the pre-requisites and the word for completion. If one or more matches are found, it builds a list of targets based on the pattern rules.

GNU make is assumed. It should correctly handle:

  • targets and pattern rules (though not all of them, see below)
  • new and old form .c.o%.o : %.c
  • paths in prereqs (e.g RCS/)
  • with or without all default rules (add -r to make if preferred)

Caveats, and not supported:

  • intermediate or chained dependencies, it's not as smart as make
  • VPATH or vpath
  • .SUFFIXES
  • make -C dir
  • "archive(member)" targets, explicit or implicit
  • make options expansion
  • pathological junk in the environment that can cause Makefile parsing problems (TERMCAP for example)
  • Makefiles named other than Makefile

Some of the above can be added relatively simply, others like archive handling are not so simple.

2
  • 1
    "a simple version" 🤔 Commented Jan 28, 2020 at 13:14
  • Perhaps simple in terms of features, rather than implementation 😬 Commented Feb 5, 2020 at 17:54

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.