web-dev-qa-db-ja.com

ls -Rが「再帰的」リストと呼ばれるのはなぜですか?

ls -Rはディレクトリのリストを表示することを理解しています。しかし、なぜ再帰的ですか?プロセスで再帰はどのように使用されますか?

35
Mint.K

まず、任意のフォルダー構造を定義しましょう。

.
├── a1 [D]
│   ├── b1 [D]
│   │   ├── c1
│   │   ├── c2 [D]
│   │   │   ├── d1
│   │   │   ├── d2
│   │   │   └── d3
│   │   ├── c3
│   │   ├── c4
│   │   └── c5
│   └── b2 [D]
│       ├── c6
│       └── c7
├── a2 [D]
│   ├── b3 [D]
│   │   ├── c8
│   │   └── c9
│   └── b4 [D]
│       ├── c10 
│       └── c11
├── a3 [D]
│   ├── b5
│   ├── b6
│   └── b7
└── a4 [D]

lsを実行すると、ベースフォルダーの出力のみが取得されます。

a1 a2 a3 a4

ただし、ls -Rを呼び出すと、異なる結果が得られます。

.:
a1  a2  a3  a4

./a1:
b1  b2

./a1/b1:
c1  c2  c3  c4  c5

./a1/b1/c2:
d1  d2  d3

./a1/b2:
c6  c7

./a2:
b3  b4

./a2/b3:
c8  c9

./a2/b4:
c10  c11

./a3:
b5  b6  b7

./a4:

ご覧のとおり、ベースフォルダーでlsを実行し、次にすべての子フォルダーを実行しています。そして、すべての孫フォルダ、無限に。事実上、コマンドはディレクトリツリーの最後に到達するまで、各フォルダーを再帰的に通過します。その時点で、ツリー内のブランチに戻り、サブフォルダーがあればそれに対して同じことを行います。

または、擬似コードで:

recursivelyList(directory) {
    files[] = listDirectory(directory)              // Get all files in the directory
    print(directory.name + ":\n" + files.join(" ")) // Print the "ls" output
    for (file : files) {                            // Go through all the files in the directory
        if (file.isDirectory()) {                   // Check if the current file being looked at is a directory
            recursivelyList(directory)              // If so, recursively list that directory
        }
    }
}

そして、私ができるので、同じの reference Java implementation

66
Kaz Wolfe

実際には、2つの密接に関連した質問があります。

  • ファイルシステム階層の各エントリに移動するプロセスが、本質的に再帰的なプロセスであるのはなぜですか?これは、 Zanna'sKaz Wolfe's などの他の回答によって対処されます。
  • lsの実装では、再帰のテクニックはどのように使用されますか?あなたの言い回し(「プロセスでの再帰の使用方法」)から、これはあなたが知りたいことの一部だと思います。この答えはその質問に対処します。

lsを再帰的な手法で実装するのが理にかなっている理由:

FOLDOC 定義 recursion として:

function (または procedure )がそれ自体を呼び出す場合。このような関数は「再帰的」と呼ばれます。呼び出しが1つ以上の他の関数を介して行われる場合、この関数グループは「相互再帰的」と呼ばれます。

lsを実装する自然な方法は、表示するファイルシステムエントリのリストを作成する関数と、パスとオプションの引数を処理し、必要に応じてエントリを表示する他のコードを記述することです。その関数は再帰的に実装される可能性が高いです。

オプションの処理中、lsは(-Rフラグで呼び出されることにより)再帰的に動作するように要求されたかどうかを判断します。その場合、表示するエントリのリストを作成する関数は、.および..を除き、リストするディレクトリごとに1回それ自体を呼び出します。この関数には再帰的バージョンと非再帰的バージョンが別々に存在する場合があります。または、関数は再帰的に動作することになっている場合は毎回チェックする場合があります。

Ubuntuの/bin/lsは、lsを実行したときに実行される実行可能ファイルであり、 GNU Coreutils によって提供され、many機能を備えています。その結果、 そのコード は予想よりもやや長く複雑です。 しかし、Ubuntuには BusyBoxが提供するlsのよりシンプルなバージョンも含まれています。これは、busybox lsと入力することで実行できます。

busybox lsが再帰を使用する方法:

BusyBoxのlsは、 coreutils/ls.c で実装されています。ディレクトリツリーを再帰的に印刷するために呼び出されるscan_and_display_dirs_recur()関数が含まれています。

static void scan_and_display_dirs_recur(struct dnode **dn, int first)
{
    unsigned nfiles;
    struct dnode **subdnp;

    for (; *dn; dn++) {
        if (G.all_fmt & (DISP_DIRNAME | DISP_RECURSIVE)) {
            if (!first)
                bb_putchar('\n');
            first = 0;
            printf("%s:\n", (*dn)->fullname);
        }
        subdnp = scan_one_dir((*dn)->fullname, &nfiles);
#if ENABLE_DESKTOP
        if ((G.all_fmt & STYLE_MASK) == STYLE_LONG || (G.all_fmt & LIST_BLOCKS))
            printf("total %"OFF_FMT"u\n", calculate_blocks(subdnp));
#endif
        if (nfiles > 0) {
            /* list all files at this level */
            sort_and_display_files(subdnp, nfiles);

            if (ENABLE_FEATURE_LS_RECURSIVE
             && (G.all_fmt & DISP_RECURSIVE)
            ) {
                struct dnode **dnd;
                unsigned dndirs;
                /* recursive - list the sub-dirs */
                dnd = splitdnarray(subdnp, SPLIT_SUBDIR);
                dndirs = count_dirs(subdnp, SPLIT_SUBDIR);
                if (dndirs > 0) {
                    dnsort(dnd, dndirs);
                    scan_and_display_dirs_recur(dnd, 0);
                    /* free the array of dnode pointers to the dirs */
                    free(dnd);
                }
            }
            /* free the dnodes and the fullname mem */
            dfree(subdnp);
        }
    }
}

再帰的な関数呼び出しが行われる行は次のとおりです。

                    scan_and_display_dirs_recur(dnd, 0);

再帰関数呼び出しが発生するのを見る:

デバッガーでbusybox lsを実行すると、動作中にこれを確認できます。最初に デバッグシンボル-dbgsym.ddebパッケージを有効にする をインストールしてから、busybox-static-dbgsymパッケージをインストールします。 gdbもインストールします(デバッガーです)。

Sudo apt-get update
Sudo apt-get install gdb busybox-static-dbgsym

単純なディレクトリツリーでcoreutils lsをデバッグすることをお勧めします。

便利なものがない場合は、1つ作成します(これは WinEunuuchs2Unixの答えmkdir -pコマンドと同じように機能します)。

mkdir -pv foo/{bar/foobar,baz/quux}

そして、いくつかのファイルを入力します:

(shopt -s globstar; for d in foo/**; do touch "$d/file$((i++))"; done)

busybox ls -R fooが期待どおりに動作することを確認して、次の出力を生成できます。

foo:
bar    baz    file0

foo/bar:
file1   foobar

foo/bar/foobar:
file2

foo/baz:
file3  quux

foo/baz/quux:
file4

デバッガーでbusyboxを開きます。

gdb busybox

GDBは、それ自体に関する情報を出力します。次に、次のようなメッセージが表示されます。

Reading symbols from busybox...Reading symbols from /usr/lib/debug/.build-id/5c/e436575b628a8f54c2a346cc6e758d494c33fe.debug...done.
done.
(gdb)

(gdb)は、デバッガーのプロンプトです。このプロンプトでGDBに最初に行うことは、scan_and_display_dirs_recur()関数の開始位置にブレークポイントを設定することです。

b scan_and_display_dirs_recur

それを実行すると、GDBから次のようなメッセージが表示されます。

Breakpoint 1 at 0x5545b4: file coreutils/ls.c, line 1026.

次に、GDBに、ls -R foo(または任意のディレクトリ名)を引数としてbusyboxを実行するように指示します。

run ls -R foo

次のようなものが表示される場合があります。

Starting program: /bin/busybox ls -R foo

Breakpoint 1, scan_and_display_dirs_recur (dn=dn@entry=0x7e6c60, first=1) at coreutils/ls.c:1026
1026    coreutils/ls.c: No such file or directory.

上記のようにNo such file or directoryが表示される場合、それは問題ありません。このデモンストレーションの目的は、scan_and_display_dirs_recur()関数がいつ呼び出されたかを確認することだけであるため、GDBは実際のソースコードを調べる必要はありません。

ディレクトリエントリが出力される前でも、デバッガがブレークポイントにヒットすることに注意してください。その関数へのentraceで中断しますが、その関数内のコードは、印刷用に列挙されるディレクトリに対して実行する必要があります。

GDBに続行するように指示するには、次を実行します。

c

scan_and_display_dirs_recur()が呼び出されるたびに、ブレークポイントに再びヒットするため、再帰の動作を確認できます。次のようになります((gdb)プロンプトとコマンドを含む):

(gdb) c
Continuing.
foo:
bar    baz    file0

Breakpoint 1, scan_and_display_dirs_recur (dn=dn@entry=0x7e6cb0, first=first@entry=0) at coreutils/ls.c:1026
1026    in coreutils/ls.c
(gdb) c
Continuing.

foo/bar:
file1   foobar

Breakpoint 1, scan_and_display_dirs_recur (dn=dn@entry=0x7e6cf0, first=first@entry=0) at coreutils/ls.c:1026
1026    in coreutils/ls.c
(gdb) c
Continuing.

foo/bar/foobar:
file2

foo/baz:
file3  quux

Breakpoint 1, scan_and_display_dirs_recur (dn=dn@entry=0x7e6cf0, first=first@entry=0) at coreutils/ls.c:1026
1026    in coreutils/ls.c
(gdb) c
Continuing.

foo/baz/quux:
file4
[Inferior 1 (process 2321) exited normally]

関数の名前にrecurが含まれています... BusyBoxは-Rフラグが指定されている場合にのみ使用しますか?デバッガーでは、これを簡単に見つけることができます。

(gdb) run ls foo
Starting program: /bin/busybox ls foo

Breakpoint 1, scan_and_display_dirs_recur (dn=dn@entry=0x7e6c60, first=1) at coreutils/ls.c:1026
1026    in coreutils/ls.c
(gdb) c
Continuing.
bar    baz    file0
[Inferior 1 (process 2327) exited normally]

-Rがなくても、lsのこの特定の実装は、同じ関数を使用して、どのファイルシステムエントリが存在するかを調べて表示します。

デバッガーを終了する場合は、次のように伝えます。

q

scan_and_display_dirs_recur()がそれ自体を呼び出す必要があるかどうかを知る方法:

具体的には、-Rフラグが渡されたときに、どのように異なる動作をしますか? ソースコード (Ubuntuシステムの正確なバージョンではない可能性があります)を調べると、内部データ構造G.all_fmtがチェックされ、どのオプションで呼び出されたかが保存されていることがわかります。

            if (ENABLE_FEATURE_LS_RECURSIVE
             && (G.all_fmt & DISP_RECURSIVE)

-RのサポートなしでBusyBoxがコンパイルされている場合、ファイルシステムエントリを再帰的に表示しようとはしません。これがENABLE_FEATURE_LS_RECURSIVE部分の目的です。)

G.all_fmt & DISP_RECURSIVEがtrueの場合にのみ、再帰的な関数呼び出しを含むコードが実行されます。

                struct dnode **dnd;
                unsigned dndirs;
                /* recursive - list the sub-dirs */
                dnd = splitdnarray(subdnp, SPLIT_SUBDIR);
                dndirs = count_dirs(subdnp, SPLIT_SUBDIR);
                if (dndirs > 0) {
                    dnsort(dnd, dndirs);
                    scan_and_display_dirs_recur(dnd, 0);
                    /* free the array of dnode pointers to the dirs */
                    free(dnd);
                }

それ以外の場合、関数は1回だけ実行されます(コマンドラインで指定されたディレクトリごとに)。

23
Eliah Kagan

考えてみると、ディレクトリとそのファイルとディレクトリ、およびそのファイルとディレクトリ、およびそのファイルとディレクトリとそのファイルに作用するコマンドには「再帰的」という意味があります.........

....指定されたポイントから下のツリー全体がコマンドによって操作されるまで、この場合は、サブディレクトリのサブディレクトリのサブディレクトリの内容をリストします..........コマンドの引数

16
Zanna

-Rは再帰用であり、大まかに「繰り返し」と呼ばれます。

たとえば、次のコードをご覧ください。

───────────────────────────────────────────────────────────────────────────────
$ mkdir -p temp/a
───────────────────────────────────────────────────────────────────────────────
$ mkdir -p temp/b/1
───────────────────────────────────────────────────────────────────────────────
$ mkdir -p temp/c/1/2
───────────────────────────────────────────────────────────────────────────────
$ ls -R temp
temp:
a  b  c

temp/a:

temp/b:
1

temp/b/1:

temp/c:
1

temp/c/1:
2

temp/c/1/2:

ディレクトリ作成の-pを使用すると、1つのコマンドでディレクトリを大量に作成できます。 1つ以上の最上位中間ディレクトリが既に存在する場合、エラーではなく、中間下位ディレクトリが作成されます。

次に、ls -Rは、tempで始まり、ツリーを下ってすべてのブランチに移動するすべてのディレクトリを再帰的にリストします。

ls -Rコマンドの補完、つまりtreeコマンドを見てみましょう。

$ tree temp
temp
├── a
├── b
│   └── 1
└── c
    └── 1
        └── 2

6 directories, 0 files

ご覧のとおり、treels -Rと同じことを達成しますが、より簡潔であえて「きれい」と言います。

ここで、作成したディレクトリを1つの簡単なコマンドで再帰的に削除する方法を見てみましょう。

$ rm -r temp

これにより、tempとその下のすべてのサブディレクトリが再帰的に削除されます。すなわち、temp/atemp/b/1、およびtemp/c/1/2に加えて、中間の中間ディレクトリ。

7

ここに簡単な説明があります。サブディレクトリのコンテンツを表示することになると、同じ関数はすでにディレクトリをどうするかを知っているので、理にかなっています。したがって、その結果を得るには、各サブディレクトリで自分自身を呼び出すだけです!

擬似コードでは、次のようになります。

recursive_ls(dir)
    print(files and directories)
    foreach (directoriy in dir)
        recursive_ls(directory)
    end foreach
end recursive_ls
5
TommyD