Skip to content

Latest commit

 

History

History
367 lines (298 loc) · 17 KB

File metadata and controls

367 lines (298 loc) · 17 KB

六十六、文件和文件名

除了读写文件,C++ 标准库还具有操作整个文件的功能,例如复制、重命名和删除文件。它也有一个可移植的方式来处理文件名和目录名。让我们看看文件系统库提供了什么。

这个探索中的所有内容都在<filesystem>中声明,并且位于std::filesystem名称空间中。虽然我更喜欢使用完全限定的名称,正如我在之前的 60 篇探索中所做的那样,但是这个名称太长了。在本文和后续研究中,假设使用以下名称空间别名:

namespace fsys = std::filesystem;

可移植文件名

<filesystem>库的关键是一种使用可移植 API 表示文件名和路径的方法,或者至少在使用许多不同文件系统的情况下尽可能地移植,从复杂的网络存储设备到灯泡和其他物联网(IoT)设备。使这成为可能的类是fsys::path

fsys::path类根据根名称、根目录和相对路径来抽象路径名。相对路径是文件名和分隔符的序列。您可以使用文件系统的首选分隔符或'/',这称为后备分隔符。它也是 UNIX、POSIX 和类似操作系统的首选分隔符。在微软的 Windows 上,首选的分隔符是'\\',它会给 C++ 字符串带来各种各样的麻烦,所以使用'/'通常更容易,甚至在 Windows 上也是如此。

大多数用于桌面工作站和服务器的现代文件系统可以使用 UTF-8 或 UTF-16 编码处理 Unicode 文件名,但回到物联网和类似设备,它们可能不会。如果您需要确保在尽可能多的环境中的可移植性,您应该将文件名限制为字母数字字符、下划线('_')、连字符('-')和句点('.'),它们构成了 POSIX 可移植文件名字符集。

根名称可以是 DOS 驱动器号和冒号或两个分隔符来表示网络名称。它可以是后跟冒号的主机名。根名称的存在是可移植路径 API 的一部分,但是它的含义完全取决于本地环境。目录是分层的,从根开始,由初始分隔符表示。

单个 path 对象可以保存完整的路径名或部分路径名。如果路径包含分隔符,则最后一个分隔符之后的路径部分称为文件名。文件名可以有一个词干,后跟一个扩展名。扩展名是最右边的句点,后跟非句点字符。但是如果唯一的句点是第一个字符,则文件名等于词干,扩展名为空。

您可以从字符串构造一个 path 对象,也可以从多个 path 元素构造一个 path 对象。/操作符被重载,用一个插入的目录分隔符组合路径。给定一个路径对象,您可以将它分解成组成部分,修改各部分,并添加文件名。

清单 66-1 演示了路径对象的各种用法。

import <filesystem>;
import <iostream>;

namespace fsys = std::filesystem;

int main()
{
   std::string line;
   while (std::getline(std::cin, line))
   {
      fsys::path path{line};
      std::cout <<
         "root-name:      " << path.root_name() << "\n"
         "root-directory: " << path.root_directory() << "\n"
         "relative-path:  " << path.relative_path() << "\n"
         "parent-path:    " << path.parent_path() << "\n"
         "filename:       " << path.filename() << "\n"
         "stem:           " << path.stem() << "\n"
         "extension:      " << path.extension() << "\n"
         "generic path:   " << path.generic_string() << "\n"
         "native path:    " << path.string() << '\n';

      fsys::path newpath;
      newpath = path.root_path() / "top" / "subdir" / "stem.ext";
      std::cout << "newpath = " << newpath << '\n';
      newpath.replace_filename("newfile.newext");
      std::cout << "newpath = " << newpath << '\n';
      newpath.replace_extension(".old");
      std::cout << "newpath = " << newpath << '\n';
      newpath.remove_filename();
      std::cout << "newpath = " << newpath << '\n';
   }
}

Listing 66-1.Demonstrating the path Class

一些命名空间范围的函数对路径名执行其他操作:

  • path absolute(path const& p)p转换为绝对路径。如果操作系统有当前工作目录的概念,current_path()将该目录作为path对象返回,absolute()可以使用current_path()作为参数p的前缀。

  • path canonical(path const& p)通过删除目录名"."(当前目录)和".."(父目录)并解析符号链接,将p转换为规范路径。

  • path relative(path const& p, path const& base=current_path())p转换为相对于base的路径。

路径名是你可能遇到国际字符集的另一个地方(探索 59 )。path类型实际上是针对主机环境的basic_path的特化,通常是charwchar_t。path 类以依赖于操作系统的方式将字符串转换为路径,然后再转换回来。

使用文件

除了文件名,标准库还提供了许多操作整个文件及其属性的函数。以一种可移植的方式查询和操作文件属性,比如权限、日期和时间等等是一个挑战,本书不能涵盖<filesystem>模块中的所有复杂性。这一节触及了一些重点。

一般来说,标准 C++ 库从 POSIX 标准中得到启示。例如,文件权限是 POSIX 权限的直接映射,并且不支持访问控制列表等复杂性。类似地,C++ 文件类型是 POSIX 文件类型的映射,比如套接字、管道和字符设备,尽管允许实现添加其他文件类型。

POSIX 为单个文件提供了两种拥有多个名称的方法。这两种方式都称为链节,分为链节和链节。软链接也称为符号链接或简称符号链接。硬链接是直接指向文件内容的目录条目。相同文件内容的两个硬链接无法区分。fsys::hard_link_count()函数返回指向同一个文件的硬链接的数量。fsys::create_hard_link()函数创建一个新的硬链接。

符号链接是包含另一个文件路径的目录条目。该路径可以是绝对路径,也可以是相对于包含软链接的目录的路径。使用符号链接可能会导致目标路径不存在。要创建新的符号链接,调用fsys::create_directory_symlink()链接到一个目录,调用fsys::create_symlink()链接到一个文件。fsys::read_symlink()函数可以读取任何一种符号链接的内容。

要查询一个文件的属性,调用status(),它返回一个fsys::file_status对象,该对象又有一个permissions()成员函数来返回文件权限,还有type()返回文件类型,比如fsys::file_type::regular。如果文件是符号链接,则返回目标文件的状态。调用fsys::symlink_status()来获取符号链接本身的状态;如果文件不是符号链接,symlink_status()就像status()。此外,fsys::is_regular_file()和类似的功能存在,以直接查询一个文件类型。

可以通过fsys::last_write_time()查询和设置文件的修改时间。C++ 标准库在<chrono>模块中有丰富复杂的日期时间库。它的用途超出了本书的范围。std::format()函数理解文件时间。在冒号之后,使用{:%F %T}表示 ISO 日期和时间,或者使用{:%x %X}表示特定于地区的日期和时间格式。许多其他选项也是可能的。有关详细信息,请查阅最新参考资料。

清单 66-2 通过展示一个非常简单的文件清单程序,类似于 POSIX ls或 DOS dir命令,演示了这些函数的使用。对于第一个程序,它在命令行上只需要一个文件名。随着探索的进展,我们将扩展这个小程序的功能。

import <filesystem>;
import <format>;
import <iostream>;
import <iterator>;

namespace fsys = std::filesystem;

void print_file_type(std::ostream& stream, fsys::path const& path)
{
    auto status{ fsys::symlink_status(path) };
    if (fsys::is_symlink(status)) {
        auto link{ fsys::read_symlink(path) };
        stream << " -> " << link.generic_string();
    }
    else if (fsys::is_directory(status))
        stream << '/';
    else if (fsys::is_fifo(status))
        stream << '|';
    else if (fsys::is_socket(status))
        stream << '=';
    else if (fsys::is_character_file(status))
        stream << "(c)";
    else if (fsys::is_block_file(status))
        stream << "(b)";
    else if (fsys::is_other(status))
        stream << "?";
}

void print_file_info(std::ostream& stream, fsys::path const& path)
{
    std::format_to(std::ostreambuf_iterator<char>(stream),
        "{0:>16} {1:%F %T} ",
        fsys::file_size(path),
        fsys::last_write_time(path));
    stream << path.generic_string();
    print_file_type(stream, path);
    stream << '\n';
}

int main(int, char** argv)
{
    if (argv[1] == nullptr)
    {
        std::cerr << "usage: " << argv[0] << " FILENAME\n";
        return EXIT_FAILURE;
    }
    fsys::path path{ argv[1] };
    try
    {
        print_file_info(std::cout, path);
    }
    catch(fsys::filesystem_error const& ex)
    {
        std::cerr << ex.what() << '\n';
    }
}

Listing 66-2.Demonstrating the path Class

fsys::copy_symlink()函数顾名思义,创建一个包含现有符号链接副本的新符号链接。fsys::copy_file()函数创建一个新文件,并将现有文件的内容复制到新文件中。可选的最终参数允许您控制是否允许覆盖现有文件。fsys::copy()功能结合了其他复印功能及更多功能。复制选项指示它应该如何处理符号链接和目录,甚至允许递归复制整个目录树。

要重命名文件,调用fsys::rename(),要删除文件,调用fsys::remove()。要删除整个目录树,调用fsys::remove_all()。存在许多其他文件级函数;有关详细信息,请查阅好的参考资料。

错误

如果您尝试过运行清单 66-2 中的程序,或者自己做过实验,您可能会发现文件系统库会对任何类型的错误或异常结果抛出异常。通常,您会遇到某些问题,例如,如果用户键入错误的文件名,文件就会丢失。权限错误很常见,等等。所以这个库让你决定是抛出一个异常还是返回一个错误代码。

当您认为错误很常见时,将一个std::error_code对象作为附加的最终参数传递给任何文件系统函数。该函数将始终存储一个结果,而不是抛出一个异常,如果成功,该异常将为零,如果失败,则为其他值。将error_code视为布尔值意味着错误为真,成功为假。如果这让你感到困扰,.value()成员函数将代码作为一个整数返回,你可以显式地与零进行比较。

重写清单 66-2 使用 error_code 代替依赖异常。这也意味着接受多个命令行参数是有意义的。程序可以为每个参数发出一条错误消息,而不是在第一次出错时就终止。您可以将error_code本身打印到一个输出流中,但这只会显示数字代码。message()成员函数返回相应的字符串消息。参见我在清单 66-3 中的重写。

import <filesystem>;
import <format>;
import <iostream>;
import <iterator>;
import <string_view>;
import <system_error>;

namespace fsys = std::filesystem;

void print_file_type(std::ostream& stream, fsys::path const& path, fsys::file_status status)
{
    if (fsys::is_symlink(status)) {
        std::error_code ec;
        auto link{ fsys::read_symlink(path, ec) };
        if (ec)
            stream << ": " << ec.message();
        else
            stream << " -> " << link.generic_string();
    }
    else if (fsys::is_directory(status))
        stream << '/';
    else if (fsys::is_fifo(status))
        stream << '|';
    else if (fsys::is_socket(status))
        stream << '=';
    else if (fsys::is_character_file(status))
        stream << "(c)";
    else if (fsys::is_block_file(status))
        stream << "(b)";
    else if (fsys::is_other(status))
        stream << "?";
}

// There may be many reasons why a file has no size, e.g., it is
// a directory. So don't treat it as an error--just return zero.
uintmax_t get_file_size(fsys::path const& path)
{
    std::error_code ec;
    auto size{ fsys::file_size(path, ec) };
    if (ec.value() != 0)
        return 0;
    else
        return size;
}

// Similarly, return a false timestamp for any error.
fsys::file_time_type get_last_write_time(fsys::path const& path)
{
    std::error_code ec;
    auto time{ fsys::last_write_time(path, ec) };
    if (ec)
        return fsys::file_time_type{};
    else
        return time;
}

void print_file_info(std::ostream& stream, fsys::path const& path)
{
    std::error_code ec;
    auto status{ fsys::symlink_status(path, ec) };
    if (ec)
        stream << path.generic_string() << ": " << ec.message();
    else
    {
        std::format_to(std::ostreambuf_iterator<char>(stream),
            "{0:>16} {1:%F %T} {2}",
            get_file_size(path),
            get_last_write_time(path),
            path.generic_string());
        print_file_type(stream, path, status);
    }

    stream << '\n';
}

int main(int, char** argv)
{
    if (argv[1] == nullptr or std::string_view(argv[1]) == "--help")
    {
        std::cerr << "usage: " << argv[0] << " FILENAME\n";
        return EXIT_FAILURE;
    }
    while (*++argv != nullptr)
    {
        fsys::path path{ *argv };
        print_file_info(std::cout, path);
    }
}

Listing 66-3.Examining Errors with error_code

下一个任务是递归进入目录。下一节将介绍目录条目和迭代器。

导航目录

目录(通常称为文件夹)包含文件条目,可以是任何类型的文件,包括另一个目录。要发现目录中的条目,需要构造一个目录迭代器。使用fsys::directory_iterator查看单个目录中的条目,或者使用fsys::recursive_directory_iterator遍历子目录中的条目。像往常一样,用可选的error_code参数构造带有目录路径的迭代器类型。即使目录迭代器是一个迭代器,它也可以在一个 ranged for循环或 ranged 函数中用作一个范围。

目录迭代器的值类型是fsys::directory_entry,它包含文件的名称、状态和其他信息。所有的操作系统和文件系统在细节上有所不同,但是通常迭代目录的行为检索关于文件的信息,因此不需要进行单独的系统调用来获得相同的信息。因此,directory_entry存储文件状态、修改时间等等,否则您必须调用fsys函数来获取这些信息。

根据这些信息,你现在可以修改清单 66-3 到目录下。我的版本在清单 66-4 中。请注意我也是如何在命令行中使用directory_entry命名文件的。这通过一种显示文件信息的方式简化了代码。

import <filesystem>;
import <format>;
import <iostream>;
import <iterator>;
import <system_error>;

namespace fsys = std::filesystem;

void print_file_type(std::ostream& stream, fsys::directory_entry const& entry)
{
    auto status{ entry.symlink_status() };
    if (fsys::is_symlink(status)) {
        std::error_code ec;
        auto link{ fsys::read_symlink(entry.path(), ec) };
        if (ec)
            stream << ": " << ec.message();
        else
            stream << " -> " << link.generic_string();
    }
    else if (fsys::is_directory(status))
        stream << '/';
    else if (fsys::is_fifo(status))
        stream << '|';
    else if (fsys::is_socket(status))
        stream << '=';
    else if (fsys::is_character_file(status))
        stream << "(c)";
    else if (fsys::is_block_file(status))
        stream << "(b)";
    else if (fsys::is_other(status))
        stream << "?";
}

// There may be many reasons why a file has no size, e.g., it is
// a directory. So don't treat it as an error--just return zero.
uintmax_t get_file_size(fsys::directory_entry const& entry)
{
    std::error_code ec;
    auto size{ entry.file_size(ec) };
    if (ec)
        return 0;
    else
        return size;
}

// Similarly, return a false timestamp for any error.
fsys::file_time_type get_last_write_time(fsys::directory_entry const& entry)
{
    std::error_code ec;
    auto time{ entry.last_write_time(ec) };
    if (ec)
        return fsys::file_time_type{};
    else
        return time;
}

void print_file_info(std::ostream& stream, fsys::directory_entry const& entry)
{
    std::format_to(std::ostreambuf_iterator<char>(stream),
        "{0:>16} {1:%F %T} {2}",
        get_file_size(entry),
        get_last_write_time(entry),
        entry.path().generic_string());
    print_file_type(stream, entry);
    stream << '\n';
    if (not entry.is_symlink() and entry.is_directory())
    {
        for (auto&& entry : fsys::directory_iterator{entry.path()})
            print_file_info(stream, entry);
    }
}

int main(int, char** argv)
{
    if (argv[1] == nullptr or std::string_view(argv[1]) == "--help")
    {
        std::cerr << "usage: " << argv[0] << " FILENAME\n";
        return EXIT_FAILURE;
    }
    while (*++argv != nullptr)
    {
        fsys::path path{ *argv };
        std::error_code ec;
        fsys::directory_entry entry{ path, ec };
        if (ec)
            std::cout << *argv << ": " << ec.message() << '\n';
        else
            print_file_info(std::cout, entry);
    }
}

Listing 66-4.Recursing into Directories

下一个主题深入到 C++ 的位和字节。