blob: 2c2871a08b4a0a7cbda9af378d9e3524765d5b0a [file] [log] [blame] [edit]
// Copyright (c) 2017 Gilad Naaman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Recursively find files in a directory using globs.
//!
//! Features include
//! - [`gitignore`'s extended glob syntax][gitignore]
//! - Control over symlink behavior
//! - Control depth walked
//! - Control order results are returned
//!
//! [gitignore]: https://git-scm.com/docs/gitignore#_pattern_format
//!
//! # Examples
//!
//! ## Finding image files in the current directory.
//!
//! ```rust
//! extern crate globwalk;
//! # include!("doctests.rs");
//!
//! use std::fs;
//! # fn run() -> Result<(), Box<::std::error::Error>> {
//! # let temp_dir = create_files(&["cow.jog", "cat.gif"])?;
//! # ::std::env::set_current_dir(&temp_dir)?;
//!
//! for img in globwalk::glob("*.{png,jpg,gif}")? {
//! if let Ok(img) = img {
//! fs::remove_file(img.path())?;
//! }
//! }
//! # Ok(()) }
//! # fn main() { run().unwrap() }
//! ```
//!
//! ## Advanced Globbing ###
//!
//! By using one of the constructors of `globwalk::GlobWalker`, it is possible to alter the
//! base-directory or add multiple patterns.
//!
//! ```rust
//! extern crate globwalk;
//! # include!("doctests.rs");
//!
//! use std::fs;
//!
//! # fn run() -> Result<(), Box<::std::error::Error>> {
//! # let temp_dir = create_files(&["cow.jog", "cat.gif"])?;
//! # let BASE_DIR = &temp_dir;
//! let walker = globwalk::GlobWalkerBuilder::from_patterns(
//! BASE_DIR,
//! &["*.{png,jpg,gif}", "!Pictures/*"],
//! )
//! .max_depth(4)
//! .follow_links(true)
//! .build()?
//! .into_iter()
//! .filter_map(Result::ok);
//!
//! for img in walker {
//! fs::remove_file(img.path())?;
//! }
//! # Ok(()) }
//! # fn main() { run().unwrap() }
//! ```
// Our doctests need main to compile; AFAICT this is a false positive generated by clippy
#![allow(clippy::needless_doctest_main)]
#![warn(missing_docs)]
extern crate ignore;
extern crate walkdir;
extern crate bitflags;
#[cfg(test)]
extern crate tempdir;
use ignore::overrides::{Override, OverrideBuilder};
use ignore::Match;
use std::cmp::Ordering;
use std::path::Path;
use std::path::PathBuf;
use walkdir::WalkDir;
/// Error from parsing globs.
#[derive(Debug)]
pub struct GlobError(ignore::Error);
/// Error from iterating on files.
pub type WalkError = walkdir::Error;
/// A directory entry.
///
/// This is the type of value that is yielded from the iterators defined in this crate.
pub type DirEntry = walkdir::DirEntry;
impl From<std::io::Error> for GlobError {
fn from(e: std::io::Error) -> Self {
GlobError(e.into())
}
}
impl From<GlobError> for std::io::Error {
fn from(e: GlobError) -> Self {
if let ignore::Error::Io(e) = e.0 {
e
} else {
std::io::ErrorKind::Other.into()
}
}
}
impl std::fmt::Display for GlobError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
self.0.fmt(f)
}
}
impl std::error::Error for GlobError {
fn description(&self) -> &str {
self.0.description()
}
}
bitflags::bitflags! {
/// Possible file type filters.
/// Constants can be OR'd to filter for several types at a time.
///
/// Note that not all files are represented in this enum.
/// For example, a char-device is neither a file, a directory, nor a symlink.
pub struct FileType: u32 {
#[allow(missing_docs)] const FILE = 0b001;
#[allow(missing_docs)] const DIR = 0b010;
#[allow(missing_docs)] const SYMLINK = 0b100;
}
}
/// An iterator for recursively yielding glob matches.
///
/// The order of elements yielded by this iterator is unspecified.
pub struct GlobWalkerBuilder {
root: PathBuf,
patterns: Vec<String>,
walker: WalkDir,
case_insensitive: bool,
file_type: Option<FileType>,
}
impl GlobWalkerBuilder {
/// Construct a new `GlobWalker` with a glob pattern.
///
/// When iterated, the `base` directory will be recursively searched for paths
/// matching `pattern`.
pub fn new<P, S>(base: P, pattern: S) -> Self
where
P: AsRef<Path>,
S: AsRef<str>,
{
GlobWalkerBuilder::from_patterns(base, &[pattern])
}
/// Construct a new `GlobWalker` from a list of patterns.
///
/// When iterated, the `base` directory will be recursively searched for paths
/// matching `patterns`.
pub fn from_patterns<P, S>(base: P, patterns: &[S]) -> Self
where
P: AsRef<Path>,
S: AsRef<str>,
{
fn normalize_pattern<S: AsRef<str>>(pattern: S) -> String {
// Either `ignore` or our iteration code treat a single asterisk pretty strangely, matching everything, even
// paths that are inside a sub-direcrtory.
if pattern.as_ref() == "*" {
String::from("/*")
} else {
pattern.as_ref().to_owned()
}
}
GlobWalkerBuilder {
root: base.as_ref().into(),
patterns: patterns.iter().map(normalize_pattern).collect::<_>(),
walker: WalkDir::new(base),
case_insensitive: false,
file_type: None,
}
}
/// Set the minimum depth of entries yielded by the iterator.
///
/// The smallest depth is `0` and always corresponds to the path given
/// to the `new` function on this type. Its direct descendents have depth
/// `1`, and their descendents have depth `2`, and so on.
pub fn min_depth(mut self, depth: usize) -> Self {
self.walker = self.walker.min_depth(depth);
self
}
/// Set the maximum depth of entries yield by the iterator.
///
/// The smallest depth is `0` and always corresponds to the path given
/// to the `new` function on this type. Its direct descendents have depth
/// `1`, and their descendents have depth `2`, and so on.
///
/// Note that this will not simply filter the entries of the iterator, but
/// it will actually avoid descending into directories when the depth is
/// exceeded.
pub fn max_depth(mut self, depth: usize) -> Self {
self.walker = self.walker.max_depth(depth);
self
}
/// Follow symbolic links. By default, this is disabled.
///
/// When `yes` is `true`, symbolic links are followed as if they were
/// normal directories and files. If a symbolic link is broken or is
/// involved in a loop, an error is yielded.
///
/// When enabled, the yielded [`DirEntry`] values represent the target of
/// the link while the path corresponds to the link. See the [`DirEntry`]
/// type for more details.
///
/// [`DirEntry`]: struct.DirEntry.html
pub fn follow_links(mut self, yes: bool) -> Self {
self.walker = self.walker.follow_links(yes);
self
}
/// Set the maximum number of simultaneously open file descriptors used
/// by the iterator.
///
/// `n` must be greater than or equal to `1`. If `n` is `0`, then it is set
/// to `1` automatically. If this is not set, then it defaults to some
/// reasonably low number.
///
/// This setting has no impact on the results yielded by the iterator
/// (even when `n` is `1`). Instead, this setting represents a trade off
/// between scarce resources (file descriptors) and memory. Namely, when
/// the maximum number of file descriptors is reached and a new directory
/// needs to be opened to continue iteration, then a previous directory
/// handle is closed and has its unyielded entries stored in memory. In
/// practice, this is a satisfying trade off because it scales with respect
/// to the *depth* of your file tree. Therefore, low values (even `1`) are
/// acceptable.
///
/// Note that this value does not impact the number of system calls made by
/// an exhausted iterator.
///
/// # Platform behavior
///
/// On Windows, if `follow_links` is enabled, then this limit is not
/// respected. In particular, the maximum number of file descriptors opened
/// is proportional to the depth of the directory tree traversed.
pub fn max_open(mut self, n: usize) -> Self {
self.walker = self.walker.max_open(n);
self
}
/// Set a function for sorting directory entries.
///
/// If a compare function is set, the resulting iterator will return all
/// paths in sorted order. The compare function will be called to compare
/// entries from the same directory.
pub fn sort_by<F>(mut self, cmp: F) -> Self
where
F: FnMut(&DirEntry, &DirEntry) -> Ordering + Send + Sync + 'static,
{
self.walker = self.walker.sort_by(cmp);
self
}
/// Yield a directory's contents before the directory itself. By default,
/// this is disabled.
///
/// When `yes` is `false` (as is the default), the directory is yielded
/// before its contents are read. This is useful when, e.g. you want to
/// skip processing of some directories.
///
/// When `yes` is `true`, the iterator yields the contents of a directory
/// before yielding the directory itself. This is useful when, e.g. you
/// want to recursively delete a directory.
pub fn contents_first(mut self, yes: bool) -> Self {
self.walker = self.walker.contents_first(yes);
self
}
/// Toggle whether the globs should be matched case insensitively or not.
///
/// This is disabled by default.
pub fn case_insensitive(mut self, yes: bool) -> Self {
self.case_insensitive = yes;
self
}
/// Toggle filtering by file type.
/// `FileType` can be an OR of several types.
///
/// Note that not all file-types can be whitelisted by this filter (e.g. char-devices, fifos, etc.)
pub fn file_type(mut self, file_type: FileType) -> Self {
self.file_type = Some(file_type);
self
}
/// Finalize and build a `GlobWalker` instance.
pub fn build(self) -> Result<GlobWalker, GlobError> {
let mut builder = OverrideBuilder::new(self.root);
builder
.case_insensitive(self.case_insensitive)
.map_err(GlobError)?;
for pattern in self.patterns {
builder.add(pattern.as_ref()).map_err(GlobError)?;
}
Ok(GlobWalker {
ignore: builder.build().map_err(GlobError)?,
walker: self.walker.into_iter(),
file_type_filter: self.file_type,
})
}
}
/// An iterator which emits glob-matched patterns.
///
/// An instance of this type must be constructed through `GlobWalker`,
/// which uses a builder-style pattern.
///
/// The order of the yielded paths is undefined, unless specified by the user
/// using `GlobWalker::sort_by`.
pub struct GlobWalker {
ignore: Override,
walker: walkdir::IntoIter,
file_type_filter: Option<FileType>,
}
impl Iterator for GlobWalker {
type Item = Result<DirEntry, WalkError>;
// Possible optimization - Do not descend into directory that will never be a match
fn next(&mut self) -> Option<Self::Item> {
let mut skip_dir = false;
// The outer loop allows us to avoid multiple mutable borrows on `self.walker` when
// we want to skip.
'skipper: loop {
if skip_dir {
self.walker.skip_current_dir();
}
// The inner loop just advances the iterator until a match is found.
for entry in &mut self.walker {
match entry {
Ok(e) => {
let is_dir = e.file_type().is_dir();
let file_type = if e.file_type().is_dir() {
Some(FileType::DIR)
} else if e.file_type().is_file() {
Some(FileType::FILE)
} else if e.file_type().is_symlink() {
Some(FileType::SYMLINK)
} else {
None
};
let file_type_matches = match (self.file_type_filter, file_type) {
(None, _) => true,
(Some(_), None) => false,
(Some(filter), Some(actual)) => filter.contains(actual),
};
// Strip the common base directory so that the matcher will be
// able to recognize the file name.
// `unwrap` here is safe, since walkdir returns the files with relation
// to the given base-dir.
let path = e
.path()
.strip_prefix(self.ignore.path())
.unwrap()
.to_owned();
// The path might be empty after stripping if the current base-directory is matched.
if let Some("") = path.to_str() {
continue 'skipper;
}
match self.ignore.matched(path, is_dir) {
Match::Whitelist(_) if file_type_matches => return Some(Ok(e)),
// If the directory is ignored, quit the iterator loop and
// skip-out of this directory.
Match::Ignore(_) if is_dir => {
skip_dir = true;
continue 'skipper;
}
_ => {}
}
}
Err(e) => {
return Some(Err(e));
}
}
}
break;
}
None
}
}
/// Construct a new `GlobWalkerBuilder` with a glob pattern.
///
/// When iterated, the current directory will be recursively searched for paths
/// matching `pattern`, unless the pattern specifies an absolute path.
pub fn glob_builder<S: AsRef<str>>(pattern: S) -> GlobWalkerBuilder {
// Check to see if the pattern starts with an absolute path
let path_pattern: PathBuf = pattern.as_ref().into();
if path_pattern.is_absolute() {
// If the pattern is an absolute path, split it into the longest base and a pattern.
let mut base = PathBuf::new();
let mut pattern = PathBuf::new();
let mut globbing = false;
// All `to_str().unwrap()` calls should be valid since the input is a string.
for c in path_pattern.components() {
let os = c.as_os_str().to_str().unwrap();
for c in &["*", "{", "}"][..] {
if os.contains(c) {
globbing = true;
break;
}
}
if globbing {
pattern.push(c);
} else {
base.push(c);
}
}
let pat = pattern.to_str().unwrap();
if cfg!(windows) {
GlobWalkerBuilder::new(base.to_str().unwrap(), pat.replace("\\", "/"))
} else {
GlobWalkerBuilder::new(base.to_str().unwrap(), pat)
}
} else {
// If the pattern is relative, start searching from the current directory.
GlobWalkerBuilder::new(".", pattern)
}
}
/// Construct a new `GlobWalker` with a glob pattern.
///
/// When iterated, the current directory will be recursively searched for paths
/// matching `pattern`, unless the pattern specifies an absolute path.
pub fn glob<S: AsRef<str>>(pattern: S) -> Result<GlobWalker, GlobError> {
glob_builder(pattern).build()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{create_dir_all, File};
use tempdir::TempDir;
fn touch(dir: &TempDir, names: &[&str]) {
for name in names {
let name = normalize_path_sep(name);
File::create(dir.path().join(name)).expect("Failed to create a test file");
}
}
fn normalize_path_sep<S: AsRef<str>>(s: S) -> String {
s.as_ref()
.replace("[/]", if cfg!(windows) { "\\" } else { "/" })
}
fn equate_to_expected(g: GlobWalker, mut expected: Vec<String>, dir_path: &Path) {
for matched_file in g.into_iter().filter_map(Result::ok) {
let path = matched_file
.path()
.strip_prefix(dir_path)
.unwrap()
.to_str()
.unwrap();
let path = normalize_path_sep(path);
let del_idx = if let Some(idx) = expected.iter().position(|n| &path == n) {
idx
} else {
panic!("Iterated file is unexpected: {}", path);
};
expected.remove(del_idx);
}
// Not equating `.len() == 0` so that the assertion output
// will contain the extra files
let empty: &[&str] = &[][..];
assert_eq!(expected, empty);
}
#[test]
fn test_absolute_path() {
let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
let dir_path = dir.path().canonicalize().unwrap();
touch(&dir, &["a.rs", "a.jpg", "a.png", "b.docx"][..]);
let expected = ["a.jpg", "a.png"].iter().map(ToString::to_string).collect();
let mut cwd = dir_path.clone();
cwd.push("*.{png,jpg,gif}");
let glob = glob(cwd.to_str().unwrap().to_owned()).unwrap();
equate_to_expected(glob, expected, &dir_path);
}
#[test]
fn test_new() {
let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
let dir_path = dir.path();
touch(&dir, &["a.rs", "a.jpg", "a.png", "b.docx"][..]);
let expected = ["a.jpg", "a.png"].iter().map(ToString::to_string).collect();
let g = GlobWalkerBuilder::new(dir_path, "*.{png,jpg,gif}")
.build()
.unwrap();
equate_to_expected(g, expected, dir_path);
}
#[test]
fn test_from_patterns() {
let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
let dir_path = dir.path();
create_dir_all(dir_path.join("src/some_mod")).expect("");
create_dir_all(dir_path.join("tests")).expect("");
create_dir_all(dir_path.join("contrib")).expect("");
touch(
&dir,
&[
"a.rs",
"b.rs",
"avocado.rs",
"lib.c",
"src[/]hello.rs",
"src[/]world.rs",
"src[/]some_mod[/]unexpected.rs",
"src[/]cruel.txt",
"contrib[/]README.md",
"contrib[/]README.rst",
"contrib[/]lib.rs",
][..],
);
let expected: Vec<_> = [
"src[/]some_mod[/]unexpected.rs",
"src[/]world.rs",
"src[/]hello.rs",
"lib.c",
"contrib[/]lib.rs",
"contrib[/]README.md",
"contrib[/]README.rst",
]
.iter()
.map(normalize_path_sep)
.collect();
let patterns = ["src/**/*.rs", "*.c", "**/lib.rs", "**/*.{md,rst}"];
let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
.build()
.unwrap();
equate_to_expected(glob, expected, dir_path);
}
#[test]
fn test_case_insensitive_matching() {
let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
let dir_path = dir.path();
create_dir_all(dir_path.join("src/some_mod")).expect("");
create_dir_all(dir_path.join("tests")).expect("");
create_dir_all(dir_path.join("contrib")).expect("");
touch(
&dir,
&[
"a.rs",
"b.rs",
"avocado.RS",
"lib.c",
"src[/]hello.RS",
"src[/]world.RS",
"src[/]some_mod[/]unexpected.rs",
"src[/]cruel.txt",
"contrib[/]README.md",
"contrib[/]README.rst",
"contrib[/]lib.rs",
][..],
);
let expected: Vec<_> = [
"src[/]some_mod[/]unexpected.rs",
"src[/]hello.RS",
"src[/]world.RS",
"lib.c",
"contrib[/]lib.rs",
"contrib[/]README.md",
"contrib[/]README.rst",
]
.iter()
.map(normalize_path_sep)
.collect();
let patterns = ["src/**/*.rs", "*.c", "**/lib.rs", "**/*.{md,rst}"];
let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
.case_insensitive(true)
.build()
.unwrap();
equate_to_expected(glob, expected, dir_path);
}
#[test]
fn test_match_dir() {
let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
let dir_path = dir.path();
create_dir_all(dir_path.join("mod")).expect("");
touch(
&dir,
&[
"a.png",
"b.png",
"c.png",
"mod[/]a.png",
"mod[/]b.png",
"mod[/]c.png",
][..],
);
let expected: Vec<_> = ["mod"].iter().map(normalize_path_sep).collect();
let glob = GlobWalkerBuilder::new(dir_path, "mod").build().unwrap();
equate_to_expected(glob, expected, dir_path);
}
#[test]
fn test_blacklist() {
let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
let dir_path = dir.path();
create_dir_all(dir_path.join("src/some_mod")).expect("");
create_dir_all(dir_path.join("tests")).expect("");
create_dir_all(dir_path.join("contrib")).expect("");
touch(
&dir,
&[
"a.rs",
"b.rs",
"avocado.rs",
"lib.c",
"src[/]hello.rs",
"src[/]world.rs",
"src[/]some_mod[/]unexpected.rs",
"src[/]cruel.txt",
"contrib[/]README.md",
"contrib[/]README.rst",
"contrib[/]lib.rs",
][..],
);
let expected: Vec<_> = [
"src[/]some_mod[/]unexpected.rs",
"src[/]hello.rs",
"lib.c",
"contrib[/]lib.rs",
"contrib[/]README.md",
"contrib[/]README.rst",
]
.iter()
.map(normalize_path_sep)
.collect();
let patterns = [
"src/**/*.rs",
"*.c",
"**/lib.rs",
"**/*.{md,rst}",
"!world.rs",
];
let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
.build()
.unwrap();
equate_to_expected(glob, expected, dir_path);
}
#[test]
fn test_blacklist_dir() {
let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
let dir_path = dir.path();
create_dir_all(dir_path.join("Pictures")).expect("");
touch(
&dir,
&[
"a.png",
"b.png",
"c.png",
"Pictures[/]a.png",
"Pictures[/]b.png",
"Pictures[/]c.png",
][..],
);
let expected: Vec<_> = ["a.png", "b.png", "c.png"]
.iter()
.map(normalize_path_sep)
.collect();
let patterns = ["*.{png,jpg,gif}", "!Pictures"];
let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
.build()
.unwrap();
equate_to_expected(glob, expected, dir_path);
}
#[test]
fn test_glob_with_double_star_pattern() {
let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
let dir_path = dir.path().canonicalize().unwrap();
touch(&dir, &["a.rs", "a.jpg", "a.png", "b.docx"][..]);
let expected = ["a.jpg", "a.png"].iter().map(ToString::to_string).collect();
let mut cwd = dir_path.clone();
cwd.push("**");
cwd.push("*.{png,jpg,gif}");
let glob = glob(cwd.to_str().unwrap().to_owned()).unwrap();
equate_to_expected(glob, expected, &dir_path);
}
#[test]
fn test_glob_single_star() {
let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
let dir_path = dir.path();
create_dir_all(dir_path.join("Pictures")).expect("");
create_dir_all(dir_path.join("Pictures").join("b")).expect("");
touch(
&dir,
&[
"a.png",
"b.png",
"c.png",
"Pictures[/]a.png",
"Pictures[/]b.png",
"Pictures[/]c.png",
"Pictures[/]b[/]c.png",
"Pictures[/]b[/]c.png",
"Pictures[/]b[/]c.png",
][..],
);
let glob = GlobWalkerBuilder::new(dir_path, "*")
.sort_by(|a, b| a.path().cmp(b.path()))
.build()
.unwrap();
let expected = ["Pictures", "a.png", "b.png", "c.png"]
.iter()
.map(ToString::to_string)
.collect();
equate_to_expected(glob, expected, dir_path);
}
#[test]
fn test_file_type() {
let dir = TempDir::new("globset_walkdir").expect("Failed to create temporary folder");
let dir_path = dir.path();
create_dir_all(dir_path.join("Pictures")).expect("");
create_dir_all(dir_path.join("Pictures").join("b")).expect("");
touch(
&dir,
&[
"a.png",
"b.png",
"c.png",
"Pictures[/]a.png",
"Pictures[/]b.png",
"Pictures[/]c.png",
"Pictures[/]b[/]c.png",
"Pictures[/]b[/]c.png",
"Pictures[/]b[/]c.png",
][..],
);
let glob = GlobWalkerBuilder::new(dir_path, "*")
.sort_by(|a, b| a.path().cmp(b.path()))
.file_type(FileType::DIR)
.build()
.unwrap();
let expected = ["Pictures"].iter().map(ToString::to_string).collect();
equate_to_expected(glob, expected, dir_path);
let glob = GlobWalkerBuilder::new(dir_path, "*")
.sort_by(|a, b| a.path().cmp(b.path()))
.file_type(FileType::FILE)
.build()
.unwrap();
let expected = ["a.png", "b.png", "c.png"]
.iter()
.map(ToString::to_string)
.collect();
equate_to_expected(glob, expected, dir_path);
}
}