目录、文件处理是脚本语言如Shell、Perl、Python所擅长的领域,C++语言缺乏对操作系统中文件的查询和操作能力,因此C++程序员经常需要再掌握另外一门脚本语言以方便自己的工作,这增加了学习的成本。
filesystem库是一个可移植的文件系统操作库,已被收入C++17标准。它在底层做了大量的工作,使用POSIX标准表示文件系统的路径,接口很类似标准库的容器和迭代器,使C++具有了类似脚本语言的功能,可以跨平台操作目录、文件,写出通用的脚本程序。
filesystem库需要system库支持,因此必须先编译system库,在jamfile里指定lib的语句是:
lib boost_filesystem : boost_system : <name>boost_filesystem;
filesystem位于名字空间boost::filesystem,需要包含头文件<boost/filesystem.hpp>。
10.4.1 类摘要
filesystem库的核心类是path,它屏蔽了不同文件系统的差异,使用可移植的POSIX语法提供了通用的目录、路径表示,并且支持POSIX的符号链接概念。<br /> path的接口比较复杂,类摘要如下(略去了一些右值引用、编码转换相关的接口):
class path // 路径表示类
{
public:
typedef char_or_wchar_t value_type; // 路径的字符类型
typedef std::basic_string<value_type> string_type; // 路径使用的字符串类型
constexpr value_type preferred_separator; // 路径分隔符
path(); // 各种构造函数
path(const path& p);
path(Source const& source);
path(InputIterator begin, InputIterator end);
~path(); // 析构函数
path& operator=(const path& p); // 赋值操作
path& operator=(Source const& source);
path& assign(Source const& source)
path& assign(InputIterator begin, InputIterator end);
path& operator/=(const path& p); // 重载/=
path& operator/=(Source const& source); // 追加路径
path& append(Source const& source);
path& append(InputIterator begin, InputIterator end);
path& operator+=(const path& x); // 重载+=
path& operator+=(const string_type& x); // 连接路径
path& operator+=(Source const& x);
path& concat(InputIterator begin, InputIterator end);
void clear(); // 清空路径表示
path& remove_filename(); // 删除文件名
path& replace_extension(); // 更改扩展名
void swap(path& rhs); // 交换操作
const string_type& native() const; // 本地路径表示
const value_type* c_str() const; // 转换为C字符串
const string string() const; // 转换为字符串
const wstring wstring() const; // 转换为宽字符串
int compare(const path& p) const; // 比较路径
int compare(const std::string& s) const;
int compare(const value_type* s) const;
path root_name() const; // 根名称
path root_directory() const; // 根目录
path root_path() const; // 根路径
path relative_path() const; // 相对路径
path parent_path() const; // 父路径
path filename() const; // 文件名
path stem() const; // 全路径名
path extension() const; // 扩展名
bool empty() const; // 是否空路径
bool has_root_name() const; // 是否有根名称
bool has_root_directory() const; // 是否有根目录
bool has_root_path() const; // 是否有根路径
bool has_relative_path() const; // 是否有相对路径
bool has_parent_path() const; // 是否有父路径
bool has_filename() const; // 是否有文件名
bool has_stem() const; // 是否有全路径名
bool has_extension() const; // 是否有扩展名
bool is_absolute() const; // 是否是绝对路径
bool is_relative() const; // 是否是相对路径
iterator begin() const; // 迭代路径
iterator end() const;
};
ostream& operator<<( ostream& os, const path& p ); // 流输出操作
path operator/ (const path& lhs, const path& rhs); // 连接两个路径
bool operator==(const path& lhs, const path& rhs); // 比较操作符
... // 其他比较操作符重载
从上面的类摘要可以看到,path提供了丰富的成员函数,基本涵盖了文件系统的大部分操作,并且还支持比较操作、流输出操作,以字符串的形式打印出内部保存的路径表示,为我们提供了一个方便的调试手段。
10.4.2 路径表示
path的构造函数可以接受C字符串和string,也可以是一个指定首末迭代器字符串序列区间,路径的分隔符由类内部的静态常量preferred_separator定义,UNIX是斜杠(/),Windows是反斜杠(\)。<br /> path使用标准的POSIX语法提供可移植的路径表示,使用斜杠(/)来分隔文件名和目录名,点号(.)表示当前目录,双点号(..)表示上一级目录。例如:
path p1("./a_dir");
path p2("/usr/local/lib");
path也支持操作系统的原生路径表示,在Windows下使用盘符,分隔符使用反斜杠(\):
path p3("c:\\tmp\\test.text");
path p4("d:/boost/boost/filesystem/");
因为反斜杠被C++定义为转义符,在字符串中使用反斜杠表示路径的时候必须连续写“\\”才能被识别成一个“\”(或者使用C++的原始字符串R"(…)"的形式),因此这种方式通常没有UNIX风格的斜杠方式方便。<br /> 空的构造函数创建一个空路径对象,不表示任何路径,成员函数empty()可以判断路径是否为空:
path p5; // 默认构造为空路径
assert(p5.empty());
path的构造函数没有被声明为explicit,因此字符串可以被隐式转换为path对象,这在编写操作文件系统的代码时非常方便,可以不用创建一个临时的path对象。<br /> path重载了operator/=,可以像使用普通路径一样用“/”来追加路径,成员函数append()也可以追加一个字符串序列。例如:
char str[] = "the path is (/root)."; // 一个普通字符串数组
path p(str + 13, str + 14); // 区间方式,取字符串中的"/"
assert(!p.empty()); // 路径非空
p /= "etc"; // 使用operator/=追加路径
string filename = "xinetd.conf";
p.append(filename.begin(), filename.end()); // 追加字符序列
cout << p << endl; // p = /etc/xinetd.conf
operator+=和concat()的作用与operator/=类似,但它仅连接字符串,不会添加路径分隔符:
path p(str + 13, str + 15); // 区间方式,取"/r"
p += "etc"; // p = /retc
string filename = "xinetd.conf";
p.concat(filename.begin(), filename.end()); // 追加字符序列
cout << p << endl; // p = /retcxinetd.conf
自由函数system_complete()可以返回路径在当前文件系统中的完整路径(也就是通常所说的绝对路径),例如
cout << system_complete(p) << endl;
// 在Linux 下的输出是: /etc/xinetd.conf
需要注意:path 仅仅是用于表示路径,而并不关心路径中的文件或目录是否存在,路径也可能是个在当前文件系统中无效的名字,例如在Windows 下不允许文件名或目录名使用冒号、尖括号、星号、问号,等等,但path 并不禁止这种表示:
path p("/::/*/?/<>"); // 完全合法
为了提高程序在不同文件系统上的可移植性,filesystem库提供了一系列的文件名(或目录名)检查函数,可以根据系统的命名规则判断一个文件名字符串的有效性,从而尽可能地为程序员编写可移植的程序创造便利。<br /> 自由函数portable_posix_name()和windows_name()分别检测文件名字符串是否符合POSIX规范和Windows规范,保证名字可以移植到符合POSIX的UNIX操作系统和Windows操作系统上。<br /> POSIX规范只有一个很小的字符集用于文件名,包括大小写字母、点号、下画线和连字符,而Windows则范围要广一些,仅不允许<>?:|/\等少量字符。例如:
string fname("w+abc.xxx");
assert(!portable_posix_name(fname)); // posix 非法文件名
assert(windows_name(fname)); // Windows 合法文件名
函数portable_name()判断名字是否是一个可移植的文件名,相当于portable_posix_name()&&windows_name(),但名字不能以点号或者连字符开头,并允许表示当前目录的“.”和父目录的“..”。它保证文件名可以移植到所有现代操作系统和一些旧有的操作系统上。<br /> portable_directory_name()的判断规则更严格,它包含了portable_name(),并且要求名字中不能出现点号,目录名可以移植到OpenVMS操作系统上。<br /> portable_file_name()类似portable_directory_name(),提供更可移植的文件名,它要求文件名中最多有一个点号,且后缀名不能超过3个字符。<br /> 函数native()判断文件名是否符合本地文件系统命名规则,在Windows下它等同于windows_name(),而在其他操作系统下则只是简单地判断文件名不是空格且不包含斜杠。<br />这些文件名检查函数的用法如下:
assert(!portable_name("w+()abc.txt") && !portable_name("./abc"));
assert(!portable_directory_name("a.txt") && portable_directory_name("abc"));
assert( portable_file_name("a.bc") && !portable_file_name("y.conf"));
10.4.4 路径处理
path类提供了丰富的函数用于处理路径,可以获取文件名、目录名、判断文件属性等,本节不能全部介绍,只能叙述其中最常用的一部分。<br /> path的成员函数string()、native()以字符串的形式返回标准格式的路径表示,而parent_path()、filename()、stem()和extension()分别返回路径中的父路径、全文件名、不含扩展名的文件名和扩展名的path对象:
path p("/usr/local/include/xxx.hpp");
cout << p.string() << endl; // 返回的是std::string
cout << p.parent_path() << endl; // 返回的是path 类型
cout << p.stem() << endl;
cout << p.filename() << endl;
cout << p.extension() << endl;
/*
/usr/local/include/xxx.hpp
/usr/local/include/
xxx
xxx.hpp
.hpp
*/
成员函数is_absolute()用于检测path是否是一个绝对(完整)路径,这需要依据具体的文件系统的表示,例如下面代码的第一个断言在UNIX下是成立的,而在Windows下则不成立,因为Windows系统的完整路径需要包括盘符:
assert(p.is_absolute()); // Windows 断言不成立
assert(system_complete(p).is_absolute()); // 总是完整路径
root_name()、root_directory()、root_path()这三个成员函数用于处理根目录,如果path中含有根,那么它们分别返回根的名字、根目录和根路径,注意它们的返回结果也是path对象:
cout << p.root_name() << endl;
cout << p.root_directory() << endl;
cout << p.root_path() << endl;
这段代码在UNIX下将输出一个空字符串和两个斜杠(/),因为UNIX下的根没有名字。而在Windows下,如果path是如“c:/xxx/yyy”的形式,输出将会是:“c:”、“/”和“c:/”<br /> 成员函数relative_path()返回path的相对路径,相当于去掉了root_path()。<br /> 根路径和相对路径的这四个函数都有对应的has_xxx()的形式,用来判断是否存在对应的路径,同样,has_filename()和has_parent_path()用于判断路径是否有文件名或者父路径。例如:
assert(!p.has_root_name());
assert( p.has_root_path());
assert( p.has_parent_path());
之前讨论的成员函数都不会改变path的值,接下来的两个函数能够原地修改path:
- remove_filename()函数可以删除路径中最后的文件名,把path变为纯路径表示;
replace_extension()可以变更文件的扩展名。
例如:
cout << p.replace_extension() << endl; cout << p.replace_extension("hxx") << endl; cout << p.remove_filename() << endl; /* /usr/local/include/xxx /usr/local/include/xxx.hxx /usr/local/include */我们也可以对两个path对象执行比较操作,它使用成员函数compare()基于字典序并且大小写敏感比较路径字符串,提供operator==、operator!=、operator<等操作符: ```cpp path p1(“/test/1.cpp”); path p2(“/TEST/1.cpp”); path p3(“/abc/1.cpp”);
assert(p1 != p2); assert(p2 < p3);
path还提供常量迭代器begin()和end(),可以迭代路径中的字符串,解引用迭代器返回的是constpath&,例如:
```cpp
path p = "/boost/tools/libs";
BOOST_FOREACH(auto& x, p) { // 使用foreach 算法,也可以用for
cout << "["<< x << "]"; // 输出路径字符串
}
/*
[/][boost][tools][libs]
*/
如果path类提供的这些操作还不足以满足要求,那么我们可以使用Boost中的字符串处理库string_algo或xpressive(5.4、5.5节),从path的字符串表达中提取字符串做处理,然后再赋值给path对象。
10.4.5 异常处理
filesystem库使用异常filesystem_error来处理文件操作时发生的错误,它是system库中system_error(10.1.5节)的子类。<br /> filesystem_error的类摘要如下:
class filesystem_error : public system_error
{
public:
filesystem_error(); // 各种构造函数
filesystem_error(const filesystem_error&);
filesystem_error(const std::string& what_arg, system::error_code ec);
filesystem_error(const std::string& what_arg,
const path& p1, system::error_code ec);
filesystem_error(const std::string& what_arg, const path& p1,
const path& p2, system::error_code ec);
const path& path1() const; // 获取路径对象
const path& path2() const;
const char* what() const; // 错误信息
};
filesystem_error的接口很简单,由于继承自system_error,因此可以使用code()获得error_code值,它还可以使用path1()和path2()来获取发生异常时的路径对象。 <br />例如下面的代码检查文件的大小,但文件不存在,将抛出异常:
path p("/test.txt"); // 一个不存在的文件
try{
file_size(p); // 检查文件的大小
}
catch(filesystem_error& e) { // 捕获异常
cout << e.path1() << endl;
cout << e.what() << endl;
}
由于文件系统位于程序之外,是不可控且全局共享的,所以访问目录或文件随时都有可能发生异常,例如文件已经删除、文件已经存在等。<br /> 读者需要注意:本书的示范代码都没有使用try-catch,只是出于使代码清晰易读的目的。为了程序的健壮性,应总使用try-catch块来保护文件访问代码。
10.4.6 文件状态
filesystem库提供一个文件状态类file_status及一组相关函数,用于检查文件的属性(如是否存在、是否是目录、是否是符号链接等)和访问权限(用户读写、组读写等)。<br /> file_status的类摘要如下:
class file_status
{
public:
file_status(); // 构造函数
explicit file_status(file_type ft, perms prms = perms_not_known);
file_type type() const; // 文件类型
void type( file_type v ); // 设置文件类型
perms permissions() const; // 访问权限
void permissions(perms prms); // 设置访问权限
};
file_status的成员函数type()和permissions()可以获得文件的状态信息,它们都是枚举类型。<br /> 文件的类型file_type取值有:
- status_error : 获取文件类型出错;
- file_not_found : 文件不存在;
- status_unknown : 文件存在但状态未知;
- regular_file : 是一个普通文件;
- directory_file : 是一个目录;
- symlink_file : 是一个链接文件;
- block_file : 是一个块设备文件;
- character_file : 是一个字符设备文件;
- fifo_file : 是一个管道设备文件;
- socket_file : 是一个 socket 设备文件;
type_unknown : 文件的类型未知。
访问权限perms枚举模仿了POSIX的文件权限,使用与S_IRUSR(0400)、S_IWUSR(0200)值对应的owner_read、owner_write等值,可以用位运算来操作。
通常我们不会直接使用file_status类(就像是std::typeinfo),而是用相关函数操作file_status对象:status()/symlink_status()测试路径p 的状态,如果路径p 不能被解析则会抛出异常filesystem_error
status_known()检查文件状态s,返回s.type != status_error。
下面的断言在Linux环境下均成立(使用了path的隐式转换): ```cpp assert(status(“/dev/null”).type() == character_file); assert(status(“/bin”).type() == directory_file); assert(status(“/bin/sh”).type() == regular_file);
// 取访问权限,使用位操作验证权限值 assert((status(“/bin/sh”).permissions() & owner_exe) == owner_exe);
filesystem库还提供了一些便利的谓词函数is_xxx(),可以简化对文件状态的判断,例如:
```cpp
path root = "/usr/local/include/boost";
assert( is_directory(root));
assert(!exists(root/"nofile"));
assert(!is_symlink(root/"version.hpp"));
assert(!is_other(root/"version.hpp"));
assert( is_regular_file(root/"version.hpp"));
assert(!is_empty(root/"version.hpp"));
大部分谓词函数都可以望文知意,比较特别的是is_other()和is_empty()。当文件存在且不是普通文件、目录或链接文件时is_other()返回true。如果path对象是目录,那么当目录里无文件时is_empty()返回true,如果path对象是文件,那么文件长度为0,is_empty()返回true。<br />函数equivalent()可以比较两个目录实体是否是同一个,但如果( !exists(p1) && !exists(p2) ) ||(is_other(p1) && is_other(p2) )则无法比较,会抛出异常。
10.4.7 文件属性
受可移植的限制,很多文件属性不是各平台共通的,因此filesystem库仅提供了少量的文件属性操作:
- 函数initial_path()返回程序启动时(进入main()函数)的路径;
- 函数current_path()返回当前路径。它和initial_path()返回的都是一个完整路径(绝对路径);
- 函数file_size()以字节为单位返回文件的大小;
函数last_write_time()返回文件的最后修改时间,是一个std::time_t。
last_write_time()还可以额外接受一个time_t参数,修改文件的最后修改时间,就像是使用UNIX的touch命令。
这些函数都要求操作的文件必须存在,否则会抛出异常,file_size()还要求文件必须是个普通文件(is_regular_file(name)==true)。
示范这几个函数用法的代码如下: ```cpp cout << initial_path() << endl; // 输出初始路径 cout << current_path() << endl; // 输出当前路径
path p(“./test.txt”); // 访问一个文件 cout << file_size(p) << endl;
time_t t = last_write_time(p); // 获取修改时间 last_write_time(p, time(0)); // 更新修改时间
此外,函数space()可以返回一个space_info结构,它表明了该路径下的磁盘空间分配情况,space_info结构的定义如下:
```cpp
struct space_info {
uintmax_t capacity;
uintmax_t free;
uintmax_t available;
};
space()函数可以这样使用(类似df命令):
space_info si = space("/home/chrono");
cout << si.capacity / giga::num<< endl; // 使用ratio 库的giga 单位
cout << si.available / giga::num<< endl;
cout << si.free / giga::num<< endl;
10.4.8 文件操作
filesystem库基于path的路径表示提供了基本的文件操作函数,如创建目录(create_directory)、文件改名(rename)、文件删除(remove)、文件拷贝(copy_file)、创建符号链接(create_symlink),等等,这些函数的名字都很容易理解。<br /> 示范文件操作用法的代码如下:
namespace fs = boost::filesystem; // 名字空间别名
path ptest = "./test";
if (exists(ptest)) { // 检查路径是否存在
if (fs::is_empty(ptest)) { // 注意名字空间限定
remove(ptest); // remove只能删除空目录或文件
}
else {
remove_all(ptest); // remove_all 可以递归删除
}
}
assert(!exists(ptest)); // 该目录已经被删除
create_directory(ptest); // 创建一个目录
copy_file("/usr/local/include/boost/version.hpp", ptest / "a.txt");
assert(exists(ptest / "a.txt"));
rename(ptest / "a.txt", ptest / "b.txt"); // 改名
assert(exists(ptest / "b.txt"));
// 使用create_directories 可以一次创建多级目录
create_directories(ptest / "sub_dir1" / "sub_dir1");
请读者注意:因为在C++标准的type_traits库中有一个同名的元函数is_empty,所以我们在这段代码中为is_empty()函数加上了名字空间限定,避免名字冲突。
10.4.9 迭代目录
filesystem库使用directory_iterator提供了迭代一个目录下的所有文件的功能,它基于boost.iterator库的iterator_facade,类摘要如下:
class directory_iterator : public boost::iterator_facade<
directory_iterator,
directory_entry, // 解引用返回类型
boost::single_pass_traversal_tag >
{
public:
directory_iterator(){}
directory_iterator(const directory_iterator&);
explicit directory_iterator(const path& p);
~directory_iterator();
directory_iterator& operator=(const directory_iterator&);
directory_iterator& operator++();
};
directory_iterator用法有些类似string_algo库的find_iterator、split_iterator(5.4.12节)或xpressive库的regex_token_iterator(5.5.8节),空的构造函数生成一个逾尾end迭代器,传入path对象构造将开始一个迭代操作,反复调用operator++即可遍历目录下的所有文件。例如:
directory_iterator end;
for (directory_iterator pos("/usr/local/lib/");pos != end; ++pos) {
cout << *pos << endl;
}
我们也可以定义一个std::pair的区间,使用foreach算法简化迭代处理:
// 定义一个迭代器的区间
typedef std::pair<directory_iterator, directory_iterator> dir_range;
dir_range dr(directory_iterator("/usr/local/lib/"), // 迭代起点
directory_iterator()); // 迭代终点
BOOST_FOREACH(auto& x, dr) { // 迭代区间
cout << x << endl;
}
需要注意的是directory_iterator迭代器返回的对象并不是path,而是一个directory_entry对象,但由于directory_entry类定义了一个到path的类型转换函数,因此可以在需要path的语境中隐式转换到path。<br /> directory_entry的类摘要如下:
class directory_entry
{
public:
const path& path() const; // 返回path 对象
file_status status() const; // 获取文件状态
file_status symlink_status() const;
};
directory_entry可以用path()方法返回路径,status()返回路径的状态,它还重载了各种比较操作符,可以执行比较操作。<br /> directory_iterator只能迭代本层目录,不支持深度遍历目录,可以使用递归来实现这个功能,并不是很难:
void recursive_dir(const path& dir) // 递归遍历目录
{
directory_iterator end; // 结束迭代器调用
for (directory_iterator pos(dir);pos != end; ++pos) {
if (is_directory(*pos)) {
recursive_dir(*pos); // 是目录则递归遍历
} else {
cout << *pos << endl; // 不是目录则输出路径
}
}
}
由于递归遍历文件系统目录的功能非常有用也很有必要,因此filesystem库专门提供了另外一个类recursive_directory_iterator,它遍历目录的效率要比递归的directory_iterator高很多。<br /> recursive_directory_iterator的基本用法与directory_iterator相同,只是它可以递归遍历目录结构。recursive_directory_iterator的类摘要如下:
class recursive_directory_iterator // 省略构造、析构函数以及迭代器通用操作
{
public:
int level() const; // 目录深度
void pop(); // 退出当前目录的遍历
void no_push(); // 不遍历本目录
private:
int m_level; // 目录深度成员变量
};
因为它的名字实在是太长了,下面为了叙述方便将它简称为rd_iterator,相当于有:
typedef recursive_directory_iterator rd_iterator;
rd_iterator具有目录迭代器的基本功能,operator++会令它返回目录中的下一个文件。它的特别之处是可以深度搜索目录,迭代当前目录及子目录下的所有文件。<br /> 成员函数level()返回当前的目录深度m_level,当rd_iterator构造时(未开始遍历)m_level==0,每深入一层子目录则m_level增加,退出时减少。成员函数pop()用于退出当前目录层次的遍历,同时--m_level。当迭代到一个目录时,no_push()可以让目录不参与遍历,使rd_iterator的行为等价于directory_iterator。<br /> 使用rd_iterator,深度遍历目录的操作会变得非常简单:
rd_iterator end;
for (rd_iterator pos("/usr/local/lib/");pos != end; ++pos) {
cout << "level" << pos.level() << ":" <<*pos << endl;
}
下面的代码使用no_push()令rd_iterator的行为等价于directory_iterator:
rd_iterator end;
for (rd_iterator pos("/usr/local/lib/");pos != end; ++pos) {
if (is_directory(*pos)) {
pos.no_push(); // 使用no_push(),不深度遍历
}
cout <<*pos << endl;
}
恰当地使用level()和no_push(),还可以实现指定深度的目录遍历,读者可以自行试验一下
10.4.10 实例1 查找文件
了解了filesystem库迭代目录的功能后,作为示范,本节来实现查找文件的功能,我们将查找文件的函数命名为find_file()。<br /> 因为查找文件很有可能找不到,因此我们选择使用optional<path>作为返回值类型。函数中首先判断目录的状态,如果不存在或者非目录则直接返回空的optional对象,表示查找失败。然后使用rd_iterator递归迭代目录内容,如果是文件则比较path对象的文件名,相等则查找成功。
#include <boost/filesystem.hpp>
#include <boost/optional.hpp>
#include <iostream>
#include <list>
using namespace std;
using namespace boost;
using namespace boost::filesystem;
typedef recursive_directory_iterator rd_iterator;
optional<list<path>> find_file(path dir, string filename)
{
// 返回值类型定义
typedef optional<list<path>> result_type;
// 检查目录的有效性
if (!exists(dir) || !is_directory(dir)) {
return result_type();
}
// 递归迭代器
rd_iterator end;
list<path> lstPath = list<path>();
for (rd_iterator pos(dir); pos != end; ++pos) {
// 不是目录、文件名相等
if ((!is_directory(*pos)) && (pos->path().filename() == filename)) {
lstPath.push_back(pos->path());
}
}
return result_type(lstPath);
}
int main()
{
// 如果是Qt环境,路径有中文的时候会出现编码问题
path searchPath(""); // 查找的路径
string findFileName = ""; // 文件名
optional<list<path>> r = find_file(searchPath, findFileName);
list<path> lstPath = r.value();
// 不存在提示并return
if (lstPath.size() == 0) {
cout << "file not found." << endl;
return -1;
}
for (path p : lstPath) {
cout << p << endl;
}
return 0;
}
find_file()可能并不是很高效,但作为通常的使用已经足够了:
auto r = find_file("/usr/local/include/boost", "version.hpp");
if (r) {
cout << *r << endl;
} else {
cout << "file not found." << endl;
}
find_file()有个缺陷,不支持通配符模糊查找,如果想要进一步增强它的话可以使用正则表达式。
10.4.11 实例2 模糊文件查找
使用正则表达式处理字符串的强大功能,我们可以很容易地实现完全的模糊文件查找。
作为示范,我们只支持通配符*,大小写敏感并且不处理点号以外的正则表达式其他特殊符号(如括号和竖线),读者可以自己扩展增强它的功能(这很容易)。
下面是模糊查找的几个实现要点:
- 文件名中用于分隔主名与扩展名的点号必须转义,因为点号在正则表达式中是一个特殊的字符;
- 通配符应该转换为正则表达式的.,以表示任意多的字符;
- 在判断文件名是否查找到时我们应该使用正则表达式而不是简单的判断相等;
函数的返回值不能再使用optional
,因为模糊查找可能会返回多个结果,所以应该使用vector 。 find_files()的实现代码如下: ```cpp
include
include
include
include
include
using namespace std; using namespace boost; using namespace boost::filesystem; using namespace boost::xpressive;
typedef recursive_directory_iterator rd_iterator;
vector
int main() { auto v = find_files(“C:/Users/14759/Desktop/test1”, “*.lib”); cout << v.size() << endl; for(path &p : v) { cout << p << endl; } return 0; }
代码中在处理正则表达式时我们使用了工厂类sregex_compiler,并把它声明为函数内部的静态变量:
```cpp
static xpressive::sregex_compiler rc;
因为sregex_compiler本身是一个flyweight工厂,它能够保证创建唯一的正则表达式对象,可以避免重入find_files()反复创建正则表达式对象的代价。如果我们不使用rd_iterator而是使用directory_iterator的递归形式就更能显现出它的好处:sregex_compiler和正则表达式对象在反复的递归调用中都仅会被创建一次,节约大量的内存和时间。<br /> 为了仅使用一个正则表达式对象,我们需要使用sregex_compiler的flyweight用法,使用filename作为享元的索引键值,sregex的regex_id()函数来确定该享元是否已经被创建。如果已经被创建则直接使用,如果未被创建则使用string_algo库的replace_all_copy()把文件名中的“.”和“*”分别替换为“\.”和“.*”,然后编译为正则表达式对象,生成一个享元。<br /> find_files()的查找目录过程与find_file()基本相同,文件名的比较改为使用regex_match(),如果匹配成功则用push_back()加入到vector中。<br />find_files()的用法如下:
auto v = find_files("/usr/local/include/boost/timer", "*.hpp");
cout << v.size() << endl;
for(path &p : v) {
cout << p << endl;
}
10.4.12 实例3 拷贝目录
find_files()是一个功能强大的函数,利用它可以做很多事情,比如为filesystem库再增加一个copy_files()函数,它对应于remove_all()。<br /> copy_files()函数利用find_files()的功能,首先查找源目录里的所有文件(使用通配符*),把它们加入到一个vector<path>。然后遍历这个vector,把路径拆分成父路径和子路径两部分,拼接并创建目标路径,最后调用copy_file()函数拷贝文件。<br /> copy_files()还使用了progress_display(2.4节),为用户提供了一个友好的进度显示。<br /> 完整的实现代码如下:
// disable pragma warning
#define BOOST_ALLOW_DEPRECATED_HEADERS
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>
#include <boost/optional.hpp>
#include <boost/progress.hpp>
#include <boost/xpressive/xpressive_dynamic.hpp>
#include <iostream>
#include <list>
using namespace boost::xpressive;
using namespace std;
using namespace boost;
using namespace boost::filesystem;
typedef recursive_directory_iterator rd_iterator;
vector<path> find_files(const path& dir, const string& filename)
{
static xpressive::sregex_compiler rc; // 正则表达式工厂
if (!rc[filename].regex_id()) {
string str = replace_all_copy(
replace_all_copy(filename, ".", "\\."),
"*", ".*"); // 处理文件名
rc[filename] = rc.compile(str); // 创建正则表达式
}
typedef vector<path> result_type; // 返回值类型定义
result_type v;
if (!exists(dir) || !is_directory(dir)) // 目录检查
{
return v;
}
rd_iterator end; // 递归迭代器逾尾位置
for (rd_iterator pos(dir); pos != end; ++pos) {
if (!is_directory(*pos) && regex_match(pos->path().filename().string(), rc[filename])) {
v.push_back(pos->path()); // 找到,加入vector
}
}
return v; // 返回查找的结果
}
size_t copy_files(const path& from_dir, const path& to_dir, const string& filename = "*")
{
if (!is_directory(from_dir)) { // 源必须是个目录
cout << "args is not a dir." << endl;
return 0;
}
cout << "prepare for copy, please wait..." << endl;
auto v = find_files(from_dir, filename); // 查找源的所有文件
if (v.empty()) { // 空目录则不拷贝
cout << "0 file copied." << endl;
return 0;
}
cout << "now begin copy files ..." << endl;
path tmp;
progress_display pd(v.size()); // 进度显示
for (auto& p : v) { // 变量容器
// 拆分基本路径与目标路径
tmp = to_dir / p.string().substr(from_dir.string().length());
if (!exists(tmp.parent_path())) { // 创建子目录
create_directories(tmp.parent_path());
}
copy_file(p, tmp); // 拷贝文件
++pd; // 更新进度
}
cout << v.size() << " file copied." << endl;
return v.size(); // 完成拷贝
}
int main()
{
copy_files("/usr/local/include/boost/timer", "./t");
return 0;
}
copy_files()的使用方法如下,非常简单易用:
copy_files("/usr/local/include/boost/timer", "./t");
copy_files()不仅能够拷贝目录下的所有文件,也可以过滤只符合通配符的文件,这需要向copy_files()传递第三个参数filename,例如要拷贝所有的文本文件可以使用“*.txt”。<br /> 不过,这里实现的copy_files()也有小小的缺陷,它不能够拷贝空目录,这是因为find_files()在查找时不考虑目录匹配。但这个缺陷很容易弥补,读者可以把对copy_files()的改进作为一个练习,实践一下filesystem库的使用。
10.4.13 文件流操作
filesystem库提供了大量的文件系统操作方法,可以很方便地操作文件或目录,但它使用的是path对象,而C++标准库中的文件流类ifstream/ofstream/fstream只支持char*或者std::string打开文件,因此使用起来很不方便,我们必须调用path对象的c_str()或string(),像这样:
path p("./INSTALL");
std::ifstream ifs(p.c_str());
在C++17标准里文件流已经增加了对path的支持,如果编译器支持C++17标准,那么filesystem库将会和标准库无缝对接,使C++程序员获得完整的文件处理能力。<br /> 不过为了让尚未实现这个特性的编译器也能够享受这个便利,filesystem库在额外的头文件<boost/filesystem/fstream.hpp>中提供了在名字空间boost::filesystem下的同名文件流类,它们可以如标准文件流一样使用,而且支持path对象。<br /> boost::filesystem的文件流的使用方法与标准文件流相同,例如下面的代码使用path对象打开了一个文本文件,并把它打印到标准输出上:
namespace newfs = boost::filesystem;
path p("./INSTALL");
newfs::ifstream ifs(p);
assert(ifs.is_open());
cout << ifs.rdbuf();
这段代码中我们使用了名字空间别名newfs,用来限定使用的是filesystem库的文件流类。<br /> 这种做法带来了一个好处,如果将来升级到支持C++17标准编译器,那么只需要把newfs指向std名字空间,原有的所有代码都不需要变动。
