除了读写文件,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的特化,通常是char或wchar_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++ 的位和字节。