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

时间:2016-08-18 10:12:38

标签: 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;

因此,在将file重命名为prefix_file之后,readdir将在prefix_file循环的后续迭代中找不到while(然后将其重命名)再次到prefix_prefix_file等等?可能很明显它不会这样做,但由于我在文档中找不到它,我还是会问这个问题。

2 个答案:

答案 0 :(得分:7)

答案

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

  

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

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

示范

ikegami asked

  但是,

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

我回答:

  

它(rename())更改目录中的条目;会发生什么取决于[文件系统]的实现方式。如果您将文件名从a更改为humongous-long-name-that-is-too-boring-to-be-believable,那么该条目将在磁盘上的目录中移动,这会导致未指定的行为[如主要答案中所述]。 ......是否...... rename()实际上用readdir()搞砸了扫描取决于系统(操作系统和文件系统),这就是我声称的全部内容。

经过进一步讨论,我创建了一个关于某个特定系统能够和确实发生的事情的例子。我使用了以下步骤:

  • 创建目录 - 其名称无关紧要。
  • 转到该目录。
  • readdir.cmake.files.sh复制到目录中。
  • 从源代码readdir创建程序readdir.c(例如,使用make readdir)。
    • 该代码假定struct dirent包含非POSIX强制要求的成员d_namlen
    • 没有它是可行的(但需要进行微小的改动)。
  • 创建文件(或目录)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
    $
  • 运行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
    $

这是我使用默认HFS +在Mac OS X 10.11.6 El Capitan上获得的 文件系统。当目录很小(没有适度长 文件名),然后重命名的文件没有显示出来。额外的时候 创建文件,使目录大小约为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

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);
}

答案 1 :(得分:1)

最简单的方法是首先chdir进入你的目录,这样就不会出现构建路径的问题,然后在列表上下文中使用glob,它将一次性返回所有名称混乱的可能性

看起来像这样(未经测试)

use strict;
use warnings 'all';
use autodie qw/ chdir rename /;

chdir 'tdir';

rename $_, "prefix_$_" for grep -f, glob '*';