使用readdir时重命名文件是否安全?

Håk*_*and 4 perl

使用扫描目录时readdir,您是否可以安全地重命名文件而不必担心输入无限递归?例如:

use v5.12;  # make readdir set $_ in while loops
use strict;
use warnings;

use File::Spec;

my $dir = 'tdir';    
opendir ( my $dh, $dir ) or die "Could not open dir '$dir': $!";
while (readdir $dh) {
    next if /^\.\.?\z/;
    my $filename = File::Spec->catfile( $dir, $_ );
    if ( -f $filename) {
        my $newname = File::Spec->catfile( $dir, "prefix_$_" );
        rename ($filename, $newname) or warn $!;
    }
}

closedir $dh;
Run Code Online (Sandbox Code Playgroud)

因此,重命名,例如后fileprefix_file,readdir不会发现prefix_file在一个后面的迭代while循环(然后再重命名为prefix_prefix_file等等?也许明显,它不会做,但因为我无法找到我的文档中提到的无论如何我会问这个问题.

Jon*_*ler 7

回答

底层系统调用是POSIX readdir(),规范说:

如果在最近一次调用之后从目录中删除或添加了文件,opendir()或者rewinddir()是否readdir()未指定后续调用以返回该文件的条目.

它只是意味着您可能会或可能不会看到文件.您可能会发现特定平台确实指定了发生的情况,但它可能无法移植到其他系统.


示范

池上 :

rename但是,既不添加也不删除任何目录条目.它只编辑了一个.

我回答了:

It(rename())更改目录中的条目; 会发生什么取决于[文件系统]的实现方式.如果您将文件名更改ahumongous-long-name-that-is-too-boring-to-be-believable,则该条目将很可能在磁盘上的目录中移动,从而导致未指定的行为[如主要答案中所述].......是否...... rename()实际上是否会扫描一次扫描readdir()取决于系统(操作系统和文件系统),这就是我所声称的.

经过进一步的讨论,我创建了一个关于一个特定系统能够和确实发生了什么的例子.我用过的步骤:

  • 创建一个目录 - 它的名称无关紧要.
  • 切换到该目录.
  • 复制readdir.cmake.files.sh进入目录.
  • readdir从源创建程序readdir.c(make readdir例如,使用).
    • 代码假定struct dirent包含成员d_namlen不是POSIX强制要求的.
    • 没有它是可行的(但需要进行微小的改动).
  • 创建文件(或目录)a.
  • ./readdir.提示您时返回.您应该看到与此类似的输出,但inode编号将不同.
    $ ./readdir
    44249044: (  1) .
    42588881: (  2) ..
    44260959: ( 10) .gitignore
    44398380: (  1) a
    Found entry 'a' - hit return to continue: 
    Continuing...
    44398371: ( 10) make.files
    44398280: ( 13) make.files.sh
    44398338: (  8) makefile
    44398351: (  7) readdir
    44260963: (  9) readdir.c
    44398352: ( 12) readdir.dSYM
    44260960: (  9) README.md
    44398364: (  6) rename
    44260964: (  8) rename.c
    44398365: ( 11) rename.dSYM
    $
Run Code Online (Sandbox Code Playgroud)
  • sh make.files.sh.这将创建文件moderately-long-file-name.000.. moderately-long-file-name.999.
  • 再跑./readdir一次.不要回来了.
  • 切换到其他终端窗口.
  • 将目录更改为运行测试的目录.
  • 跑: mv a zzz-let-sleeping-file-renames-lie-unperturbed
  • 切换回终端窗口运行readdir.
  • 点击返回.你可能会看到类似于的输出:
    $ ./readdir
    44249044: (  1) .
    42588881: (  2) ..
    44260959: ( 10) .gitignore
    44398380: (  1) a
    Found entry 'a' - hit return to continue: 
    Continuing...
    44398371: ( 10) make.files
    44398280: ( 13) make.files.sh
    44398338: (  8) makefile
    44431473: ( 29) moderately-long-file-name.000
    44431474: ( 29) moderately-long-file-name.001
    44431475: ( 29) moderately-long-file-name.002
    ...
    44432470: ( 29) moderately-long-file-name.997
    44432471: ( 29) moderately-long-file-name.998
    44432472: ( 29) moderately-long-file-name.999
    44398351: (  7) readdir
    44260963: (  9) readdir.c
    44398352: ( 12) readdir.dSYM
    44260960: (  9) README.md
    44398364: (  6) rename
    44260964: (  8) rename.c
    44398365: ( 11) rename.dSYM
    44398380: ( 45) zzz-let-sleeping-file-renames-lie-unperturbed
    $
Run Code Online (Sandbox Code Playgroud)

这是我在Mac OS X 10.11.6 El Capitan上使用默认的HFS +文件系统.当目录很小(没有中等长度的文件名)时,重命名的文件没有显示出来.创建额外文件以使目录大小约为34 KiB时,重命名的文件确实显示出来.

这表明在某些文件系统(特别是Apple的HFS +)上,在某些情况下,readdir()目录的扫描会受到文件重命名操作的影响.如果你想编写和使用rename命令而不是使用mv,那么就这样 - 当我尝试时,它对结果没有任何影响.

结论

在其他文件系统或其他操作系统上,YMMV.但是,这足以证明在某些系统上,在readdir()扫描进行过程中重命名文件 最终可能会在输出中出现两次相同的"文件".

make.files.sh

#!/bin/sh

for file in $(seq -f 'moderately-long-file-name.%03.0f' 0 999)
do > "$file"
done
Run Code Online (Sandbox Code Playgroud)

readdir.c

/* SO 3901-5527 - attempt to demonstrate renaming moving entries */
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static const char *stop_after = "a";

static void process_directory(const char *dirname)
{
    DIR *dp = opendir(dirname);

    if (dp == 0)
        fprintf(stderr, "Failed to open directory %s\n", dirname);
    else
    {
        struct dirent *entry;
        while ((entry = readdir(dp)) != 0)
        {
            /* Ignore current and parent directory */
            printf("%8d: (%3d) %s\n", (int)entry->d_ino, entry->d_namlen, entry->d_name);
            if (strcmp(entry->d_name, stop_after) == 0)
            {
                printf("Found entry '%s' - hit return to continue: ", stop_after);
                fflush(stdout);
                char *buffer = 0;
                size_t buflen = 0;
                getline(&buffer, &buflen, stdin);
                free(buffer);
                printf("Continuing...\n");
            }
        }
        closedir(dp);
    }
}

int main(int argc, char **argv)
{
    int opt;
    while ((opt = getopt(argc, argv, "s:")) != -1)
    {
        switch (opt)
        {
        case 's':
            stop_after = optarg;
            break;;
        default:
            fprintf(stderr, "%s: Unrecognized option '-%c'\n", argv[0], optopt);
            fprintf(stderr, "Usage: %s [-s stop_after] [directory ...]\n", argv[0]);
            return(EXIT_FAILURE);
        }
    }
    if (optind == argc)
        process_directory(".");
    else
    {
        for (int i = optind; i < argc; i++)
            process_directory(argv[i]);
    }
    return(0);
}
Run Code Online (Sandbox Code Playgroud)