Linuxの ~(tilde) について

先日一緒に作業をしていた若者が~(tilde)を知らなかったので、~(tilde) ってのはHOMEディレクトリを指すんだよー。と教えてあげていたのですが、ふと実装が気になったのでソースをのぞいてみました。

まずはググってみる

なぜUnix&Linuxではホームディレクトリを「~」文字で表現するのか。 - [モ]Modern Syntax  
なるほど昔のキーボード由来なんですね。  

本題

ソースを見てみます

GNU Project Archives から該当バージョンのソースをダウンロード。

$ bash --version
GNU bash, version 3.2.53(1)-release (x86_64-apple-darwin14)
Copyright (C) 2007 Free Software Foundation, Inc.

環境変数Home を返すところ

それっぽい名前のファイルがあるのでtilde.hを見ながらそれっぽいのを探してみると tilde_expand や tilde_expand_word という function がありました。

// bash-3.2/lib/tilde/tilde.c
char *
tilde_expand_word (filename)
     const char *filename;
{
  char *dirname, *expansion, *username;
  int user_len;
  struct passwd *user_entry;

  if (filename == 0)
    return ((char *)NULL);

  if (*filename != '~')
    return (savestring (filename));

  /* A leading `~/' or a bare `~' is *always* translated to the value of
     $HOME or the home directory of the current user, regardless of any
     preexpansion hook. */
  if (filename[1] == '\0' || filename[1] == '/')
    {
      /* Prefix $HOME to the rest of the string. */
      expansion = sh_get_env_value ("HOME");

      /* If there is no HOME variable, look up the directory in
    the password database. */
      if (expansion == 0)
    expansion = sh_get_home_dir ();

      return (glue_prefix_and_suffix (expansion, filename, 1));
    }

まさにここですね。~(tilde) であれば環境変数の HOME を返しています。

~+と~-

tilde.cの続き。

// bash-3.2/lib/tilde/tilde.c
  username = isolate_tilde_prefix (filename, &user_len);

  if (tilde_expansion_preexpansion_hook)
    {
      expansion = (*tilde_expansion_preexpansion_hook) (username);
      if (expansion)
    {
      dirname = glue_prefix_and_suffix (expansion, filename, user_len);
      free (username);
      free (expansion);
      return (dirname);
    }
    }

tilde_expansion_preexpansion_hook というのを見てみます。

// bash-3.2/general.c
void
tilde_initialize ()
{
  static int times_called = 0;

  /* Tell the tilde expander that we want a crack first. */
  tilde_expansion_preexpansion_hook = bash_special_tilde_expansions;
// bash-3.2/general.c
static char *
bash_special_tilde_expansions (text)
     char *text;
{
  char *result;

  result = (char *)NULL;

  if (text[0] == '+' && text[1] == '\0')
    result = get_string_value ("PWD");
  else if (text[0] == '-' && text[1] == '\0')
    result = get_string_value ("OLDPWD");
#if defined (PUSHD_AND_POPD)
  else if (DIGIT (*text) || ((*text == '+' || *text == '-') && DIGIT (text[1])))
    result = get_dirstack_from_string (text);
#endif

  return (result ? savestring (result) : (char *)NULL);
}

なんだこれは。
~+ だと カレントディレクトリ、 ~- だと OLDPWD を返しています。
というかOLDPWD知らんかった。
さらに +~2 とかもできるようです。地味に便利。

user名に応じたホームディレクトリを返す

さらにtilde.cの続き。

  /* No preexpansion hook, or the preexpansion hook failed.  Look in the
     password database. */
  dirname = (char *)NULL;
#if defined (HAVE_GETPWNAM)
  user_entry = getpwnam (username);
#else
  user_entry = 0;
#endif
  if (user_entry == 0)
    {
      /* If the calling program has a special syntax for expanding tildes,
    and we couldn't find a standard expansion, then let them try. */
      if (tilde_expansion_failure_hook)
    {
      expansion = (*tilde_expansion_failure_hook) (username);
      if (expansion)
        {
          dirname = glue_prefix_and_suffix (expansion, filename, user_len);
          free (expansion);
        }
    }
      /* If we don't have a failure hook, or if the failure hook did not
    expand the tilde, return a copy of what we were passed. */
      if (dirname == 0)
    dirname = savestring (filename);
    }
#if defined (HAVE_GETPWENT)
  else
    dirname = glue_prefix_and_suffix (user_entry->pw_dir, filename, user_len);
#endif

ここは一見分かりにくのですが、最後のところで getpwnamで得たpw_dir(ユーザ名に応じた home directory) を返しているようです。(当初この実装をどうやっているのか知りたかった。)
(e.g.: ~foo -> /home/foo)

感想

bashのソースを初めて読んだのですが、以外と読めるという印象を持ちました。
知らない機能を見つけたりするのも楽しいです。
ただhardtabとsofttab(しかもspace2つと4つ)が混在しているのはイラッとします。