Pocket

こんにちは。根本です。

例えば、webアプリケーションを開発しているとします。
もちろんフレームワークの選定をし、そのフレームワークを用いて実装をしているでしょう。
そしてフレームワークの挙動を確認するために、ソースコードを漁る。

こんなことはよくあることではないでしょうか?

使うことは出来るが(使えればいいんですがね)、
フレームワークの挙動の詳細を理解していないシーンは珍しくありません。

これは、そんなフレームワークに限ったことではなく、

例えば、私の目の前のターミナルには、
リリース手順を書きかけの状態で、コマンドの結果としてカレントディレクトリが表示されています。

pwd

これは老若男女(?)誰もが知っている、現在のカレントディレクトリを返すコマンド。

でも実際には「どう返しているのか?」と問われると、私は「?!」。

気になったら善は急げ。ソースコードを読もう!

ソースコードの準備

一応、pwdコマンドのパッケージを確認。

rpm -qf `which pwd`
  # coreutils-8.4-37.el6.x86_64

そしてソースコードをダウンロード

wget "http://ftp.gnu.org/gnu/coreutils/coreutils-8.4.tar.gz"
tar -zxvf coreutils-8.4.tar.gz; cd coreutils-8.4

目的のコードは src/pwd.c
(ソースコード)

コード量は

wc -l src/pwd.c
  # 390 src/pwd.c

結構短い。

まずは main 関数。
最初にコマンドライン引数を処理しています。
-L (–logical) と -P(–physical)のオプションが渡されたとき、
logical 変数のフラグを変えていることが分かります。(346行~351行)

324 int
325 main (int argc, char **argv)
326 {
327   char *wd;
328   /* POSIX requires a default of -L, but most scripts expect -P.  */
329   bool logical = (getenv ("POSIXLY_CORRECT") != NULL);
330
331   initialize_main (&argc, &argv);
332   set_program_name (argv[0]);
333   setlocale (LC_ALL, "");
334   bindtextdomain (PACKAGE, LOCALEDIR);
335   textdomain (PACKAGE);
336
337   atexit (close_stdout);
338
339   while (1)
340     {
341       int c = getopt_long (argc, argv, "LP", longopts, NULL);
342       if (c == -1)
343         break;
344       switch (c)
345         {
346         case 'L':
347           logical = true;
348           break;
349         case 'P':
350           logical = false;
351           break;
352
353         case_GETOPT_HELP_CHAR;
354
355         case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS);
356
357         default:
358           usage (EXIT_FAILURE);
359         }
360     }
361
362   if (optind < argc)
363     error (0, 0, _("ignoring non-option arguments"));

こちらが --help ですが、

/bin/pwd --help
  # Usage: /bin/pwd [OPTION]...
  # Print the full filename of the current working directory.
  # 
  #   -L, --logical   use PWD from environment, even if it contains symlinks
  #   -P, --physical  avoid all symlinks
  #       --help     display this help and exit
  #       --version  output version information and exit
  # 
  # ...

-L-P の違いは、以下で理解できると思います。

mkdir a
ln -sn a b; cd b
/bin/pwd -L
  # /home/nemoto/b
/bin/pwd -P
  # /home/nemoto/a

話を戻して main関数 の続きです。
はじめに logicaltrue の場合の処理を行います。
つまり -L オプションの処理です。

365   if (logical)
366     {
367       wd = logical_getcwd ();
368       if (wd)
369         {
370           puts (wd);
371           exit (EXIT_SUCCESS);
372         }
373     }

logical_getcwd 関数を呼んでいまが、この関数は src/pwd.c 内にあります。

297 static char *
298 logical_getcwd (void)
299 {
300   struct stat st1;
301   struct stat st2;
302   char *wd = getenv ("PWD");
303   char *p;
304
305   /* Textual validation first.  */
306   if (!wd || wd[0] != '/')
307     return NULL;
308   p = wd;
309   while ((p = strstr (p, "/.")))
310     {
311       if (!p[2] || p[2] == '/'
312           || (p[2] == '.' && (!p[3] || p[3] == '/')))
313         return NULL;
314       p++;
315     }
316
317   /* System call validation.  */
318   if (stat (wd, &st1) == 0 && stat (".", &st2) == 0 && SAME_INODE(st1, st2))
319     return wd;
320   return NULL;
321 }

logical_getcwd では、

302行目で getenv で 環境変数PWD を参照しています。

305行目以降では、その値のバリデーションをチェックしています。

317行目以降では、環境変数PWD のディレクトリ情報と、カレントディレクトリ . のディレクトリ情報が同じであるかをチェックしているということがわかりますね?

ということは、以下のように環境変数 PWD を操作し、 pwd コマンドを実行すると

( PWD=/home; /bin/pwd -L )
  # /home/nemoto/a

というように、上の例で -L オプションを指定し取得できた値と異なる値が取れていることがわかります。

368行目を見ると、NULLの場合には次の処理へ流れていくことがわかると思います。

この次に説明しますが、その次の処理というのが -P オプションの処理になります。

つまり、環境変数PWDの値がカレントディレクトリと異なる場合には、-Pオプションと同一の結果を表示することになります。

では、その続きのmain関数。つまり -P オプションを指定した場合の処理です。

375   wd = xgetcwd ();
376   if (wd != NULL)
377     {
378       puts (wd);
379       free (wd);
380     }
381   else
382     {
383       struct file_name *file_name = file_name_init ();
384       robust_getcwd (file_name);
385       puts (file_name->start);
386       file_name_free (file_name);
387     }
388
389   exit (EXIT_SUCCESS);
390 }

375行目で xgetcwd を呼んでいます。
こちらは、lib/xgetcwd.c を参照してください。以下がコードです。

 34 char *
 35 xgetcwd (void)
 36 {
 37   char *cwd = getcwd (NULL, 0);
 38   if (! cwd && errno == ENOMEM)
 39     xalloc_die ();
 40   return cwd;
 41 }

単純に getcwd を呼んでいます。これが pwd -P の実体です。

では、main関数の残りを見ていきましょう。

376行目では、xgetcwd でディレクトリが取得できれば、それを表示しています。

382行目以降では、そこで取得できなかった場合の処理へと続きます。( robust_getcwd )

265 static void
266 robust_getcwd (struct file_name *file_name)
267 {
268   size_t height = 1;
269   struct dev_ino dev_ino_buf;
270   struct dev_ino *root_dev_ino = get_root_dev_ino (&dev_ino_buf);
271   struct stat dot_sb;
272
273   if (root_dev_ino == NULL)
274     error (EXIT_FAILURE, errno, _("failed to get attributes of %s"),
275            quote ("/"));
276
277   if (stat (".", &dot_sb) < 0)
278     error (EXIT_FAILURE, errno, _("failed to stat %s"), quote ("."));
279
280   while (1)
281     {
282       /* If we've reached the root, we're done.  */
283       if (SAME_INODE (dot_sb, *root_dev_ino))
284         break;
285
286       find_dir_entry (&dot_sb, file_name, height++);
287     }
288
289   /* See if a leading slash is needed; file_name_prepend adds one.  */
290   if (file_name->start[0] == '')
291     file_name_prepend (file_name, "", 0);
292 }

これは何をしているかというと、280行~287行で、find_dir_entryを呼んでいます。

こちらは、file_name で指定したディレクトリの親を検索し、参照渡ししている dot_sb にセットしています。

while では、dot_sb が / のディレクトリ情報と同じになるまで続き、その結果返す。という処理の流れになります。

find_dir_entrysrc/pwd.c 内にあります。ここでは割愛)

pwd コマンドのまとめ

pwd コマンドの流れは、

-L オプション :

  1. 環境変数PWDを返す。
  2. 取得できなかった場合は、-P オプションの処理に続く。

-P オプション(default)

  1. getcwdの結果を返す。
  2. 取得できなかった場合は、カレントディレクトリから / まで辿り、そのパスを返す。

となるのかな。

まとめ

今回は pwd のコードリーディングを行いました。
結果、どのような挙動をしているのかを理解することが出来ました。

プログラムの挙動がどのようになっているのかを理解することは、運用上必要のないことなのかもしれません。
しかし、冒頭でも話したように、現場では摩訶不思議な挙動による障害に悩まされることは、珍しいことではないとはずです。
その場合、ソースコードレベルでの調査は、避けて通れないのではないのかと。

その中、コードリーディングの癖をつけておくことは、業務改善の一環になるのではないでしょうか?
みなさんも暇を見つけて、先人の英知に触れてみてはどうでしょう?

面白かったのでまた、面白いコードがあればご紹介するかも!?