1 Commits

Author SHA1 Message Date
Sylvestre Ledru adda4c1ba5 Update 0.3.0 2022-01-26 23:38:16 +01:00
43 changed files with 1103 additions and 2509 deletions
+6 -10
View File
@@ -1,12 +1,8 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 5
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 5
- package-ecosystem: cargo
directory: "/"
schedule:
interval: daily
time: "07:00"
open-pull-requests-limit: 10
+95 -50
View File
@@ -10,12 +10,12 @@ jobs:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v3
- name: Install `rust` toolchain
run: |
## Install `rust` toolchain
rustup toolchain install stable --no-self-update -c rustfmt --profile minimal
rustup default stable
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
# For bindgen: https://github.com/rust-lang/rust-bindgen/issues/1797
- uses: KyleMayes/install-llvm-action@v1
@@ -26,9 +26,9 @@ jobs:
- run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV
if: matrix.os == 'windows-latest'
- name: Check
run: |
cargo check
- uses: actions-rs/cargo@v1
with:
command: check
test:
name: cargo test
@@ -37,12 +37,12 @@ jobs:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v3
- name: Install `rust` toolchain
run: |
## Install `rust` toolchain
rustup toolchain install stable --no-self-update -c rustfmt --profile minimal
rustup default stable
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
# For bindgen: https://github.com/rust-lang/rust-bindgen/issues/1797
- uses: KyleMayes/install-llvm-action@v1
@@ -53,40 +53,41 @@ jobs:
- run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV
if: matrix.os == 'windows-latest'
- name: Check
run: |
cargo check
- uses: actions-rs/cargo@v1
with:
command: test
fmt:
name: cargo fmt --all -- --check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install `rust` toolchain
run: |
## Install `rust` toolchain
rustup toolchain install stable --no-self-update -c rustfmt --profile minimal
rustup default stable
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- name: cargo fmt
run: |
cargo fmt --all -- --check
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: cargo clippy -- -D warnings
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install `rust` toolchain
run: |
## Install `rust` toolchain
rustup toolchain install stable --no-self-update -c rustfmt --profile minimal
rustup default stable
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add clippy
- name: cargo clippy
run: |
cargo clippy -- -D warnings
- uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings
grcov:
name: Code coverage
@@ -101,20 +102,27 @@ jobs:
- "--all-features"
steps:
- name: Checkout source code
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Install `rust` toolchain
run: |
## Install `rust` toolchain
rustup toolchain install nightly --no-self-update -c rustfmt --profile minimal
rustup default nightly
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.toolchain }}
override: true
- name: "`grcov` ~ install"
run: cargo install grcov
- name: Install grcov
uses: actions-rs/install@v0.1
with:
crate: grcov
version: latest
use-tool-cache: true
- name: cargo test
run: |
cargo test --all --no-fail-fast ${{ matrix.cargo_flags }}
- name: Test
uses: actions-rs/cargo@v1
with:
command: test
args: --all --no-fail-fast ${{ matrix.cargo_flags }}
env:
CARGO_INCREMENTAL: "0"
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cdebug-assertions=off'
@@ -122,6 +130,7 @@ jobs:
- name: Generate coverage data
id: grcov
# uses: actions-rs/grcov@v0.1
run: |
grcov target/debug/ \
--branch \
@@ -138,15 +147,51 @@ jobs:
--service-name "GitHub Actions" \
--service-number ${{ github.run_id }}
- name: Upload coverage as artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: lcov.info
# path: ${{ steps.grcov.outputs.report }}
path: lcov.info
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v1
with:
# file: ${{ steps.grcov.outputs.report }}
file: lcov.info
fail_ci_if_error: true
external:
name: External testsuites
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-latest
toolchain:
- nightly
cargo_flags:
- "--all-features"
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.toolchain }}
override: true
- name: Run the gnu testsuite
id: gnu
run: |
sudo apt install autopoint
git clone https://git.savannah.gnu.org/git/findutils.git ../findutils.gnu
bash util/build-gnu.sh || true
- name: Run the bfs testsuite
id: bfs
run: |
sudo apt install acl libacl1-dev attr libattr1-dev libcap2-bin libcap-dev
git clone https://github.com/tavianator/bfs.git ../bfs
bash util/build-bfs.sh || true
+98 -26
View File
@@ -1,6 +1,6 @@
on: [push, pull_request]
name: External testsuites
name: GNU findutils compatibility tests
jobs:
gnu-tests:
@@ -8,21 +8,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout findutils
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: findutils
- name: Checkout GNU findutils
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
repository: gnu-mirror-unofficial/findutils
path: findutils.gnu
ref: 5768a03ddfb5e18b1682e339d6cdd24ff721c510
submodules: true
- name: Install `rust` toolchain
run: |
## Install `rust` toolchain
rustup toolchain install stable --no-self-update -c rustfmt --profile minimal
rustup default stable
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Install dependencies
shell: bash
run: |
@@ -38,14 +38,39 @@ jobs:
- name: Extract testing info
shell: bash
run: |
- uses: actions/upload-artifact@v3
LOG_FILE=findutils.gnu/tests/test-suite.log
if test -f "$LOG_FILE"; then
TOTAL=$(sed -n "s/.*# TOTAL: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1)
PASS=$(sed -n "s/.*# PASS: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1)
SKIP=$(sed -n "s/.*# SKIP: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1)
FAIL=$(sed -n "s/.*# FAIL: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1)
XPASS=$(sed -n "s/.*# XPASS: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1)
ERROR=$(sed -n "s/.*# ERROR: \(.*\)/\1/p" "$LOG_FILE"|tr -d '\r'|head -n1)
if [[ "$TOTAL" -eq 0 || "$TOTAL" -eq 1 ]]; then
echo "Error in the execution, failing early"
exit 1
fi
output="GNU tests summary = TOTAL: $TOTAL / PASS: $PASS / FAIL: $FAIL / ERROR: $ERROR"
echo "${output}"
if [[ "$FAIL" -gt 0 || "$ERROR" -gt 0 ]]; then echo "::warning ::${output}" ; fi
jq -n \
--arg date "$(date --rfc-email)" \
--arg sha "$GITHUB_SHA" \
--arg total "$TOTAL" \
--arg pass "$PASS" \
--arg skip "$SKIP" \
--arg fail "$FAIL" \
--arg xpass "$XPASS" \
--arg error "$ERROR" \
'{($date): { sha: $sha, total: $total, pass: $pass, skip: $skip, fail: $fail, xpass: $xpass, error: $error, }}' > gnu-result.json
else
echo "::error ::Failed to get summary of test results"
fi
- uses: actions/upload-artifact@v2
with:
name: gnu-test-report
path: |
findutils.gnu/find/testsuite/*.log
findutils.gnu/xargs/testsuite/*.log
findutils.gnu/tests/**/*.log
- uses: actions/upload-artifact@v3
path: findutils.gnu/tests/**/*.log
- uses: actions/upload-artifact@v2
with:
name: gnu-result
path: gnu-result.json
@@ -70,7 +95,18 @@ jobs:
- name: Compare failing tests against master
shell: bash
run: |
./findutils/util/diff-gnu.sh ./dl ./findutils.gnu
OLD_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" dl/test-suite.log | sort)
NEW_FAILING=$(sed -n "s/^FAIL: \([[:print:]]\+\).*/\1/p" findutils.gnu/tests/test-suite.log | sort)
for LINE in $OLD_FAILING; do
if ! grep -Fxq $LINE<<<"$NEW_FAILING"; then
echo "::warning ::Congrats! The gnu test $LINE is now passing!"
fi
done
for LINE in $NEW_FAILING; do
if ! grep -Fxq $LINE<<<"$OLD_FAILING"; then
echo "::error ::gnu test failed: $LINE. $LINE is passing on 'main'. Maybe you have to rebase?"
fi
done
- name: Compare against main results
shell: bash
run: |
@@ -82,20 +118,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout findutils
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: findutils
- name: Checkout BFS
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
repository: tavianator/bfs
path: bfs
ref: '2.6.2'
- name: Install `rust` toolchain
run: |
## Install `rust` toolchain
rustup toolchain install stable --no-self-update -c rustfmt --profile minimal
rustup default stable
ref: '2.3.1'
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Install dependencies
shell: bash
run: |
@@ -108,11 +144,36 @@ jobs:
run: |
cd findutils
bash util/build-bfs.sh ||:
- uses: actions/upload-artifact@v3
- name: Extract testing info
shell: bash
run: |
LOG_FILE=bfs/tests.log
if test -f "$LOG_FILE"; then
PASS=$(sed -n "s/^tests passed: \(.*\)/\1/p" "$LOG_FILE"|head -n1)
FAIL=$(sed -n "s/^tests failed: \(.*\)/\1/p" "$LOG_FILE"|head -n1)
TOTAL=$(($PASS + $FAIL))
if [[ "$TOTAL" -eq 0 || "$TOTAL" -eq 1 ]]; then
echo "Error in the execution, failing early"
exit 1
fi
output="BFS tests summary = TOTAL: $TOTAL / PASS: $PASS / FAIL: $FAIL"
echo "${output}"
if [[ "$FAIL" -gt 0 || "$ERROR" -gt 0 ]]; then echo "::warning ::${output}" ; fi
jq -n \
--arg date "$(date --rfc-email)" \
--arg sha "$GITHUB_SHA" \
--arg total "$TOTAL" \
--arg pass "$PASS" \
--arg fail "$FAIL" \
'{($date): { sha: $sha, total: $total, pass: $pass, fail: $fail, }}' > bfs-result.json
else
echo "::error ::Failed to get summary of test results"
fi
- uses: actions/upload-artifact@v2
with:
name: bfs-test-report
path: bfs/tests.log
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v2
with:
name: bfs-result
path: bfs-result.json
@@ -137,7 +198,18 @@ jobs:
- name: Compare failing tests against main
shell: bash
run: |
./findutils/util/diff-bfs.sh dl/tests.log bfs/tests.log
OLD_FAILING=$(sed -n "s/^\([[:print:]]\+\) failed\!/\1/p" dl/tests.log | sort)
NEW_FAILING=$(sed -n "s/^\([[:print:]]\+\) failed\!/\1/p" bfs/tests.log | sort)
for LINE in $OLD_FAILING; do
if ! grep -Fxq $LINE<<<"$NEW_FAILING"; then
echo "::warning ::Congrats! The bfs test $LINE is now passing!"
fi
done
for LINE in $NEW_FAILING; do
if ! grep -Fxq $LINE<<<"$OLD_FAILING"; then
echo "::error ::bfs test failed: $LINE. $LINE is passing on 'main'. Maybe you have to rebase?"
fi
done
- name: Compare against main results
shell: bash
run: |
-17
View File
@@ -1,17 +0,0 @@
repos:
- repo: local
hooks:
- id: rust-linting
name: Rust linting
description: Run cargo fmt on files included in the commit.
entry: cargo +nightly fmt --
pass_filenames: true
types: [file, rust]
language: system
- id: rust-clippy
name: Rust clippy
description: Run cargo clippy on files included in the commit.
entry: cargo +nightly clippy --workspace --all-targets --all-features --
pass_filenames: false
types: [file, rust]
language: system
Generated
+81 -458
View File
File diff suppressed because it is too large Load Diff
+8 -9
View File
@@ -1,6 +1,6 @@
[package]
name = "findutils"
version = "0.4.0"
version = "0.3.0"
homepage = "https://github.com/uutils/findutils"
repository = "https://github.com/uutils/findutils"
edition = "2018"
@@ -12,19 +12,18 @@ authors = ["uutils developers"]
[dependencies]
chrono = "0.4"
clap = "2.34"
faccess = "0.2.4"
glob = "0.3"
walkdir = "2.3"
regex = "1.7"
once_cell = "1.17"
onig = "6.4"
uucore = { version = "0.0.12", features = ["entries", "fs", "fsext", "mode"] }
regex = "1.5"
once_cell = "1.9"
onig = "6.3"
uucore = { version = "0.0.12", features = ["entries", "fs", "fsext"] }
[dev-dependencies]
assert_cmd = "2"
filetime = "0.2"
nix = "0.26"
predicates = "3"
serial_test = "1.0"
predicates = "2"
serial_test = "0.5"
tempfile = "3"
[[bin]]
-2
View File
@@ -19,5 +19,3 @@ bash util/build-gnu.sh tests/misc/help-version.sh
![Evolution over time - GNU testsuite](https://github.com/uutils/findutils-tracking/blob/main/gnu-results.png?raw=true)
![Evolution over time - BFS testsuite](https://github.com/uutils/findutils-tracking/blob/main/bfs-results.png?raw=true)
For more details, see https://github.com/uutils/findutils-tracking/
+4 -1
View File
@@ -4,9 +4,12 @@
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
extern crate findutils;
extern crate glob;
fn main() {
let args = std::env::args().collect::<Vec<String>>();
let strs: Vec<&str> = args.iter().map(std::convert::AsRef::as_ref).collect();
let strs: Vec<&str> = args.iter().map(|s| s.as_ref()).collect();
let deps = findutils::find::StandardDependencies::new();
std::process::exit(findutils::find::find_main(&strs, &deps));
}
-60
View File
@@ -1,60 +0,0 @@
// Copyright 2022 Tavian Barnes
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
use faccess::PathExt;
use walkdir::DirEntry;
use super::{Matcher, MatcherIO};
/// Matcher for -{read,writ,execut}able.
pub enum AccessMatcher {
Readable,
Writable,
Executable,
}
impl Matcher for AccessMatcher {
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
let path = file_info.path();
match self {
Self::Readable => path.readable(),
Self::Writable => path.writable(),
Self::Executable => path.executable(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::find::matchers::tests::get_dir_entry_for;
use crate::find::matchers::Matcher;
use crate::find::tests::FakeDependencies;
#[test]
fn access_matcher() {
let file_info = get_dir_entry_for("test_data/simple", "abbbc");
let deps = FakeDependencies::new();
assert!(
AccessMatcher::Readable.matches(&file_info, &mut deps.new_matcher_io()),
"file should be readable"
);
assert!(
AccessMatcher::Writable.matches(&file_info, &mut deps.new_matcher_io()),
"file should be writable"
);
#[cfg(unix)]
assert!(
!AccessMatcher::Executable.matches(&file_info, &mut deps.new_matcher_io()),
"file should not be executable"
);
}
}
+5 -1
View File
@@ -18,10 +18,14 @@ use super::{Matcher, MatcherIO};
pub struct DeleteMatcher;
impl DeleteMatcher {
pub fn new() -> Self {
pub fn new() -> DeleteMatcher {
DeleteMatcher
}
pub fn new_box() -> io::Result<Box<dyn Matcher>> {
Ok(Box::new(DeleteMatcher::new()))
}
fn delete(&self, file_path: &Path, file_type: FileType) -> io::Result<()> {
if file_type.is_dir() {
fs::remove_dir(file_path)
-100
View File
@@ -1,100 +0,0 @@
// Copyright 2021 Collabora, Ltd.
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
use std::{
fs::read_dir,
io::{stderr, Write},
};
use super::Matcher;
pub struct EmptyMatcher;
impl EmptyMatcher {
pub fn new() -> EmptyMatcher {
EmptyMatcher
}
}
impl Matcher for EmptyMatcher {
fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool {
if file_info.file_type().is_file() {
match file_info.metadata() {
Ok(meta) => meta.len() == 0,
Err(err) => {
writeln!(
&mut stderr(),
"Error getting size for {}: {}",
file_info.path().display(),
err
)
.unwrap();
false
}
}
} else if file_info.file_type().is_dir() {
match read_dir(file_info.path()) {
Ok(mut it) => it.next().is_none(),
Err(err) => {
writeln!(
&mut stderr(),
"Error getting contents of {}: {}",
file_info.path().display(),
err
)
.unwrap();
false
}
}
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use tempfile::Builder;
use super::*;
use crate::find::matchers::tests::get_dir_entry_for;
use crate::find::matchers::Matcher;
use crate::find::tests::FakeDependencies;
#[test]
fn empty_files() {
let empty_file_info = get_dir_entry_for("test_data/simple", "abbbc");
let nonempty_file_info = get_dir_entry_for("test_data/size", "512bytes");
let matcher = EmptyMatcher::new();
let deps = FakeDependencies::new();
assert!(matcher.matches(&empty_file_info, &mut deps.new_matcher_io()));
assert!(!matcher.matches(&nonempty_file_info, &mut deps.new_matcher_io()));
}
#[test]
fn empty_directories() {
let temp_dir = Builder::new()
.prefix("empty_directories")
.tempdir()
.unwrap();
let temp_dir_path = temp_dir.path().to_string_lossy();
let subdir_name = "subdir";
std::fs::create_dir(temp_dir.path().join(subdir_name)).unwrap();
let matcher = EmptyMatcher::new();
let deps = FakeDependencies::new();
let file_info = get_dir_entry_for(&temp_dir_path, subdir_name);
assert!(matcher.matches(&file_info, &mut deps.new_matcher_io()));
std::fs::File::create(temp_dir.path().join(subdir_name).join("a")).unwrap();
let file_info = get_dir_entry_for(&temp_dir_path, subdir_name);
assert!(!matcher.matches(&file_info, &mut deps.new_matcher_io()));
}
}
+22 -15
View File
@@ -14,7 +14,7 @@ use walkdir::DirEntry;
use super::{Matcher, MatcherIO};
enum Arg {
FileArg(Vec<OsString>),
Filename,
LiteralArg(OsString),
}
@@ -29,26 +29,33 @@ impl SingleExecMatcher {
executable: &str,
args: &[&str],
exec_in_parent_dir: bool,
) -> Result<Self, Box<dyn Error>> {
) -> Result<SingleExecMatcher, Box<dyn Error>> {
let transformed_args = args
.iter()
.map(|&a| {
let parts = a.split("{}").collect::<Vec<_>>();
if parts.len() == 1 {
// No {} present
Arg::LiteralArg(OsString::from(a))
} else {
Arg::FileArg(parts.iter().map(OsString::from).collect())
}
.map(|&a| match a {
"{}" => Arg::Filename,
_ => Arg::LiteralArg(OsString::from(a)),
})
.collect();
Ok(Self {
Ok(SingleExecMatcher {
executable: executable.to_string(),
args: transformed_args,
exec_in_parent_dir,
})
}
pub fn new_box(
executable: &str,
args: &[&str],
exec_in_parent_dir: bool,
) -> Result<Box<dyn Matcher>, Box<dyn Error>> {
Ok(Box::new(SingleExecMatcher::new(
executable,
args,
exec_in_parent_dir,
)?))
}
}
impl Matcher for SingleExecMatcher {
@@ -65,10 +72,10 @@ impl Matcher for SingleExecMatcher {
};
for arg in &self.args {
match *arg {
Arg::LiteralArg(ref a) => command.arg(a.as_os_str()),
Arg::FileArg(ref parts) => command.arg(&parts.join(path_to_file.as_os_str())),
};
command.arg(match *arg {
Arg::LiteralArg(ref a) => a.as_os_str(),
Arg::Filename => path_to_file.as_os_str(),
});
}
if self.exec_in_parent_dir {
match file_info.path().parent() {
-238
View File
@@ -1,238 +0,0 @@
// Copyright 2022 Tavian Barnes
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
use onig::{self, Regex, RegexOptions, Syntax};
/// Parse a string as a POSIX Basic Regular Expression.
fn parse_bre(expr: &str, options: RegexOptions) -> Result<Regex, onig::Error> {
let bre = Syntax::posix_basic();
Regex::with_options(expr, bre.options() | options, bre)
}
/// Push a literal character onto a regex, escaping it if necessary.
fn regex_push_literal(regex: &mut String, ch: char) {
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_03
if matches!(ch, '.' | '[' | '\\' | '*' | '^' | '$') {
regex.push('\\');
}
regex.push(ch);
}
/// Extracts a bracket expression from a glob.
fn extract_bracket_expr(pattern: &str) -> Option<(String, &str)> {
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13_01
//
// If an open bracket introduces a bracket expression as in XBD RE Bracket Expression,
// except that the <exclamation-mark> character ( '!' ) shall replace the <circumflex>
// character ( '^' ) in its role in a non-matching list in the regular expression notation,
// it shall introduce a pattern bracket expression. A bracket expression starting with an
// unquoted <circumflex> character produces unspecified results. Otherwise, '[' shall match
// the character itself.
//
// To check for valid bracket expressions, we scan for the closing bracket and
// attempt to parse that segment as a regex. If that fails, we treat the '['
// literally.
let mut expr = "[".to_string();
let mut chars = pattern.chars();
let mut next = chars.next();
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05
//
// 3. A non-matching list expression begins with a <circumflex> ( '^' ) ...
//
// (but in a glob, '!' is used instead of '^')
if next == Some('!') {
expr.push('^');
next = chars.next();
}
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05
//
// 1. ... The <right-square-bracket> ( ']' ) shall lose its special meaning and represent
// itself in a bracket expression if it occurs first in the list (after an initial
// <circumflex> ( '^' ), if any).
if next == Some(']') {
expr.push(']');
next = chars.next();
}
while let Some(ch) = next {
expr.push(ch);
match ch {
'[' => {
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05
//
// 4. A collating symbol is a collating element enclosed within bracket-period
// ( "[." and ".]" ) delimiters. ...
//
// 5. An equivalence class expression shall ... be expressed by enclosing any
// one of the collating elements in the equivalence class within bracket-
// equal ( "[=" and "=]" ) delimiters.
//
// 6. ... A character class expression is expressed as a character class name
// enclosed within bracket- <colon> ( "[:" and ":]" ) delimiters.
next = chars.next();
if let Some(delim) = next {
expr.push(delim);
if matches!(delim, '.' | '=' | ':') {
let rest = chars.as_str();
let end = rest.find([delim, ']'])? + 2;
expr.push_str(&rest[..end]);
chars = rest[end..].chars();
}
}
}
']' => {
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05
//
// 1. ... The <right-square-bracket> ( ']' ) shall ... terminate the bracket
// expression, unless it appears in a collating symbol (such as "[.].]" ) or is
// the ending <right-square-bracket> for a collating symbol, equivalence class,
// or character class.
break;
}
_ => {}
}
next = chars.next();
}
if parse_bre(&expr, RegexOptions::REGEX_OPTION_NONE).is_ok() {
Some((expr, chars.as_str()))
} else {
None
}
}
/// Converts a POSIX glob into a POSIX Basic Regular Expression
fn glob_to_regex(pattern: &str) -> String {
let mut regex = String::new();
let mut chars = pattern.chars();
while let Some(ch) = chars.next() {
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13
match ch {
'?' => regex.push('.'),
'*' => regex.push_str(".*"),
'\\' => {
if let Some(ch) = chars.next() {
regex_push_literal(&mut regex, ch);
} else {
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/fnmatch.html
//
// If pattern ends with an unescaped <backslash>, fnmatch() shall return a
// non-zero value (indicating either no match or an error).
//
// Most implementations return FNM_NOMATCH in this case, so return a regex that
// never matches.
return "$.".to_string();
}
}
'[' => {
if let Some((expr, rest)) = extract_bracket_expr(chars.as_str()) {
regex.push_str(&expr);
chars = rest.chars();
} else {
regex_push_literal(&mut regex, ch);
}
}
_ => regex_push_literal(&mut regex, ch),
}
}
regex
}
/// An fnmatch()-style glob matcher.
pub struct Pattern {
regex: Regex,
}
impl Pattern {
/// Parse an fnmatch()-style glob.
pub fn new(pattern: &str, caseless: bool) -> Self {
let options = if caseless {
RegexOptions::REGEX_OPTION_IGNORECASE
} else {
RegexOptions::REGEX_OPTION_NONE
};
// As long as glob_to_regex() is correct, this should never fail
let regex = parse_bre(&glob_to_regex(pattern), options).unwrap();
Self { regex }
}
/// Test if this patern matches a string.
pub fn matches(&self, string: &str) -> bool {
self.regex.is_match(string)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn literals() {
assert_eq!(glob_to_regex(r"foo.bar"), r"foo\.bar");
}
#[test]
fn regex_special() {
assert_eq!(glob_to_regex(r"^foo.bar$"), r"\^foo\.bar\$");
}
#[test]
fn wildcards() {
assert_eq!(glob_to_regex(r"foo?bar*baz"), r"foo.bar.*baz");
}
#[test]
fn escapes() {
assert_eq!(glob_to_regex(r"fo\o\?bar\*baz\\"), r"foo?bar\*baz\\");
}
#[test]
fn incomplete_escape() {
assert_eq!(glob_to_regex(r"foo\"), r"$.")
}
#[test]
fn valid_brackets() {
assert_eq!(glob_to_regex(r"foo[bar][!baz]"), r"foo[bar][^baz]");
}
#[test]
fn complex_brackets() {
assert_eq!(
glob_to_regex(r"[!]!.*[\[.].][=]=][:space:]-]"),
r"[^]!.*[\[.].][=]=][:space:]-]"
);
}
#[test]
fn invalid_brackets() {
assert_eq!(glob_to_regex(r"foo[bar[!baz"), r"foo\[bar\[!baz");
}
#[test]
fn pattern_matches() {
assert!(Pattern::new(r"foo*bar", false).matches("foo--bar"));
assert!(!Pattern::new(r"foo*bar", false).matches("bar--foo"));
}
#[test]
fn caseless_matches() {
assert!(Pattern::new(r"foo*BAR", true).matches("FOO--bar"));
assert!(!Pattern::new(r"foo*BAR", true).matches("BAR--foo"));
}
}
-107
View File
@@ -1,107 +0,0 @@
// Copyright 2017 Google Inc.
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
use std::io::{stderr, Write};
use std::path::PathBuf;
use walkdir::DirEntry;
use super::glob::Pattern;
use super::{Matcher, MatcherIO};
fn read_link_target(file_info: &DirEntry) -> Option<PathBuf> {
match file_info.path().read_link() {
Ok(target) => Some(target),
Err(err) => {
// If it's not a symlink, then it's not an error that should be
// shown.
if err.kind() != std::io::ErrorKind::InvalidInput {
writeln!(
&mut stderr(),
"Error reading target of {}: {}",
file_info.path().display(),
err
)
.unwrap();
}
None
}
}
}
/// This matcher makes a comparison of the link target against a shell wildcard
/// pattern. See `glob::Pattern` for details on the exact syntax.
pub struct LinkNameMatcher {
pattern: Pattern,
}
impl LinkNameMatcher {
pub fn new(pattern_string: &str, caseless: bool) -> LinkNameMatcher {
let pattern = Pattern::new(pattern_string, caseless);
Self { pattern }
}
}
impl Matcher for LinkNameMatcher {
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
if let Some(target) = read_link_target(file_info) {
self.pattern.matches(&target.to_string_lossy())
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::find::matchers::tests::get_dir_entry_for;
use crate::find::matchers::Matcher;
use crate::find::tests::FakeDependencies;
use std::io::ErrorKind;
#[cfg(unix)]
use std::os::unix::fs::symlink;
#[cfg(windows)]
use std::os::windows::fs::symlink_file;
fn create_file_link() {
#[cfg(unix)]
if let Err(e) = symlink("abbbc", "test_data/links/link-f") {
if e.kind() != ErrorKind::AlreadyExists {
panic!("Failed to create sym link: {:?}", e);
}
}
#[cfg(windows)]
if let Err(e) = symlink_file("abbbc", "test_data/links/link-f") {
if e.kind() != ErrorKind::AlreadyExists {
panic!("Failed to create sym link: {:?}", e);
}
}
}
#[test]
fn matches_against_link_target() {
create_file_link();
let link_f = get_dir_entry_for("test_data/links", "link-f");
let matcher = LinkNameMatcher::new("ab?bc", false);
let deps = FakeDependencies::new();
assert!(matcher.matches(&link_f, &mut deps.new_matcher_io()));
}
#[test]
fn caseless_matches_against_link_target() {
create_file_link();
let link_f = get_dir_entry_for("test_data/links", "link-f");
let matcher = LinkNameMatcher::new("AbB?c", true);
let deps = FakeDependencies::new();
assert!(matcher.matches(&link_f, &mut deps.new_matcher_io()));
}
}
+73 -136
View File
@@ -25,8 +25,8 @@ pub struct AndMatcher {
}
impl AndMatcher {
pub fn new(submatchers: Vec<Box<dyn Matcher>>) -> Self {
Self { submatchers }
pub fn new(submatchers: Vec<Box<dyn Matcher>>) -> AndMatcher {
AndMatcher { submatchers }
}
}
@@ -35,16 +35,9 @@ impl Matcher for AndMatcher {
/// place. If the nth sub-matcher returns false, then we immediately return
/// and don't make any further calls.
fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
for matcher in &self.submatchers {
if !matcher.matches(dir_entry, matcher_io) {
return false;
}
if matcher_io.should_quit() {
break;
}
}
true
self.submatchers
.iter()
.all(|x| x.matches(dir_entry, matcher_io))
}
fn has_side_effects(&self) -> bool {
@@ -69,14 +62,14 @@ pub struct AndMatcherBuilder {
}
impl AndMatcherBuilder {
pub fn new() -> Self {
Self {
pub fn new() -> AndMatcherBuilder {
AndMatcherBuilder {
submatchers: Vec::new(),
}
}
pub fn new_and_condition(&mut self, matcher: impl Matcher) {
self.submatchers.push(matcher.into_box());
pub fn new_and_condition(&mut self, matcher: Box<dyn Matcher>) {
self.submatchers.push(matcher);
}
/// Builds a Matcher: consuming the builder in the process.
@@ -86,7 +79,9 @@ impl AndMatcherBuilder {
// safe to unwrap: we've just checked the size
return self.submatchers.pop().unwrap();
}
AndMatcher::new(self.submatchers).into_box()
let matcher = Box::new(AndMatcher::new(self.submatchers));
self.submatchers = Vec::new();
matcher
}
}
@@ -99,8 +94,8 @@ pub struct OrMatcher {
}
impl OrMatcher {
pub fn new(submatchers: Vec<Box<dyn Matcher>>) -> Self {
Self { submatchers }
pub fn new(submatchers: Vec<Box<dyn Matcher>>) -> OrMatcher {
OrMatcher { submatchers }
}
}
@@ -109,16 +104,9 @@ impl Matcher for OrMatcher {
/// place. If the nth sub-matcher returns true, then we immediately return
/// and don't make any further calls.
fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
for matcher in &self.submatchers {
if matcher.matches(dir_entry, matcher_io) {
return true;
}
if matcher_io.should_quit() {
break;
}
}
false
self.submatchers
.iter()
.any(|x| x.matches(dir_entry, matcher_io))
}
fn has_side_effects(&self) -> bool {
@@ -143,7 +131,7 @@ pub struct OrMatcherBuilder {
}
impl OrMatcherBuilder {
pub fn new_and_condition(&mut self, matcher: impl Matcher) {
pub fn new_and_condition(&mut self, matcher: Box<dyn Matcher>) {
// safe to unwrap. submatchers always has at least one member
self.submatchers
.last_mut()
@@ -163,8 +151,8 @@ impl OrMatcherBuilder {
Ok(())
}
pub fn new() -> Self {
let mut o = Self {
pub fn new() -> OrMatcherBuilder {
let mut o = OrMatcherBuilder {
submatchers: Vec::new(),
};
o.submatchers.push(AndMatcherBuilder::new());
@@ -182,7 +170,7 @@ impl OrMatcherBuilder {
for x in self.submatchers {
submatchers.push(x.build());
}
OrMatcher::new(submatchers).into_box()
Box::new(OrMatcher::new(submatchers))
}
}
@@ -196,8 +184,8 @@ pub struct ListMatcher {
}
impl ListMatcher {
pub fn new(submatchers: Vec<Box<dyn Matcher>>) -> Self {
Self { submatchers }
pub fn new(submatchers: Vec<Box<dyn Matcher>>) -> ListMatcher {
ListMatcher { submatchers }
}
}
@@ -208,9 +196,6 @@ impl Matcher for ListMatcher {
let mut rc = false;
for matcher in &self.submatchers {
rc = matcher.matches(dir_entry, matcher_io);
if matcher_io.should_quit() {
break;
}
}
rc
}
@@ -237,7 +222,7 @@ pub struct ListMatcherBuilder {
}
impl ListMatcherBuilder {
pub fn new_and_condition(&mut self, matcher: impl Matcher) {
pub fn new_and_condition(&mut self, matcher: Box<dyn Matcher>) {
// safe to unwrap. submatchers always has at least one member
self.submatchers
.last_mut()
@@ -280,8 +265,8 @@ impl ListMatcherBuilder {
Ok(())
}
pub fn new() -> Self {
let mut o = Self {
pub fn new() -> ListMatcherBuilder {
let mut o = ListMatcherBuilder {
submatchers: Vec::new(),
};
o.submatchers.push(OrMatcherBuilder::new());
@@ -306,6 +291,12 @@ impl ListMatcherBuilder {
/// A simple matcher that always matches.
pub struct TrueMatcher;
impl TrueMatcher {
pub fn new_box() -> Box<dyn Matcher> {
Box::new(TrueMatcher {})
}
}
impl Matcher for TrueMatcher {
fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool {
true
@@ -321,16 +312,24 @@ impl Matcher for FalseMatcher {
}
}
impl FalseMatcher {
pub fn new_box() -> Box<dyn Matcher> {
Box::new(FalseMatcher {})
}
}
/// Matcher that wraps another matcher and inverts matching criteria.
pub struct NotMatcher {
submatcher: Box<dyn Matcher>,
}
impl NotMatcher {
pub fn new(submatcher: impl Matcher) -> Self {
Self {
submatcher: submatcher.into_box(),
}
pub fn new(submatcher: Box<dyn Matcher>) -> NotMatcher {
NotMatcher { submatcher }
}
pub fn new_box(submatcher: Box<dyn Matcher>) -> Box<NotMatcher> {
Box::new(NotMatcher::new(submatcher))
}
}
@@ -356,16 +355,13 @@ impl Matcher for NotMatcher {
mod tests {
use super::*;
use crate::find::matchers::quit::QuitMatcher;
use crate::find::matchers::tests::get_dir_entry_for;
use crate::find::matchers::{Matcher, MatcherIO};
use crate::find::tests::FakeDependencies;
use std::cell::RefCell;
use std::rc::Rc;
use walkdir::DirEntry;
/// Simple Matcher impl that has side effects
pub struct HasSideEffects;
pub struct HasSideEffects {}
impl Matcher for HasSideEffects {
fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool {
@@ -377,13 +373,9 @@ mod tests {
}
}
/// Matcher that counts its invocations
struct Counter(Rc<RefCell<u32>>);
impl Matcher for Counter {
fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool {
*self.0.borrow_mut() += 1;
true
impl HasSideEffects {
pub fn new_box() -> Box<dyn Matcher> {
Box::new(HasSideEffects {})
}
}
@@ -394,12 +386,12 @@ mod tests {
let deps = FakeDependencies::new();
// start with one matcher returning true
builder.new_and_condition(TrueMatcher);
builder.new_and_condition(TrueMatcher::new_box());
assert!(builder.build().matches(&abbbc, &mut deps.new_matcher_io()));
builder = AndMatcherBuilder::new();
builder.new_and_condition(TrueMatcher);
builder.new_and_condition(FalseMatcher);
builder.new_and_condition(TrueMatcher::new_box());
builder.new_and_condition(FalseMatcher::new_box());
assert!(!builder.build().matches(&abbbc, &mut deps.new_matcher_io()));
}
@@ -410,13 +402,13 @@ mod tests {
let deps = FakeDependencies::new();
// start with one matcher returning false
builder.new_and_condition(FalseMatcher);
builder.new_and_condition(FalseMatcher::new_box());
assert!(!builder.build().matches(&abbbc, &mut deps.new_matcher_io()));
let mut builder = OrMatcherBuilder::new();
builder.new_and_condition(FalseMatcher);
builder.new_and_condition(FalseMatcher::new_box());
builder.new_or_condition("-o").unwrap();
builder.new_and_condition(TrueMatcher);
builder.new_and_condition(TrueMatcher::new_box());
assert!(builder.build().matches(&abbbc, &mut deps.new_matcher_io()));
}
@@ -427,21 +419,21 @@ mod tests {
let deps = FakeDependencies::new();
// result should always match that of the last pushed submatcher
builder.new_and_condition(FalseMatcher);
builder.new_and_condition(FalseMatcher::new_box());
assert!(!builder.build().matches(&abbbc, &mut deps.new_matcher_io()));
builder = ListMatcherBuilder::new();
builder.new_and_condition(FalseMatcher);
builder.new_and_condition(FalseMatcher::new_box());
builder.new_list_condition().unwrap();
builder.new_and_condition(TrueMatcher);
builder.new_and_condition(TrueMatcher::new_box());
assert!(builder.build().matches(&abbbc, &mut deps.new_matcher_io()));
builder = ListMatcherBuilder::new();
builder.new_and_condition(FalseMatcher);
builder.new_and_condition(FalseMatcher::new_box());
builder.new_list_condition().unwrap();
builder.new_and_condition(TrueMatcher);
builder.new_and_condition(TrueMatcher::new_box());
builder.new_list_condition().unwrap();
builder.new_and_condition(FalseMatcher);
builder.new_and_condition(FalseMatcher::new_box());
assert!(!builder.build().matches(&abbbc, &mut deps.new_matcher_io()));
}
@@ -468,12 +460,12 @@ mod tests {
let mut builder = AndMatcherBuilder::new();
// start with one matcher with no side effects false
builder.new_and_condition(TrueMatcher);
builder.new_and_condition(TrueMatcher::new_box());
assert!(!builder.build().has_side_effects());
builder = AndMatcherBuilder::new();
builder.new_and_condition(TrueMatcher);
builder.new_and_condition(HasSideEffects);
builder.new_and_condition(TrueMatcher::new_box());
builder.new_and_condition(HasSideEffects::new_box());
assert!(builder.build().has_side_effects());
}
@@ -482,12 +474,12 @@ mod tests {
let mut builder = OrMatcherBuilder::new();
// start with one matcher with no side effects false
builder.new_and_condition(TrueMatcher);
builder.new_and_condition(TrueMatcher::new_box());
assert!(!builder.build().has_side_effects());
builder = OrMatcherBuilder::new();
builder.new_and_condition(TrueMatcher);
builder.new_and_condition(HasSideEffects);
builder.new_and_condition(TrueMatcher::new_box());
builder.new_and_condition(HasSideEffects::new_box());
assert!(builder.build().has_side_effects());
}
@@ -496,12 +488,12 @@ mod tests {
let mut builder = ListMatcherBuilder::new();
// start with one matcher with no side effects false
builder.new_and_condition(TrueMatcher);
builder.new_and_condition(TrueMatcher::new_box());
assert!(!builder.build().has_side_effects());
builder = ListMatcherBuilder::new();
builder.new_and_condition(TrueMatcher);
builder.new_and_condition(HasSideEffects);
builder.new_and_condition(TrueMatcher::new_box());
builder.new_and_condition(HasSideEffects::new_box());
assert!(builder.build().has_side_effects());
}
@@ -520,8 +512,8 @@ mod tests {
#[test]
fn not_matches_works() {
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let not_true = NotMatcher::new(TrueMatcher);
let not_false = NotMatcher::new(FalseMatcher);
let not_true = NotMatcher::new(TrueMatcher::new_box());
let not_false = NotMatcher::new(FalseMatcher::new_box());
let deps = FakeDependencies::new();
assert!(!not_true.matches(&abbbc, &mut deps.new_matcher_io()));
assert!(not_false.matches(&abbbc, &mut deps.new_matcher_io()));
@@ -529,64 +521,9 @@ mod tests {
#[test]
fn not_has_side_effects_works() {
let has_fx = NotMatcher::new(HasSideEffects);
let hasnt_fx = NotMatcher::new(FalseMatcher);
let has_fx = NotMatcher::new(HasSideEffects::new_box());
let hasnt_fx = NotMatcher::new(FalseMatcher::new_box());
assert!(has_fx.has_side_effects());
assert!(!hasnt_fx.has_side_effects());
}
#[test]
fn and_quit_works() {
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let mut builder = AndMatcherBuilder::new();
let deps = FakeDependencies::new();
let before = Rc::new(RefCell::new(0));
let after = Rc::new(RefCell::new(0));
builder.new_and_condition(Counter(before.clone()));
builder.new_and_condition(QuitMatcher);
builder.new_and_condition(Counter(after.clone()));
builder.build().matches(&abbbc, &mut deps.new_matcher_io());
assert_eq!(*before.borrow(), 1);
assert_eq!(*after.borrow(), 0);
}
#[test]
fn or_quit_works() {
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let mut builder = OrMatcherBuilder::new();
let deps = FakeDependencies::new();
let before = Rc::new(RefCell::new(0));
let after = Rc::new(RefCell::new(0));
builder.new_and_condition(Counter(before.clone()));
builder.new_or_condition("-o").unwrap();
builder.new_and_condition(QuitMatcher);
builder.new_or_condition("-o").unwrap();
builder.new_and_condition(Counter(after.clone()));
builder.build().matches(&abbbc, &mut deps.new_matcher_io());
assert_eq!(*before.borrow(), 1);
assert_eq!(*after.borrow(), 0);
}
#[test]
fn list_quit_works() {
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let mut builder = ListMatcherBuilder::new();
let deps = FakeDependencies::new();
let before = Rc::new(RefCell::new(0));
let after = Rc::new(RefCell::new(0));
builder.new_and_condition(Counter(before.clone()));
builder.new_list_condition().unwrap();
builder.new_and_condition(QuitMatcher);
builder.new_list_condition().unwrap();
builder.new_and_condition(Counter(after.clone()));
builder.build().matches(&abbbc, &mut deps.new_matcher_io());
assert_eq!(*before.borrow(), 1);
assert_eq!(*after.borrow(), 0);
}
}
+45 -145
View File
@@ -4,23 +4,16 @@
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
mod access;
mod delete;
mod empty;
pub mod exec;
mod glob;
mod lname;
mod logical_matchers;
mod name;
mod path;
mod perm;
mod printer;
mod printf;
mod prune;
mod quit;
mod regex;
mod size;
mod stat;
mod time;
mod type_matcher;
@@ -30,34 +23,12 @@ use std::time::SystemTime;
use std::{error::Error, str::FromStr};
use walkdir::DirEntry;
use self::access::AccessMatcher;
use self::delete::DeleteMatcher;
use self::empty::EmptyMatcher;
use self::exec::SingleExecMatcher;
use self::lname::LinkNameMatcher;
use self::logical_matchers::{
AndMatcherBuilder, FalseMatcher, ListMatcherBuilder, NotMatcher, TrueMatcher,
};
use self::name::NameMatcher;
use self::path::PathMatcher;
use self::perm::PermMatcher;
use self::printer::{PrintDelimiter, Printer};
use self::printf::Printf;
use self::prune::PruneMatcher;
use self::quit::QuitMatcher;
use self::regex::RegexMatcher;
use self::size::SizeMatcher;
use self::stat::{InodeMatcher, LinksMatcher};
use self::time::{FileTimeMatcher, FileTimeType, NewerMatcher};
use self::type_matcher::TypeMatcher;
use super::{Config, Dependencies};
/// Struct holding references to outputs and any inputs that can't be derived
/// from the file/directory info.
pub struct MatcherIO<'a> {
should_skip_dir: bool,
quit: bool,
deps: &'a dyn Dependencies<'a>,
}
@@ -66,7 +37,6 @@ impl<'a> MatcherIO<'a> {
MatcherIO {
deps,
should_skip_dir: false,
quit: false,
}
}
@@ -78,14 +48,6 @@ impl<'a> MatcherIO<'a> {
self.should_skip_dir
}
pub fn quit(&mut self) {
self.quit = true;
}
pub fn should_quit(&self) -> bool {
self.quit
}
pub fn now(&self) -> SystemTime {
self.deps.now()
}
@@ -95,15 +57,7 @@ impl<'a> MatcherIO<'a> {
/// is what's being searched for. To a first order approximation, find consists
/// of building a chain of Matcher objects, and then walking a directory tree,
/// passing each entry to the chain of Matchers.
pub trait Matcher: 'static {
/// Boxes this matcher as a trait object.
fn into_box(self) -> Box<dyn Matcher>
where
Self: Sized,
{
Box::new(self)
}
pub trait Matcher {
/// Returns whether the given file matches the object's predicate.
fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool;
@@ -126,28 +80,6 @@ pub trait Matcher: 'static {
fn finished(&self) {}
}
impl Matcher for Box<dyn Matcher> {
fn into_box(self) -> Box<dyn Matcher> {
self
}
fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
(**self).matches(file_info, matcher_io)
}
fn has_side_effects(&self) -> bool {
(**self).has_side_effects()
}
fn finished_dir(&self, finished_directory: &Path) {
(**self).finished_dir(finished_directory)
}
fn finished(&self) {
(**self).finished()
}
}
pub enum ComparableValue {
MoreThan(u64),
EqualTo(u64),
@@ -183,9 +115,10 @@ pub fn build_top_level_matcher(
// if the matcher doesn't have any side-effects, then we default to printing
if !top_level_matcher.has_side_effects() {
let mut new_and_matcher = AndMatcherBuilder::new();
let mut new_and_matcher = logical_matchers::AndMatcherBuilder::new();
new_and_matcher.new_and_condition(top_level_matcher);
new_and_matcher.new_and_condition(Printer::new(PrintDelimiter::Newline));
new_and_matcher
.new_and_condition(printer::Printer::new_box(printer::PrintDelimiter::Newline));
return Ok(new_and_matcher.build());
}
Ok(top_level_matcher)
@@ -265,7 +198,7 @@ fn build_matcher_tree(
arg_index: usize,
expecting_bracket: bool,
) -> Result<(usize, Box<dyn Matcher>), Box<dyn Error>> {
let mut top_level_matcher = ListMatcherBuilder::new();
let mut top_level_matcher = logical_matchers::ListMatcherBuilder::new();
let mut regex_type = regex::RegexType::default();
@@ -277,39 +210,31 @@ fn build_matcher_tree(
let mut invert_next_matcher = false;
while i < args.len() {
let possible_submatcher = match args[i] {
"-print" => Some(Printer::new(PrintDelimiter::Newline).into_box()),
"-print0" => Some(Printer::new(PrintDelimiter::Null).into_box()),
"-print" => Some(printer::Printer::new_box(printer::PrintDelimiter::Newline)),
"-print0" => Some(printer::Printer::new_box(printer::PrintDelimiter::Null)),
"-printf" => {
if i >= args.len() - 1 {
return Err(From::from(format!("missing argument to {}", args[i])));
}
i += 1;
Some(Printf::new(args[i])?.into_box())
Some(printf::Printf::new_box(args[i])?)
}
"-true" => Some(TrueMatcher.into_box()),
"-false" => Some(FalseMatcher.into_box()),
"-lname" | "-ilname" => {
"-true" => Some(logical_matchers::TrueMatcher::new_box()),
"-false" => Some(logical_matchers::FalseMatcher::new_box()),
"-name" => {
if i >= args.len() - 1 {
return Err(From::from(format!("missing argument to {}", args[i])));
}
i += 1;
Some(LinkNameMatcher::new(args[i], args[i - 1].starts_with("-i")).into_box())
Some(name::NameMatcher::new_box(args[i])?)
}
"-name" | "-iname" => {
"-iname" => {
if i >= args.len() - 1 {
return Err(From::from(format!("missing argument to {}", args[i])));
}
i += 1;
Some(NameMatcher::new(args[i], args[i - 1].starts_with("-i")).into_box())
Some(name::CaselessNameMatcher::new_box(args[i])?)
}
"-path" | "-ipath" | "-wholename" | "-iwholename" => {
if i >= args.len() - 1 {
return Err(From::from(format!("missing argument to {}", args[i])));
}
i += 1;
Some(PathMatcher::new(args[i], args[i - 1].starts_with("-i")).into_box())
}
"-readable" => Some(AccessMatcher::Readable.into_box()),
"-regextype" => {
if i >= args.len() - 1 {
return Err(From::from(format!("missing argument to {}", args[i])));
@@ -323,49 +248,49 @@ fn build_matcher_tree(
return Err(From::from(format!("missing argument to {}", args[i])));
}
i += 1;
Some(RegexMatcher::new(regex_type, args[i], false)?.into_box())
Some(regex::RegexMatcher::new_box(regex_type, args[i], false)?)
}
"-iregex" => {
if i >= args.len() - 1 {
return Err(From::from(format!("missing argument to {}", args[i])));
}
i += 1;
Some(RegexMatcher::new(regex_type, args[i], true)?.into_box())
Some(regex::RegexMatcher::new_box(regex_type, args[i], true)?)
}
"-type" => {
if i >= args.len() - 1 {
return Err(From::from(format!("missing argument to {}", args[i])));
}
i += 1;
Some(TypeMatcher::new(args[i])?.into_box())
Some(type_matcher::TypeMatcher::new_box(args[i])?)
}
"-delete" => {
// -delete implicitly requires -depth
config.depth_first = true;
Some(DeleteMatcher::new().into_box())
Some(delete::DeleteMatcher::new_box()?)
}
"-newer" => {
if i >= args.len() - 1 {
return Err(From::from(format!("missing argument to {}", args[i])));
}
i += 1;
Some(NewerMatcher::new(args[i])?.into_box())
Some(time::NewerMatcher::new_box(args[i])?)
}
"-mtime" | "-atime" | "-ctime" => {
if i >= args.len() - 1 {
return Err(From::from(format!("missing argument to {}", args[i])));
}
let file_time_type = match args[i] {
"-atime" => FileTimeType::Accessed,
"-ctime" => FileTimeType::Created,
"-mtime" => FileTimeType::Modified,
"-atime" => time::FileTimeType::Accessed,
"-ctime" => time::FileTimeType::Created,
"-mtime" => time::FileTimeType::Modified,
// This shouldn't be possible. We've already checked the value
// is one of those three values.
_ => unreachable!("Encountered unexpected value {}", args[i]),
};
let days = convert_arg_to_comparable_value(args[i], args[i + 1])?;
i += 1;
Some(FileTimeMatcher::new(file_time_type, days).into_box())
Some(time::FileTimeMatcher::new_box(file_time_type, days))
}
"-size" => {
if i >= args.len() - 1 {
@@ -374,9 +299,8 @@ fn build_matcher_tree(
let (size, unit) =
convert_arg_to_comparable_value_and_suffix(args[i], args[i + 1])?;
i += 1;
Some(SizeMatcher::new(size, &unit)?.into_box())
Some(size::SizeMatcher::new_box(size, &unit)?)
}
"-empty" => Some(EmptyMatcher::new().into_box()),
"-exec" | "-execdir" => {
let mut arg_index = i + 1;
while arg_index < args.len() && args[arg_index] != ";" {
@@ -398,38 +322,20 @@ fn build_matcher_tree(
let executable = args[i + 1];
let exec_args = &args[i + 2..arg_index];
i = arg_index;
Some(
SingleExecMatcher::new(executable, exec_args, expression == "-execdir")?
.into_box(),
)
Some(exec::SingleExecMatcher::new_box(
executable,
exec_args,
expression == "-execdir",
)?)
}
"-inum" => {
if i >= args.len() - 1 {
return Err(From::from(format!("missing argument to {}", args[i])));
}
let inum = convert_arg_to_comparable_value(args[i], args[i + 1])?;
i += 1;
Some(InodeMatcher::new(inum)?.into_box())
}
"-links" => {
if i >= args.len() - 1 {
return Err(From::from(format!("missing argument to {}", args[i])));
}
let inum = convert_arg_to_comparable_value(args[i], args[i + 1])?;
i += 1;
Some(LinksMatcher::new(inum)?.into_box())
}
"-executable" => Some(AccessMatcher::Executable.into_box()),
"-perm" => {
if i >= args.len() - 1 {
return Err(From::from(format!("missing argument to {}", args[i])));
}
i += 1;
Some(PermMatcher::new(args[i])?.into_box())
Some(perm::PermMatcher::new_box(args[i])?)
}
"-prune" => Some(PruneMatcher::new().into_box()),
"-quit" => Some(QuitMatcher.into_box()),
"-writable" => Some(AccessMatcher::Writable.into_box()),
"-prune" => Some(prune::PruneMatcher::new_box()),
"-not" | "!" => {
if !are_more_expressions(args, i) {
return Err(From::from(format!(
@@ -440,7 +346,7 @@ fn build_matcher_tree(
invert_next_matcher = !invert_next_matcher;
None
}
"-and" | "-a" => {
"-a" => {
if !are_more_expressions(args, i) {
return Err(From::from(format!(
"expected an expression after {}",
@@ -486,11 +392,6 @@ fn build_matcher_tree(
config.depth_first = true;
None
}
"-mount" | "-xdev" => {
// TODO add warning if it appears after actual testing criterion
config.same_file_system = true;
None
}
"-sorted" => {
// TODO add warning if it appears after actual testing criterion
config.sorted_output = true;
@@ -525,7 +426,8 @@ fn build_matcher_tree(
};
if let Some(submatcher) = possible_submatcher {
if invert_next_matcher {
top_level_matcher.new_and_condition(NotMatcher::new(submatcher));
top_level_matcher
.new_and_condition(logical_matchers::NotMatcher::new_box(submatcher));
invert_next_matcher = false;
} else {
top_level_matcher.new_and_condition(submatcher);
@@ -722,19 +624,17 @@ mod tests {
#[test]
fn build_top_level_matcher_dash_a_works() {
for arg in &["-a", "-and"] {
let abbbc = get_dir_entry_for("./test_data/simple", "abbbc");
let mut config = Config::default();
let deps = FakeDependencies::new();
let abbbc = get_dir_entry_for("./test_data/simple", "abbbc");
let mut config = Config::default();
let deps = FakeDependencies::new();
// build a matcher using an explicit -a argument
let matcher = build_top_level_matcher(&["-true", arg, "-true"], &mut config).unwrap();
assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io()));
assert_eq!(
deps.get_output_as_string(),
fix_up_slashes("./test_data/simple/abbbc\n")
);
}
// build a matcher using an explicit -a argument
let matcher = build_top_level_matcher(&["-true", "-a", "-true"], &mut config).unwrap();
assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io()));
assert_eq!(
deps.get_output_as_string(),
fix_up_slashes("./test_data/simple/abbbc\n")
);
}
#[test]
@@ -1125,7 +1025,7 @@ mod tests {
fn build_top_level_matcher_perm_bad() {
let mut config = Config::default();
if let Err(e) = build_top_level_matcher(&["-perm", "foo"], &mut config) {
assert!(e.to_string().contains("invalid operator"));
assert!(e.to_string().contains("invalid mode"));
} else {
panic!("-perm with bad mode pattern should fail");
}
+57 -51
View File
@@ -4,65 +4,79 @@
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
use glob::Pattern;
use glob::PatternError;
use walkdir::DirEntry;
use super::glob::Pattern;
use super::{Matcher, MatcherIO};
/// This matcher makes a comparison of the name against a shell wildcard
/// pattern. See `glob::Pattern` for details on the exact syntax.
/// This matcher makes a case-sensitive comparison of the name against a
/// shell wildcard pattern. See `glob::Pattern` for details on the exact
/// syntax.
pub struct NameMatcher {
pattern: Pattern,
}
impl NameMatcher {
pub fn new(pattern_string: &str, caseless: bool) -> Self {
let pattern = Pattern::new(pattern_string, caseless);
Self { pattern }
pub fn new(pattern_string: &str) -> Result<NameMatcher, PatternError> {
let p = Pattern::new(pattern_string)?;
Ok(NameMatcher { pattern: p })
}
pub fn new_box(pattern_string: &str) -> Result<Box<dyn Matcher>, PatternError> {
Ok(Box::new(NameMatcher::new(pattern_string)?))
}
}
impl Matcher for NameMatcher {
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
let name = file_info.file_name().to_string_lossy();
self.pattern.matches(&name)
self.pattern
.matches(file_info.file_name().to_string_lossy().as_ref())
}
}
/// This matcher makes a case-insensitive comparison of the name against a
/// shell wildcard pattern. See `glob::Pattern` for details on the exact
/// syntax.
pub struct CaselessNameMatcher {
pattern: Pattern,
}
impl CaselessNameMatcher {
pub fn new(pattern_string: &str) -> Result<CaselessNameMatcher, PatternError> {
let p = Pattern::new(&pattern_string.to_lowercase())?;
Ok(CaselessNameMatcher { pattern: p })
}
pub fn new_box(pattern_string: &str) -> Result<Box<dyn Matcher>, PatternError> {
Ok(Box::new(CaselessNameMatcher::new(pattern_string)?))
}
}
impl super::Matcher for CaselessNameMatcher {
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
self.pattern.matches(
file_info
.file_name()
.to_string_lossy()
.to_lowercase()
.as_ref(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::find::matchers::tests::get_dir_entry_for;
use crate::find::matchers::Matcher;
use crate::find::tests::FakeDependencies;
use std::io::ErrorKind;
#[cfg(unix)]
use std::os::unix::fs::symlink;
#[cfg(windows)]
use std::os::windows::fs::symlink_file;
fn create_file_link() {
#[cfg(unix)]
if let Err(e) = symlink("abbbc", "test_data/links/link-f") {
if e.kind() != ErrorKind::AlreadyExists {
panic!("Failed to create sym link: {:?}", e);
}
}
#[cfg(windows)]
if let Err(e) = symlink_file("abbbc", "test_data/links/link-f") {
if e.kind() != ErrorKind::AlreadyExists {
panic!("Failed to create sym link: {:?}", e);
}
}
}
#[test]
fn matching_with_wrong_case_returns_false() {
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let matcher = NameMatcher::new("A*C", false);
let matcher = NameMatcher::new(&"A*C".to_string()).unwrap();
let deps = FakeDependencies::new();
assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io()));
}
@@ -70,7 +84,7 @@ mod tests {
#[test]
fn matching_with_right_case_returns_true() {
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let matcher = NameMatcher::new("abb?c", false);
let matcher = NameMatcher::new(&"abb?c".to_string()).unwrap();
let deps = FakeDependencies::new();
assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io()));
}
@@ -78,25 +92,21 @@ mod tests {
#[test]
fn not_matching_returns_false() {
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let matcher = NameMatcher::new("shouldn't match", false);
let matcher = NameMatcher::new(&"shouldn't match".to_string()).unwrap();
let deps = FakeDependencies::new();
assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io()));
}
#[test]
fn matches_against_link_file_name() {
create_file_link();
let link_f = get_dir_entry_for("test_data/links", "link-f");
let matcher = NameMatcher::new("link?f", false);
let deps = FakeDependencies::new();
assert!(matcher.matches(&link_f, &mut deps.new_matcher_io()));
fn cant_create_with_invalid_pattern() {
let result = NameMatcher::new(&"a**c".to_string());
assert!(result.is_err());
}
#[test]
fn caseless_matching_with_wrong_case_returns_true() {
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let matcher = NameMatcher::new("A*C", true);
let matcher = CaselessNameMatcher::new(&"A*C".to_string()).unwrap();
let deps = FakeDependencies::new();
assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io()));
}
@@ -104,7 +114,7 @@ mod tests {
#[test]
fn caseless_matching_with_right_case_returns_true() {
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let matcher = NameMatcher::new("abb?c", true);
let matcher = CaselessNameMatcher::new(&"abb?c".to_string()).unwrap();
let deps = FakeDependencies::new();
assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io()));
}
@@ -112,18 +122,14 @@ mod tests {
#[test]
fn caseless_not_matching_returns_false() {
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let matcher = NameMatcher::new("shouldn't match", true);
let matcher = CaselessNameMatcher::new(&"shouldn't match".to_string()).unwrap();
let deps = FakeDependencies::new();
assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io()));
}
#[test]
fn caseless_matches_against_link_file_name() {
create_file_link();
let link_f = get_dir_entry_for("test_data/links", "link-f");
let matcher = NameMatcher::new("linK?f", true);
let deps = FakeDependencies::new();
assert!(matcher.matches(&link_f, &mut deps.new_matcher_io()));
fn caseless_cant_create_with_invalid_pattern() {
let result = CaselessNameMatcher::new(&"a**c".to_string());
assert!(result.is_err());
}
}
-82
View File
@@ -1,82 +0,0 @@
// Copyright 2017 Google Inc.
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
use walkdir::DirEntry;
use super::glob::Pattern;
use super::{Matcher, MatcherIO};
/// This matcher makes a comparison of the path against a shell wildcard
/// pattern. See `glob::Pattern` for details on the exact syntax.
pub struct PathMatcher {
pattern: Pattern,
}
impl PathMatcher {
pub fn new(pattern_string: &str, caseless: bool) -> Self {
let pattern = Pattern::new(pattern_string, caseless);
Self { pattern }
}
}
impl Matcher for PathMatcher {
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
let path = file_info.path().to_string_lossy();
self.pattern.matches(&path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::find::matchers::tests::get_dir_entry_for;
use crate::find::matchers::Matcher;
use crate::find::tests::FakeDependencies;
// Variants of fix_up_slashes that properly escape the forward slashes for
// being in a glob.
#[cfg(windows)]
fn fix_up_glob_slashes(re: &str) -> String {
re.replace("/", "\\\\")
}
#[cfg(not(windows))]
fn fix_up_glob_slashes(re: &str) -> String {
re.to_owned()
}
#[test]
fn matching_against_whole_path() {
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let matcher = PathMatcher::new(&fix_up_glob_slashes("test_*/*/a*c"), false);
let deps = FakeDependencies::new();
assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io()));
}
#[test]
fn not_matching_against_just_name() {
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let matcher = PathMatcher::new("a*c", false);
let deps = FakeDependencies::new();
assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io()));
}
#[test]
fn not_matching_against_wrong_case() {
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let matcher = PathMatcher::new(&fix_up_glob_slashes("test_*/*/A*C"), false);
let deps = FakeDependencies::new();
assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io()));
}
#[test]
fn caseless_matching() {
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let matcher = PathMatcher::new(&fix_up_glob_slashes("test_*/*/A*C"), true);
let deps = FakeDependencies::new();
assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io()));
}
}
+376 -89
View File
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -28,8 +28,12 @@ pub struct Printer {
}
impl Printer {
pub fn new(delimiter: PrintDelimiter) -> Self {
Self { delimiter }
pub fn new(delimiter: PrintDelimiter) -> Printer {
Printer { delimiter }
}
pub fn new_box(delimiter: PrintDelimiter) -> Box<dyn Matcher> {
Box::new(Printer::new(delimiter))
}
}

Some files were not shown because too many files have changed in this diff Show More