This all started with a request from one of our users who had a very specific issue. Their workflow makes heavy use of the cd ~username syntax, and for difficult to spell usernames, they relied on tab completion which broke when their machine was updated.

More experienced admins than myself will probably be able to guess the issue from the beginning, especially once they know that we moved to RHEL7 with SSS. However, I am fond of the journey that this issue took me on which is why I’ve chosen to write about it.

So, where do bash autocompletions actually come from? And why did this one in particular break when we switched over to RHEL 7?

Other than installing the bash-completions package and just having completions automatically work I had never used programmatic completion before. After some quick searching I found that bash completions are made possible with the complete built-in function.

From the manual we can get a printout of existing completions by running complete without any arguments. Here’s what I get when I run it on my machine.

complete | grep cd
complete -o nospace -F _cd pushd
complete -F _filedir_xspec cdiff
complete -o nospace -F _cd cd

From this it seems that _cd is the function we’re looking for. Some research reveals that it’s customary to register the completion function for a command foo as _foo.

So what does the _cd function do? We can have bash print the body of a function with the type command.

type _cd
_cd is a function
_cd ()
{
    local cur prev words cword;
    _init_completion || return;
    local IFS='
' i j k;
    compopt -o filenames;
    if [[ -z "${CDPATH:-}" || "$cur" == ?(.)?(.)/* ]]; then
        _filedir -d;
        return;
    fi;
    local -r mark_dirs=$(_rl_enabled mark-directories && echo y);
    local -r mark_symdirs=$(_rl_enabled mark-symlinked-directories && echo y);
    for i in ${CDPATH//:/'
'};
    do
        k="${#COMPREPLY[@]}";
        for j in $( compgen -d -- $i/$cur );
        do
            if [[ ( -n $mark_symdirs && -h $j || -n $mark_dirs && ! -h $j ) && ! -d ${j#$i/} ]]; then
                j+="/";
            fi;
            COMPREPLY[k++]=${j#$i/};
        done;
    done;
    _filedir -d;
    if [[ ${#COMPREPLY[@]} -eq 1 ]]; then
        i=${COMPREPLY[0]};
        if [[ "$i" == "$cur" && $i != "*/" ]]; then
            COMPREPLY[0]="${i}/";
        fi;
    fi;
    return
}

And people say shell code isn’t pretty? Anyway, the takeaway is that _cd is basically a wrapper around the compgen command that does all the heavy lifting. The next step will be investigating how this command works.

Straight to The Source

Since compgen is a bash built-in if we want it to divulge its secrets we’ll have to inspect the source. Doing our due diligence would require that we use the source for the exact same version of bash that we’re using, including all of RedHat’s patches, but since we’re just poking around we can probably use the latest version safely.Since compgen is a bash built-in if we want it to divulge its secrets we’ll have to inspect the source. Doing our due diligence would require that we use the source for the exact same version of bash that we’re using, including all of RedHat’s patches, but since we’re just poking around we can probably use the latest version safely.

Looking around the directories the most relevant file for our purposes seems to be builtins/complete.def which contains the source for compgen. Here’s what we find in the compgen_builtin function.

cs->funcname = STRDUP (Farg);
  cs->command = STRDUP (Carg);
  cs->filterpat = STRDUP (Xarg);

  rval = EXECUTION_FAILURE;
  sl = gen_compspec_completions (cs, "compgen", word, 0, 0, 0);

  /* If the compspec wants the bash default completions, temporarily
     turn off programmable completion and call the bash completion code. */
  if ((sl == 0 || sl->list_len == 0) && (copts & COPT_BASHDEFAULT))
    {
      matches = bash_default_completion (word, 0, 0, 0, 0);
      sl = completions_to_stringlist (matches);
      strvec_dispose (matches);
    }

From this it seems that, at lest when bash isn’t performing the default completion, that it offloads the actual work to the function gen_compspec_completions. After searching around the source tree we discover that this function lives in the file pcomplete.c. Here is the relevant excerpt from its definition.

#ifdef DEBUG
  debug_printf ("gen_compspec_completions (%s, %s, %d, %d)", cmd, word, start, end);
  debug_printf ("gen_compspec_completions: %s -> %p", cmd, cs);
#endif
  ret = gen_action_completions (cs, word);
#ifdef DEBUG
  if (ret && progcomp_debug)
    {
      debug_printf ("gen_action_completions (%p, %s) -->", cs, word);
      strlist_print (ret, "\t");
      rl_on_new_line ();
    }
#endif

In the same file we see that gen_action_completions has the following in its definition.

GEN_XCOMPS(flags, CA_COMMAND, text, command_word_completion_function, cmatches, ret, tmatches);
GEN_XCOMPS(flags, CA_FILE, text, pcomp_filename_completion_function, cmatches, ret, tmatches);
GEN_XCOMPS(flags, CA_USER, text, rl_username_completion_function, cmatches, ret, tmatches);
GEN_XCOMPS(flags, CA_GROUP, text, bash_groupname_completion_function, cmatches, ret, tmatches);
GEN_XCOMPS(flags, CA_SERVICE, text, bash_servicename_completion_function, cmatches, ret, tmatches);

It’s not important what this macro actually does because all we need is the fact that it’s using the function rl_username_completion_function from the GNU readline package. Finally! A light at the end of the tunnel!

Readline? Isn’t that an input library?

Not knowing anything about readline, other than that bash uses it to process user input, I wouldn’t have guessed that it had built-in completion capabilities. Luckily our copy of bash comes with lib/readline so we don’t have to download it separately.

In the file lib/readline/complete.c we find the following definition of our function rl_username_completion_function.

char *
rl_username_completion_function (text, state)
     const char *text;
     int state;
{
#if defined (__WIN32__) || defined (__OPENNT)
  return (char *)NULL;
#else /* !__WIN32__ && !__OPENNT) */
  static char *username = (char *)NULL;
  static struct passwd *entry;
  static int namelen, first_char, first_char_loc;
  char *value;

  if (state == 0)
    {
      FREE (username);

      first_char = *text;
      first_char_loc = first_char == '~';

      username = savestring (&text[first_char_loc]);
      namelen = strlen (username);
#if defined (HAVE_GETPWENT)
      setpwent ();
#endif
    }

#if defined (HAVE_GETPWENT)
  while (entry = getpwent ())
    {
      /* Null usernames should result in all users as possible completions. */
      if (namelen == 0 || (STREQN (username, entry->pw_name, namelen)))
        break;
    }
#endif

  if (entry == 0)
    {
#if defined (HAVE_GETPWENT)
      endpwent ();
#endif
      return ((char *)NULL);
    }
  else
    {
      value = (char *)xmalloc (2 + strlen (entry->pw_name));

      *value = *text;

      strcpy (value + first_char_loc, entry->pw_name);

      if (first_char == '~')
        rl_filename_completion_desired = 1;

      return (value);
    }
#endif /* !__WIN32__ && !__OPENNT */
}

Hallelujah, we finally found it. All this to just find out that it’s calling the libc function getpwent. This is where those experienced admins will probably sigh and utter something like “well obviously” but hey, at least now we have proven it.

The Upshot

How does this knowledge help us with our problem? Well we know with certainty that username predictions originate from a call to getpwent so we should look at what is returned by the commandl getent passwd.

root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/usr/bin/nologin
daemon:x:2:2:daemon:/:/usr/bin/nologin
mail:x:8:12:mail:/var/spool/mail:/usr/bin/nologin
ftp:x:14:11:ftp:/srv/ftp:/usr/bin/nologin
http:x:33:33:http:/srv/http:/usr/bin/nologin
...

Only local users! It’s no wonder our tab completion isn’t finding our users because they’re simply not in the database. And from where is this database populated? To find out we need to look in /etc/nsswitch.conf.

passwd:     files sss

Now we’ve really narrowed down the problem, SSS isn’t returning any of our domain users. Can we pull an individual user’s information?

$ getent passwd $me
me:x:$uid:$gid:$me:$homedir:/bin/bash

So we can query individual users, which would explain how we were able to log in in the first place, but for some reason we can’t list the information of every user. Isn’t there a SSS setting about that? In sssd.conf(5) we find just what we’re looking for.

enumerate (bool)
    Determines if a domain can be enumerated. This parameter can have one of the following values:

    TRUE = Users and groups are enumerated

    FALSE = No enumerations for this domain

    Default: FALSE

After enabling this setting and restarting sssd we can now see all of the domain users in the passwd database and completions start working again!

Conclusion

To me this is one of the big advantages of open source. If you don’t understand how something works, the source will happily divulge. Sure, it’s not something you’ll do every day, but it certainly comes in handy when you need it.