mirror of
https://github.com/uutils/findutils.git
synced 2026-06-10 15:48:30 -07:00
Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b94b5f0122 | |||
| 4f8fbf55a1 | |||
| 3dbdbe8442 | |||
| 949bcb83c8 | |||
| 61b396ac31 | |||
| 6c99dc175f | |||
| 778abdb6a9 | |||
| a61993b705 | |||
| bd159d7627 | |||
| ad61338bb2 | |||
| 4a1c683834 | |||
| 2ec695381e | |||
| a04b499e15 | |||
| de42f9bd83 | |||
| 6d264dbbcf | |||
| 81c39aad7d | |||
| 5cb273eecb | |||
| 55021afb69 | |||
| 013be1b093 | |||
| 0429507f15 | |||
| 5235d78291 | |||
| 1a40442f20 | |||
| cb83b6db3a | |||
| e92ebc9288 | |||
| d6dc18973d | |||
| 9fdfb1ee08 | |||
| 830298ed22 | |||
| 8784923b54 | |||
| 89f98f1aff | |||
| f911610d93 | |||
| 22c5f7dcc3 | |||
| f3a70e9eb4 | |||
| 55298be4ad | |||
| 764be30111 | |||
| 6d54482ed4 | |||
| 49975f5c62 | |||
| dacc473c85 | |||
| 5bad2e0afe | |||
| 28b1c88d9c | |||
| c7b89d5341 | |||
| b4c5c4946b | |||
| 16bb731937 | |||
| 6d366cbbda | |||
| 280381bb28 | |||
| 1809a3edca | |||
| 0a54e7c6d9 | |||
| 5d264c9c7b | |||
| d8355d48a3 | |||
| a7b596daef | |||
| 22a58468b8 | |||
| 0f9ca9eace | |||
| a90f505510 | |||
| e32e754102 | |||
| 83dc5fe7de | |||
| cb3f3cfea4 | |||
| 1f9d90eb4f | |||
| 98ce63a65e | |||
| a547d24f46 | |||
| b263b20801 | |||
| 5ae9315c1b | |||
| 87459296e6 | |||
| e9374afc99 | |||
| 1ea16258c3 | |||
| 9226ff10b6 | |||
| 8c93719bfc | |||
| 629f5b793d | |||
| d7ce651130 | |||
| 7be665cbbe | |||
| 5319654dfb | |||
| 145b96871a | |||
| fb4108192c | |||
| e0397917e4 | |||
| 9d1a97768f | |||
| fc3de09ca2 | |||
| 78e520106c | |||
| a21c379953 | |||
| c45e474c7e | |||
| f536f813e2 | |||
| 8685fa5c64 | |||
| b7f93ea816 | |||
| 10c24ca5f1 | |||
| 33cd6573aa | |||
| 6ce5ffaa9e | |||
| 78b264deae | |||
| 3fa75542c4 | |||
| 4e954a7a41 | |||
| 50b7ec67e0 | |||
| b3f4deda63 | |||
| 1b3ad03b1f | |||
| 11a0b64c91 | |||
| 2e9e90887f | |||
| 526079b5d3 | |||
| dce8b2dda5 | |||
| fb7a56be63 | |||
| acbfcc52ca | |||
| b641b85239 | |||
| eefc5804d4 | |||
| 309d3d9211 | |||
| 9d66be6b7d | |||
| 27d95aa1cc | |||
| 05493375c7 | |||
| 7f3c00db69 | |||
| 4d367891dc | |||
| e3f4224f12 | |||
| 8fc5ddb299 | |||
| b4f22f0747 | |||
| db8e265547 | |||
| f9d74ada2e | |||
| 907f3841ab | |||
| 888d3cd516 | |||
| f2e49d8d48 | |||
| d847a7bf7e | |||
| abf851dd7d | |||
| d9d2a4e865 | |||
| 9b0fea9880 | |||
| a7a73c325d | |||
| 465856ce24 | |||
| e456c23d5c | |||
| 4dc350e5fb | |||
| fcc929a978 | |||
| 423453eaf8 | |||
| 79986a26f2 | |||
| de1bc84812 | |||
| f0cd3748b8 | |||
| 780d106b34 | |||
| 4043c00bde | |||
| f0c3562aca | |||
| d7bf7da4a1 | |||
| 73dcd94827 | |||
| 60e4a8cf92 | |||
| 8b295e7d8a | |||
| 2dc916f980 | |||
| 19012452e1 | |||
| 6042d5a47f | |||
| 45e5964e6a | |||
| 9544f78977 | |||
| 5be00c85d7 | |||
| 8771861b0c | |||
| c4a56e9c3f | |||
| 3b222d6040 | |||
| 6f53585a4c | |||
| 1b9904eb5f | |||
| 4807264faa | |||
| 9f44977270 | |||
| 7f4423e703 | |||
| e9634f1ed4 | |||
| 37a543b9d6 | |||
| 9c11f11045 | |||
| a0b5713b61 | |||
| 5d4b9ca5e2 | |||
| 3bf5567397 | |||
| bbc9f06543 |
@@ -105,49 +105,24 @@ jobs:
|
||||
|
||||
- name: Install `rust` toolchain
|
||||
run: |
|
||||
## Install `rust` toolchain
|
||||
rustup toolchain install nightly --no-self-update -c rustfmt --profile minimal
|
||||
rustup default nightly
|
||||
|
||||
- name: "`grcov` ~ install"
|
||||
run: cargo install grcov
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- name: Generate code coverage
|
||||
run: cargo llvm-cov --all-features --lcov --branch --output-path lcov.info
|
||||
|
||||
- name: cargo test
|
||||
run: |
|
||||
cargo test --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"
|
||||
RUSTDOCFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cdebug-assertions=off"
|
||||
|
||||
- name: Generate coverage data
|
||||
id: grcov
|
||||
run: |
|
||||
grcov target/debug/ \
|
||||
--branch \
|
||||
--llvm \
|
||||
--source-dir . \
|
||||
--output-path lcov.info \
|
||||
--ignore-not-existing \
|
||||
--excl-line "#\\[derive\\(" \
|
||||
--excl-br-line "#\\[derive\\(" \
|
||||
--excl-start "#\\[cfg\\(test\\)\\]" \
|
||||
--excl-br-start "#\\[cfg\\(test\\)\\]" \
|
||||
--commit-sha ${{ github.sha }} \
|
||||
--service-job-id ${{ github.job }} \
|
||||
--service-name "GitHub Actions" \
|
||||
--service-number ${{ github.run_id }}
|
||||
- name: Upload coverage as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lcov.info
|
||||
# path: ${{ steps.grcov.outputs.report }}
|
||||
path: lcov.info
|
||||
|
||||
- name: Upload coverage to codecov.io
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
# file: ${{ steps.grcov.outputs.report }}
|
||||
file: lcov.info
|
||||
files: lcov.info
|
||||
fail_ci_if_error: true
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -4,6 +4,9 @@ name: External-testsuites
|
||||
|
||||
jobs:
|
||||
gnu-tests:
|
||||
permissions:
|
||||
actions: read
|
||||
|
||||
name: Run GNU findutils tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -17,7 +20,15 @@ jobs:
|
||||
repository: gnu-mirror-unofficial/findutils
|
||||
path: findutils.gnu
|
||||
ref: 5768a03ddfb5e18b1682e339d6cdd24ff721c510
|
||||
submodules: true
|
||||
submodules: false
|
||||
|
||||
- name: Override submodule URL and initialize submodules
|
||||
# Use github instead of upstream git server
|
||||
run: |
|
||||
git submodule sync --recursive
|
||||
git config submodule.gnulib.url https://github.com/coreutils/gnulib.git
|
||||
git submodule update --init --recursive --depth 1
|
||||
working-directory: findutils.gnu
|
||||
- name: Install `rust` toolchain
|
||||
run: |
|
||||
## Install `rust` toolchain
|
||||
@@ -27,7 +38,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
# Enable sources & install dependencies
|
||||
sudo find /etc/apt/sources.list* -type f -exec sed -i 'p; s/^deb /deb-src /' '{}' +
|
||||
sudo sed -i 's/^Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources
|
||||
sudo apt-get update
|
||||
sudo apt-get build-dep findutils
|
||||
- name: Run GNU tests
|
||||
@@ -38,6 +49,7 @@ jobs:
|
||||
- name: Extract testing info
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
- name: Upload gnu-test-report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -51,24 +63,43 @@ jobs:
|
||||
with:
|
||||
name: gnu-result
|
||||
path: gnu-result.json
|
||||
- name: Download the result
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
- name: Download artifacts (gnu-result and gnu-test-report)
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
workflow: compat.yml
|
||||
workflow_conclusion: completed
|
||||
name: gnu-result
|
||||
repo: uutils/findutils
|
||||
branch: main
|
||||
path: dl
|
||||
- name: Download the log
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
workflow: compat.yml
|
||||
workflow_conclusion: completed
|
||||
name: gnu-test-report
|
||||
repo: uutils/findutils
|
||||
branch: main
|
||||
path: dl
|
||||
script: |
|
||||
let fs = require('fs');
|
||||
fs.mkdirSync('${{ github.workspace }}/dl', { recursive: true });
|
||||
|
||||
async function downloadArtifact(artifactName) {
|
||||
// List all artifacts from the workflow run
|
||||
let artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.run_id }},
|
||||
});
|
||||
|
||||
// Find the specified artifact
|
||||
let matchArtifact = artifacts.data.artifacts.find((artifact) => artifact.name === artifactName);
|
||||
if (!matchArtifact) {
|
||||
throw new Error(`Artifact "${artifactName}" not found.`);
|
||||
}
|
||||
|
||||
// Download the artifact
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
|
||||
// Save the artifact to a file
|
||||
fs.writeFileSync(`${{ github.workspace }}/dl/${artifactName}.zip`, Buffer.from(download.data));
|
||||
}
|
||||
|
||||
// Download the required artifacts
|
||||
await downloadArtifact("gnu-result");
|
||||
await downloadArtifact("gnu-test-report");
|
||||
|
||||
- name: Compare failing tests against master
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -76,6 +107,8 @@ jobs:
|
||||
- name: Compare against main results
|
||||
shell: bash
|
||||
run: |
|
||||
unzip dl/gnu-result.zip -d dl/
|
||||
unzip dl/gnu-test-report.zip -d dl/
|
||||
mv dl/gnu-result.json latest-gnu-result.json
|
||||
python findutils/util/compare_gnu_result.py
|
||||
|
||||
@@ -92,7 +125,7 @@ jobs:
|
||||
with:
|
||||
repository: tavianator/bfs
|
||||
path: bfs
|
||||
ref: "3.1.3"
|
||||
ref: "4.0"
|
||||
- name: Install `rust` toolchain
|
||||
run: |
|
||||
## Install `rust` toolchain
|
||||
@@ -102,7 +135,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
# Enable sources & install dependencies
|
||||
sudo find /etc/apt/sources.list* -type f -exec sed -i 'p; s/^deb /deb-src /' '{}' +
|
||||
sudo sed -i 's/^Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources
|
||||
sudo apt-get update
|
||||
sudo apt-get build-dep bfs
|
||||
- name: Run BFS tests
|
||||
@@ -120,24 +153,42 @@ jobs:
|
||||
with:
|
||||
name: bfs-result
|
||||
path: bfs-result.json
|
||||
- name: Download the result
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
- name: Download artifacts (gnu-result and bfs-test-report)
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
workflow: compat.yml
|
||||
workflow_conclusion: completed
|
||||
name: bfs-result
|
||||
repo: uutils/findutils
|
||||
branch: main
|
||||
path: dl
|
||||
- name: Download the log
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
workflow: compat.yml
|
||||
workflow_conclusion: completed
|
||||
name: bfs-test-report
|
||||
repo: uutils/findutils
|
||||
branch: main
|
||||
path: dl
|
||||
script: |
|
||||
let fs = require('fs');
|
||||
fs.mkdirSync('${{ github.workspace }}/dl', { recursive: true });
|
||||
|
||||
async function downloadArtifact(artifactName) {
|
||||
// List all artifacts from the workflow run
|
||||
let artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.run_id }},
|
||||
});
|
||||
|
||||
// Find the specified artifact
|
||||
let matchArtifact = artifacts.data.artifacts.find((artifact) => artifact.name === artifactName);
|
||||
if (!matchArtifact) {
|
||||
throw new Error(`Artifact "${artifactName}" not found.`);
|
||||
}
|
||||
|
||||
// Download the artifact
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
|
||||
// Save the artifact to a file
|
||||
fs.writeFileSync(`${{ github.workspace }}/dl/${artifactName}.zip`, Buffer.from(download.data));
|
||||
}
|
||||
|
||||
// Download the required artifacts
|
||||
await downloadArtifact("bfs-result");
|
||||
await downloadArtifact("bfs-test-report");
|
||||
- name: Compare failing tests against main
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -145,6 +196,8 @@ jobs:
|
||||
- name: Compare against main results
|
||||
shell: bash
|
||||
run: |
|
||||
unzip dl/bfs-result.zip -d dl/
|
||||
unzip dl/bfs-test-report.zip -d dl/
|
||||
mv dl/bfs-result.json latest-bfs-result.json
|
||||
python findutils/util/compare_bfs_result.py
|
||||
|
||||
@@ -157,7 +210,7 @@ jobs:
|
||||
steps:
|
||||
- name: List Annotations
|
||||
uses: actions/github-script@v7
|
||||
|
||||
|
||||
with:
|
||||
script: |
|
||||
let runs = await github.rest.checks.listForRef({
|
||||
@@ -177,7 +230,7 @@ jobs:
|
||||
repo: context.repo.repo,
|
||||
check_run_id: result.id
|
||||
});
|
||||
|
||||
|
||||
run.data.forEach(data => {
|
||||
annotations.data.push({
|
||||
run: result.name,
|
||||
@@ -187,7 +240,7 @@ jobs:
|
||||
}
|
||||
|
||||
// Remove duplicate items.
|
||||
annotations.data = annotations.data.filter((value, index, self) =>
|
||||
annotations.data = annotations.data.filter((value, index, self) =>
|
||||
self.findIndex(v => v.annotation.message === value.annotation.message) === index);
|
||||
|
||||
let fs = require('fs');
|
||||
@@ -197,4 +250,4 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: comment
|
||||
path: annotations.json
|
||||
path: annotations.json
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
# Copyright 2022-2023, axodotdev
|
||||
# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/
|
||||
#
|
||||
# Copyright 2022-2024, axodotdev
|
||||
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||
#
|
||||
# CI that:
|
||||
#
|
||||
# * checks for a Git Tag that looks like a release
|
||||
# * builds artifacts with cargo-dist (archives, installers, hashes)
|
||||
# * builds artifacts with dist (archives, installers, hashes)
|
||||
# * uploads those artifacts to temporary workflow zip
|
||||
# * on success, uploads the artifacts to a Github Release
|
||||
# * on success, uploads the artifacts to a GitHub Release
|
||||
#
|
||||
# Note that the Github Release will be created with a generated
|
||||
# Note that the GitHub Release will be created with a generated
|
||||
# title/body based on your changelogs.
|
||||
|
||||
name: Release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
"contents": "write"
|
||||
|
||||
# This task will run whenever you push a git tag that looks like a version
|
||||
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
|
||||
@@ -23,30 +24,30 @@ permissions:
|
||||
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
|
||||
#
|
||||
# If PACKAGE_NAME is specified, then the announcement will be for that
|
||||
# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
|
||||
# package (erroring out if it doesn't have the given version or isn't dist-able).
|
||||
#
|
||||
# If PACKAGE_NAME isn't specified, then the announcement will be for all
|
||||
# (cargo-dist-able) packages in the workspace with that version (this mode is
|
||||
# (dist-able) packages in the workspace with that version (this mode is
|
||||
# intended for workspaces with only one dist-able package, or with all dist-able
|
||||
# packages versioned/released in lockstep).
|
||||
#
|
||||
# If you push multiple tags at once, separate instances of this workflow will
|
||||
# spin up, creating an independent announcement for each one. However Github
|
||||
# spin up, creating an independent announcement for each one. However, GitHub
|
||||
# will hard limit this to 3 tags per commit, as it will assume more tags is a
|
||||
# mistake.
|
||||
#
|
||||
# If there's a prerelease-style suffix to the version, then the release(s)
|
||||
# will be marked as a prerelease.
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- '**[0-9]+.[0-9]+.[0-9]+*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
# Run 'cargo dist plan' (or host) to determine what tasks we need to do
|
||||
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||
plan:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: "ubuntu-20.04"
|
||||
outputs:
|
||||
val: ${{ steps.plan.outputs.manifest }}
|
||||
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
|
||||
@@ -58,11 +59,16 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install cargo-dist
|
||||
- name: Install dist
|
||||
# we specify bash to get pipefail; it guards against the `curl` command
|
||||
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh"
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.28.0/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/dist
|
||||
# sure would be cool if github gave us proper conditionals...
|
||||
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
|
||||
# functionality based on whether this is a pull_request, and whether it's from a fork.
|
||||
@@ -70,8 +76,8 @@ jobs:
|
||||
# but also really annoying to build CI around when it needs secrets to work right.)
|
||||
- id: plan
|
||||
run: |
|
||||
cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||
echo "cargo dist ran successfully"
|
||||
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
cat plan-dist-manifest.json
|
||||
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
@@ -89,28 +95,38 @@ jobs:
|
||||
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# Target platforms/runners are computed by cargo-dist in create-release.
|
||||
# Target platforms/runners are computed by dist in create-release.
|
||||
# Each member of the matrix has the following arguments:
|
||||
#
|
||||
# - runner: the github runner
|
||||
# - dist-args: cli flags to pass to cargo dist
|
||||
# - install-dist: expression to run to install cargo-dist on the runner
|
||||
# - dist-args: cli flags to pass to dist
|
||||
# - install-dist: expression to run to install dist on the runner
|
||||
#
|
||||
# Typically there will be:
|
||||
# - 1 "global" task that builds universal installers
|
||||
# - N "local" tasks that build each platform's binaries and platform-specific installers
|
||||
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
container: ${{ matrix.container && matrix.container.image || null }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
|
||||
steps:
|
||||
- name: enable windows longpaths
|
||||
run: |
|
||||
git config --global core.longpaths true
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: swatinem/rust-cache@v2
|
||||
- name: Install cargo-dist
|
||||
run: ${{ matrix.install_dist }}
|
||||
- name: Install Rust non-interactively if not already installed
|
||||
if: ${{ matrix.container }}
|
||||
run: |
|
||||
if ! command -v cargo > /dev/null 2>&1; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
fi
|
||||
- name: Install dist
|
||||
run: ${{ matrix.install_dist.run }}
|
||||
# Get the dist-manifest
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -124,8 +140,8 @@ jobs:
|
||||
- name: Build artifacts
|
||||
run: |
|
||||
# Actually do builds and make zips and whatnot
|
||||
cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
|
||||
echo "cargo dist ran successfully"
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
- id: cargo-dist
|
||||
name: Post-build
|
||||
# We force bash here just because github makes it really hard to get values up
|
||||
@@ -135,7 +151,7 @@ jobs:
|
||||
run: |
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
@@ -160,9 +176,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install cargo-dist
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh"
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -173,12 +192,12 @@ jobs:
|
||||
- id: cargo-dist
|
||||
shell: bash
|
||||
run: |
|
||||
cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||
echo "cargo dist ran successfully"
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
@@ -206,8 +225,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install cargo-dist
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.0/cargo-dist-installer.sh | sh"
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Fetch artifacts from scratch-storage
|
||||
- name: Fetch artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -215,11 +238,10 @@ jobs:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
# This is a harmless no-op for Github Releases, hosting for that happens in "announce"
|
||||
- id: host
|
||||
shell: bash
|
||||
run: |
|
||||
cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||
echo "artifacts uploaded and released successfully"
|
||||
cat dist-manifest.json
|
||||
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
@@ -229,8 +251,29 @@ jobs:
|
||||
# Overwrite the previous copy
|
||||
name: artifacts-dist-manifest
|
||||
path: dist-manifest.json
|
||||
# Create a GitHub Release while uploading all files to it
|
||||
- name: "Download GitHub Artifacts"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
- name: Cleanup
|
||||
run: |
|
||||
# Remove the granular manifests
|
||||
rm -f artifacts/*-dist-manifest.json
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
|
||||
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
|
||||
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
|
||||
RELEASE_COMMIT: "${{ github.sha }}"
|
||||
run: |
|
||||
# Write and read notes from a file to avoid quoting breaking things
|
||||
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
|
||||
|
||||
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
|
||||
|
||||
# Create a Github Release while uploading all files to it
|
||||
announce:
|
||||
needs:
|
||||
- plan
|
||||
@@ -246,21 +289,3 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: "Download Github Artifacts"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
- name: Cleanup
|
||||
run: |
|
||||
# Remove the granular manifests
|
||||
rm -f artifacts/*-dist-manifest.json
|
||||
- name: Create Github Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: ${{ needs.plan.outputs.tag }}
|
||||
name: ${{ fromJson(needs.host.outputs.val).announcement_title }}
|
||||
body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }}
|
||||
prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }}
|
||||
artifacts: "artifacts/*"
|
||||
|
||||
Generated
+344
-198
File diff suppressed because it is too large
Load Diff
+23
-24
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "findutils"
|
||||
version = "0.6.0"
|
||||
version = "0.8.0"
|
||||
homepage = "https://github.com/uutils/findutils"
|
||||
repository = "https://github.com/uutils/findutils"
|
||||
edition = "2021"
|
||||
@@ -10,24 +10,24 @@ description = "Rust implementation of GNU findutils"
|
||||
authors = ["uutils developers"]
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.38"
|
||||
chrono = "0.4.40"
|
||||
clap = "4.5"
|
||||
faccess = "0.2.4"
|
||||
walkdir = "2.5"
|
||||
regex = "1.10"
|
||||
once_cell = "1.19"
|
||||
regex = "1.11"
|
||||
onig = { version = "6.4", default-features = false }
|
||||
uucore = { version = "0.0.27", features = ["entries", "fs", "fsext", "mode"] }
|
||||
uucore = { version = "0.0.30", features = ["entries", "fs", "fsext", "mode"] }
|
||||
nix = { version = "0.29", features = ["fs", "user"] }
|
||||
argmax = "0.3.1"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2"
|
||||
filetime = "0.2"
|
||||
nix = { version = "0.29", features = ["fs"] }
|
||||
predicates = "3"
|
||||
serial_test = "3.1"
|
||||
serial_test = "3.2"
|
||||
tempfile = "3"
|
||||
pretty_assertions = "1.4.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
|
||||
[[bin]]
|
||||
name = "find"
|
||||
@@ -46,20 +46,19 @@ path = "src/testing/commandline/main.rs"
|
||||
inherits = "release"
|
||||
lto = "thin"
|
||||
|
||||
# Config for 'cargo dist'
|
||||
[workspace.metadata.dist]
|
||||
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.12.0"
|
||||
# CI backends to support
|
||||
ci = ["github"]
|
||||
# The installers to generate for each app
|
||||
installers = []
|
||||
# Target platforms to build apps for (Rust target-triple syntax)
|
||||
targets = [
|
||||
"aarch64-apple-darwin",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-pc-windows-msvc",
|
||||
]
|
||||
# Publish jobs to run in CI
|
||||
pr-run-mode = "plan"
|
||||
|
||||
[lints.clippy]
|
||||
multiple_crate_versions = "allow"
|
||||
cargo_common_metadata = "allow"
|
||||
uninlined_format_args = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
|
||||
use_self = "warn"
|
||||
needless_pass_by_value = "warn"
|
||||
semicolon_if_nothing_returned = "warn"
|
||||
single_char_pattern = "warn"
|
||||
explicit_iter_loop = "warn"
|
||||
if_not_else = "warn"
|
||||
manual_let_else = "warn"
|
||||
# Disable for now, we have a few occurrences
|
||||
# panic = "warn"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[workspace]
|
||||
members = ["cargo:."]
|
||||
|
||||
# Config for 'dist'
|
||||
[dist]
|
||||
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.28.0"
|
||||
# CI backends to support
|
||||
ci = "github"
|
||||
# The installers to generate for each app
|
||||
installers = ["shell"]
|
||||
# Target platforms to build apps for (Rust target-triple syntax)
|
||||
targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
|
||||
# Which actions to run on pull requests
|
||||
pr-run-mode = "plan"
|
||||
# Path that installers should place binaries in
|
||||
install-path = "CARGO_HOME"
|
||||
# Whether to install an updater program
|
||||
install-updater = false
|
||||
@@ -5,9 +5,8 @@
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use faccess::PathExt;
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
/// Matcher for -{read,writ,execut}able.
|
||||
pub enum AccessMatcher {
|
||||
@@ -17,7 +16,7 @@ pub enum AccessMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for AccessMatcher {
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
let path = file_info.path();
|
||||
|
||||
match self {
|
||||
|
||||
+10
-12
@@ -7,32 +7,29 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use std::fs::{self, FileType};
|
||||
use std::fs;
|
||||
use std::io::{self, stderr, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
pub struct DeleteMatcher;
|
||||
|
||||
impl DeleteMatcher {
|
||||
pub fn new() -> Self {
|
||||
DeleteMatcher
|
||||
Self
|
||||
}
|
||||
|
||||
fn delete(&self, file_path: &Path, file_type: FileType) -> io::Result<()> {
|
||||
if file_type.is_dir() {
|
||||
fs::remove_dir(file_path)
|
||||
fn delete(&self, entry: &WalkEntry) -> io::Result<()> {
|
||||
if entry.file_type().is_dir() && !entry.path_is_symlink() {
|
||||
fs::remove_dir(entry.path())
|
||||
} else {
|
||||
fs::remove_file(file_path)
|
||||
fs::remove_file(entry.path())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for DeleteMatcher {
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
let path = file_info.path();
|
||||
let path_str = path.to_string_lossy();
|
||||
|
||||
@@ -43,9 +40,10 @@ impl Matcher for DeleteMatcher {
|
||||
return true;
|
||||
}
|
||||
|
||||
match self.delete(path, file_info.file_type()) {
|
||||
match self.delete(file_info) {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
matcher_io.set_exit_code(1);
|
||||
writeln!(&mut stderr(), "Failed to delete {path_str}: {e}").unwrap();
|
||||
false
|
||||
}
|
||||
|
||||
@@ -9,18 +9,18 @@ use std::{
|
||||
io::{stderr, Write},
|
||||
};
|
||||
|
||||
use super::Matcher;
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
pub struct EmptyMatcher;
|
||||
|
||||
impl EmptyMatcher {
|
||||
pub fn new() -> EmptyMatcher {
|
||||
EmptyMatcher
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for EmptyMatcher {
|
||||
fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
if file_info.file_type().is_file() {
|
||||
match file_info.metadata() {
|
||||
Ok(meta) => meta.len() == 0,
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
//! Paths encountered during a walk.
|
||||
|
||||
use std::cell::OnceCell;
|
||||
use std::error::Error;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::fs::{self, Metadata};
|
||||
use std::io::{self, ErrorKind};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::Follow;
|
||||
|
||||
/// Wrapper for a directory entry.
|
||||
#[derive(Debug)]
|
||||
enum Entry {
|
||||
/// Wraps an explicit path and depth.
|
||||
Explicit(PathBuf, usize),
|
||||
/// Wraps a WalkDir entry.
|
||||
WalkDir(DirEntry),
|
||||
}
|
||||
|
||||
/// File types.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum FileType {
|
||||
Unknown,
|
||||
Fifo,
|
||||
CharDevice,
|
||||
Directory,
|
||||
BlockDevice,
|
||||
Regular,
|
||||
Symlink,
|
||||
Socket,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
pub fn is_dir(self) -> bool {
|
||||
self == Self::Directory
|
||||
}
|
||||
|
||||
pub fn is_file(self) -> bool {
|
||||
self == Self::Regular
|
||||
}
|
||||
|
||||
pub fn is_symlink(self) -> bool {
|
||||
self == Self::Symlink
|
||||
}
|
||||
}
|
||||
|
||||
impl From<fs::FileType> for FileType {
|
||||
fn from(t: fs::FileType) -> Self {
|
||||
if t.is_dir() {
|
||||
return Self::Directory;
|
||||
}
|
||||
if t.is_file() {
|
||||
return Self::Regular;
|
||||
}
|
||||
if t.is_symlink() {
|
||||
return Self::Symlink;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if t.is_fifo() {
|
||||
return Self::Fifo;
|
||||
}
|
||||
if t.is_char_device() {
|
||||
return Self::CharDevice;
|
||||
}
|
||||
if t.is_block_device() {
|
||||
return Self::BlockDevice;
|
||||
}
|
||||
if t.is_socket() {
|
||||
return Self::Socket;
|
||||
}
|
||||
}
|
||||
|
||||
Self::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/// An error encountered while walking a file system.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WalkError {
|
||||
/// The path that caused the error, if known.
|
||||
path: Option<PathBuf>,
|
||||
/// The depth below the root path, if known.
|
||||
depth: Option<usize>,
|
||||
/// The io::Error::raw_os_error(), if known.
|
||||
raw: Option<i32>,
|
||||
}
|
||||
|
||||
impl WalkError {
|
||||
/// Get the path this error occurred on, if known.
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
self.path.as_deref()
|
||||
}
|
||||
|
||||
/// Get the traversal depth when this error occurred, if known.
|
||||
pub fn depth(&self) -> Option<usize> {
|
||||
self.depth
|
||||
}
|
||||
|
||||
/// Get the kind of I/O error.
|
||||
pub fn kind(&self) -> ErrorKind {
|
||||
io::Error::from(self).kind()
|
||||
}
|
||||
|
||||
/// Check for ErrorKind::{NotFound,NotADirectory}.
|
||||
pub fn is_not_found(&self) -> bool {
|
||||
if self.kind() == ErrorKind::NotFound {
|
||||
return true;
|
||||
}
|
||||
|
||||
// NotADirectory is nightly-only
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if self.raw == Some(uucore::libc::ENOTDIR) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check for ErrorKind::FilesystemLoop.
|
||||
pub fn is_loop(&self) -> bool {
|
||||
#[cfg(unix)]
|
||||
return self.raw == Some(uucore::libc::ELOOP);
|
||||
|
||||
#[cfg(not(unix))]
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for WalkError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
let ioe = io::Error::from(self);
|
||||
if let Some(path) = &self.path {
|
||||
write!(f, "{}: {}", path.display(), ioe)
|
||||
} else {
|
||||
write!(f, "{}", ioe)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for WalkError {}
|
||||
|
||||
impl From<io::Error> for WalkError {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Self::from(&e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&io::Error> for WalkError {
|
||||
fn from(e: &io::Error) -> Self {
|
||||
Self {
|
||||
path: None,
|
||||
depth: None,
|
||||
raw: e.raw_os_error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<walkdir::Error> for WalkError {
|
||||
fn from(e: walkdir::Error) -> Self {
|
||||
Self::from(&e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&walkdir::Error> for WalkError {
|
||||
fn from(e: &walkdir::Error) -> Self {
|
||||
Self {
|
||||
path: e.path().map(|p| p.to_owned()),
|
||||
depth: Some(e.depth()),
|
||||
raw: e.io_error().and_then(|e| e.raw_os_error()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WalkError> for io::Error {
|
||||
fn from(e: WalkError) -> Self {
|
||||
Self::from(&e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&WalkError> for io::Error {
|
||||
fn from(e: &WalkError) -> Self {
|
||||
e.raw
|
||||
.map(Self::from_raw_os_error)
|
||||
.unwrap_or_else(|| ErrorKind::Other.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A path encountered while walking a file system.
|
||||
#[derive(Debug)]
|
||||
pub struct WalkEntry {
|
||||
/// The wrapped path/dirent.
|
||||
inner: Entry,
|
||||
/// Whether to follow symlinks.
|
||||
follow: Follow,
|
||||
/// Cached metadata.
|
||||
meta: OnceCell<Result<Metadata, WalkError>>,
|
||||
}
|
||||
|
||||
impl WalkEntry {
|
||||
/// Create a new WalkEntry for a specific file.
|
||||
pub fn new(path: impl Into<PathBuf>, depth: usize, follow: Follow) -> Self {
|
||||
Self {
|
||||
inner: Entry::Explicit(path.into(), depth),
|
||||
follow,
|
||||
meta: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [walkdir::DirEntry] to a [WalkEntry]. Errors due to broken symbolic links will be
|
||||
/// converted to valid entries, but other errors will be propagated.
|
||||
pub fn from_walkdir(
|
||||
result: walkdir::Result<DirEntry>,
|
||||
follow: Follow,
|
||||
) -> Result<Self, WalkError> {
|
||||
let result = result.map_err(WalkError::from);
|
||||
|
||||
match result {
|
||||
Ok(entry) => {
|
||||
let ret = if entry.depth() == 0 && follow != Follow::Never {
|
||||
// DirEntry::file_type() is wrong for root symlinks when follow_root_links is set
|
||||
Self::new(entry.path(), 0, follow)
|
||||
} else {
|
||||
Self {
|
||||
inner: Entry::WalkDir(entry),
|
||||
follow,
|
||||
meta: OnceCell::new(),
|
||||
}
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
Err(e) if e.is_not_found() => {
|
||||
// Detect broken symlinks and replace them with explicit entries
|
||||
if let (Some(path), Some(depth)) = (e.path(), e.depth()) {
|
||||
if let Ok(meta) = path.symlink_metadata() {
|
||||
return Ok(Self {
|
||||
inner: Entry::Explicit(path.into(), depth),
|
||||
follow: Follow::Never,
|
||||
meta: Ok(meta).into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(e)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to this entry.
|
||||
pub fn path(&self) -> &Path {
|
||||
match &self.inner {
|
||||
Entry::Explicit(path, _) => path.as_path(),
|
||||
Entry::WalkDir(ent) => ent.path(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to this entry.
|
||||
pub fn into_path(self) -> PathBuf {
|
||||
match self.inner {
|
||||
Entry::Explicit(path, _) => path,
|
||||
Entry::WalkDir(ent) => ent.into_path(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the name of this entry.
|
||||
pub fn file_name(&self) -> &OsStr {
|
||||
match &self.inner {
|
||||
Entry::Explicit(path, _) => {
|
||||
// Path::file_name() only works if the last component is normal
|
||||
path.components()
|
||||
.next_back()
|
||||
.map(|c| c.as_os_str())
|
||||
.unwrap_or_else(|| path.as_os_str())
|
||||
}
|
||||
Entry::WalkDir(ent) => ent.file_name(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the depth of this entry below the root.
|
||||
pub fn depth(&self) -> usize {
|
||||
match &self.inner {
|
||||
Entry::Explicit(_, depth) => *depth,
|
||||
Entry::WalkDir(ent) => ent.depth(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get whether symbolic links are followed for this entry.
|
||||
pub fn follow(&self) -> bool {
|
||||
self.follow.follow_at_depth(self.depth())
|
||||
}
|
||||
|
||||
/// Get the metadata on a cache miss.
|
||||
fn get_metadata(&self) -> Result<Metadata, WalkError> {
|
||||
self.follow.metadata_at_depth(self.path(), self.depth())
|
||||
}
|
||||
|
||||
/// Get the [Metadata] for this entry, following symbolic links if appropriate.
|
||||
/// Multiple calls to this function will cache and re-use the same [Metadata].
|
||||
pub fn metadata(&self) -> Result<&Metadata, WalkError> {
|
||||
let result = self.meta.get_or_init(|| match &self.inner {
|
||||
Entry::Explicit(_, _) => Ok(self.get_metadata()?),
|
||||
Entry::WalkDir(ent) => Ok(ent.metadata()?),
|
||||
});
|
||||
result.as_ref().map_err(|e| e.clone())
|
||||
}
|
||||
|
||||
/// Get the file type of this entry.
|
||||
pub fn file_type(&self) -> FileType {
|
||||
match &self.inner {
|
||||
Entry::Explicit(_, _) => self
|
||||
.metadata()
|
||||
.map(|m| m.file_type().into())
|
||||
.unwrap_or(FileType::Unknown),
|
||||
Entry::WalkDir(ent) => ent.file_type().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether this entry is a symbolic link, regardless of whether links
|
||||
/// are being followed.
|
||||
pub fn path_is_symlink(&self) -> bool {
|
||||
match &self.inner {
|
||||
Entry::Explicit(path, _) => {
|
||||
if self.follow() {
|
||||
path.symlink_metadata()
|
||||
.is_ok_and(|m| m.file_type().is_symlink())
|
||||
} else {
|
||||
self.file_type().is_symlink()
|
||||
}
|
||||
}
|
||||
Entry::WalkDir(ent) => ent.path_is_symlink(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
-4
@@ -4,14 +4,14 @@
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::error::Error;
|
||||
use std::ffi::OsString;
|
||||
use std::io::{stderr, Write};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
enum Arg {
|
||||
FileArg(Vec<OsString>),
|
||||
@@ -52,7 +52,7 @@ impl SingleExecMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for SingleExecMatcher {
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
let mut command = Command::new(&self.executable);
|
||||
let path_to_file = if self.exec_in_parent_dir {
|
||||
if let Some(f) = file_info.path().file_name() {
|
||||
@@ -67,7 +67,7 @@ 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())),
|
||||
Arg::FileArg(ref parts) => command.arg(parts.join(path_to_file.as_os_str())),
|
||||
};
|
||||
}
|
||||
if self.exec_in_parent_dir {
|
||||
@@ -98,6 +98,125 @@ impl Matcher for SingleExecMatcher {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MultiExecMatcher {
|
||||
executable: String,
|
||||
args: Vec<OsString>,
|
||||
exec_in_parent_dir: bool,
|
||||
/// Command to build while matching.
|
||||
command: RefCell<Option<argmax::Command>>,
|
||||
}
|
||||
|
||||
impl MultiExecMatcher {
|
||||
pub fn new(
|
||||
executable: &str,
|
||||
args: &[&str],
|
||||
exec_in_parent_dir: bool,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
let transformed_args = args.iter().map(OsString::from).collect();
|
||||
|
||||
Ok(Self {
|
||||
executable: executable.to_string(),
|
||||
args: transformed_args,
|
||||
exec_in_parent_dir,
|
||||
command: RefCell::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
fn new_command(&self) -> argmax::Command {
|
||||
let mut command = argmax::Command::new(&self.executable);
|
||||
command.try_args(&self.args).unwrap();
|
||||
command
|
||||
}
|
||||
|
||||
fn run_command(&self, command: &mut argmax::Command, matcher_io: &mut MatcherIO) {
|
||||
match command.status() {
|
||||
Ok(status) => {
|
||||
if !status.success() {
|
||||
matcher_io.set_exit_code(1);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
writeln!(&mut stderr(), "Failed to run {}: {}", self.executable, e).unwrap();
|
||||
matcher_io.set_exit_code(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for MultiExecMatcher {
|
||||
fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
let path_to_file = if self.exec_in_parent_dir {
|
||||
if let Some(f) = file_info.path().file_name() {
|
||||
Path::new(".").join(f)
|
||||
} else {
|
||||
Path::new(".").join(file_info.path())
|
||||
}
|
||||
} else {
|
||||
file_info.path().to_path_buf()
|
||||
};
|
||||
let mut command = self.command.borrow_mut();
|
||||
let command = command.get_or_insert_with(|| self.new_command());
|
||||
|
||||
// Build command, or dispatch it before when it is long enough.
|
||||
if command.try_arg(&path_to_file).is_err() {
|
||||
if self.exec_in_parent_dir {
|
||||
match file_info.path().parent() {
|
||||
None => {
|
||||
// Root paths like "/" have no parent. Run them from the root to match GNU find.
|
||||
command.current_dir(file_info.path());
|
||||
}
|
||||
Some(parent) if parent == Path::new("") => {
|
||||
// Paths like "foo" have a parent of "". Avoid chdir("").
|
||||
}
|
||||
Some(parent) => {
|
||||
command.current_dir(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.run_command(command, matcher_io);
|
||||
|
||||
// Reset command status.
|
||||
*command = self.new_command();
|
||||
if let Err(e) = command.try_arg(&path_to_file) {
|
||||
writeln!(
|
||||
&mut stderr(),
|
||||
"Cannot fit a single argument {}: {}",
|
||||
&path_to_file.to_string_lossy(),
|
||||
e
|
||||
)
|
||||
.unwrap();
|
||||
matcher_io.set_exit_code(1);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn finished_dir(&self, dir: &Path, matcher_io: &mut MatcherIO) {
|
||||
// Dispatch command for -execdir.
|
||||
if self.exec_in_parent_dir {
|
||||
let mut command = self.command.borrow_mut();
|
||||
if let Some(mut command) = command.take() {
|
||||
command.current_dir(Path::new(".").join(dir));
|
||||
self.run_command(&mut command, matcher_io);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finished(&self, matcher_io: &mut MatcherIO) {
|
||||
// Dispatch command for -exec.
|
||||
if !self.exec_in_parent_dir {
|
||||
let mut command = self.command.borrow_mut();
|
||||
if let Some(mut command) = command.take() {
|
||||
self.run_command(&mut command, matcher_io);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_side_effects(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// No tests here, because we need to call out to an external executable. See
|
||||
/// `tests/exec_unit_tests.rs` instead.
|
||||
|
||||
+91
-35
@@ -2,19 +2,21 @@
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
#[cfg(unix)]
|
||||
use uucore::error::UResult;
|
||||
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{stderr, Write},
|
||||
};
|
||||
|
||||
use super::Matcher;
|
||||
/// The latest mapping from dev_id to fs_type, used for saving mount info reads
|
||||
#[cfg(unix)]
|
||||
pub struct Cache {
|
||||
dev_id: String,
|
||||
fs_type: String,
|
||||
}
|
||||
|
||||
/// Get the filesystem type of a file.
|
||||
/// 1. get the metadata of the file
|
||||
/// 2. get the device ID of the metadata
|
||||
/// 3. search the filesystem list
|
||||
/// 3. search the cache, then the filesystem list
|
||||
///
|
||||
/// Returns an empty string when no file system list matches.
|
||||
///
|
||||
@@ -24,23 +26,42 @@ use super::Matcher;
|
||||
///
|
||||
/// This is only supported on Unix.
|
||||
#[cfg(unix)]
|
||||
pub fn get_file_system_type(path: &Path) -> Result<String, Box<dyn Error>> {
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
io::{stderr, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn get_file_system_type(path: &Path, cache: &RefCell<Option<Cache>>) -> UResult<String> {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
let metadata = match path.metadata() {
|
||||
// use symlink_metadata (lstat under the hood) instead of metadata (stat) to make sure that it
|
||||
// does not return an error when there is a (broken) symlink; this is aligned with GNU find.
|
||||
let metadata = match path.symlink_metadata() {
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) => Err(err)?,
|
||||
};
|
||||
let dev_id = metadata.dev().to_string();
|
||||
let fs_list = match uucore::fsext::read_fs_list() {
|
||||
Ok(fs_list) => fs_list,
|
||||
Err(err) => Err(err)?,
|
||||
};
|
||||
|
||||
if let Some(cache) = cache.borrow().as_ref() {
|
||||
if cache.dev_id == dev_id {
|
||||
return Ok(cache.fs_type.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let fs_list = uucore::fsext::read_fs_list()?;
|
||||
let result = fs_list
|
||||
.into_iter()
|
||||
.find(|fs| fs.dev_id == dev_id)
|
||||
.map_or_else(String::new, |fs| fs.fs_type);
|
||||
|
||||
// cache the latest query if not a match before
|
||||
cache.replace(Some(Cache {
|
||||
dev_id,
|
||||
fs_type: result.clone(),
|
||||
}));
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -49,38 +70,49 @@ pub fn get_file_system_type(path: &Path) -> Result<String, Box<dyn Error>> {
|
||||
///
|
||||
/// This is only supported on Unix.
|
||||
pub struct FileSystemMatcher {
|
||||
#[cfg(unix)]
|
||||
fs_text: String,
|
||||
#[cfg(unix)]
|
||||
cache: RefCell<Option<Cache>>,
|
||||
}
|
||||
|
||||
impl FileSystemMatcher {
|
||||
#[cfg(unix)]
|
||||
pub fn new(fs_text: String) -> Self {
|
||||
Self { fs_text }
|
||||
Self {
|
||||
fs_text,
|
||||
cache: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub fn new(_fs_text: String) -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for FileSystemMatcher {
|
||||
fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool {
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
false
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
match get_file_system_type(file_info.path()) {
|
||||
Ok(result) => result == self.fs_text,
|
||||
Err(_) => {
|
||||
writeln!(
|
||||
&mut stderr(),
|
||||
"Error getting filesystem type for {}",
|
||||
file_info.path().to_string_lossy()
|
||||
)
|
||||
.unwrap();
|
||||
#[cfg(unix)]
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
match get_file_system_type(file_info.path(), &self.cache) {
|
||||
Ok(result) => result == self.fs_text,
|
||||
Err(_) => {
|
||||
writeln!(
|
||||
&mut stderr(),
|
||||
"Error getting filesystem type for {}",
|
||||
file_info.path().to_string_lossy()
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
false
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -89,9 +121,14 @@ mod tests {
|
||||
#[cfg(unix)]
|
||||
fn test_fs_matcher() {
|
||||
use crate::find::{
|
||||
matchers::{fs::get_file_system_type, tests::get_dir_entry_for, Matcher},
|
||||
matchers::{
|
||||
fs::{get_file_system_type, Cache},
|
||||
tests::get_dir_entry_for,
|
||||
Matcher,
|
||||
},
|
||||
tests::FakeDependencies,
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
use std::fs::File;
|
||||
use tempfile::Builder;
|
||||
|
||||
@@ -105,7 +142,26 @@ mod tests {
|
||||
let _ = File::create(foo_path).expect("create temp file");
|
||||
let file_info = get_dir_entry_for(&temp_dir.path().to_string_lossy(), "foo");
|
||||
|
||||
let target_fs_type = get_file_system_type(file_info.path()).unwrap();
|
||||
// create an empty cache for initial fs type lookup
|
||||
let empty_cache = RefCell::new(None);
|
||||
let target_fs_type = get_file_system_type(file_info.path(), &empty_cache).unwrap();
|
||||
|
||||
// should work with unmatched cache, and the cache should be set to the last query result
|
||||
let unmatched_cache = RefCell::new(Some(Cache {
|
||||
dev_id: "foo".to_string(),
|
||||
fs_type: "bar".to_string(),
|
||||
}));
|
||||
let target_fs_type_unmatched_cache =
|
||||
get_file_system_type(file_info.path(), &unmatched_cache).unwrap();
|
||||
assert_eq!(
|
||||
target_fs_type, target_fs_type_unmatched_cache,
|
||||
"get_file_system_type should return correct result with unmatched cache"
|
||||
);
|
||||
assert_eq!(
|
||||
unmatched_cache.borrow().as_ref().unwrap().fs_type,
|
||||
target_fs_type,
|
||||
"get_file_system_type should set the cache to the last query result"
|
||||
);
|
||||
|
||||
// should match fs type
|
||||
let matcher = super::FileSystemMatcher::new(target_fs_type.clone());
|
||||
|
||||
+31
-21
@@ -112,7 +112,7 @@ fn extract_bracket_expr(pattern: &str) -> Option<(String, &str)> {
|
||||
}
|
||||
|
||||
/// Converts a POSIX glob into a POSIX Basic Regular Expression
|
||||
fn glob_to_regex(pattern: &str) -> String {
|
||||
fn glob_to_regex(pattern: &str) -> Option<String> {
|
||||
let mut regex = String::new();
|
||||
|
||||
let mut chars = pattern.chars();
|
||||
@@ -130,9 +130,9 @@ fn glob_to_regex(pattern: &str) -> String {
|
||||
// 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
|
||||
// Most implementations return FNM_NOMATCH in this case, so create a pattern that
|
||||
// never matches.
|
||||
return "$.".to_string();
|
||||
return None;
|
||||
}
|
||||
}
|
||||
'[' => {
|
||||
@@ -147,12 +147,12 @@ fn glob_to_regex(pattern: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
regex
|
||||
Some(regex)
|
||||
}
|
||||
|
||||
/// An fnmatch()-style glob matcher.
|
||||
pub struct Pattern {
|
||||
regex: Regex,
|
||||
regex: Option<Regex>,
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
@@ -165,13 +165,13 @@ impl Pattern {
|
||||
};
|
||||
|
||||
// As long as glob_to_regex() is correct, this should never fail
|
||||
let regex = parse_bre(&glob_to_regex(pattern), options).unwrap();
|
||||
let regex = glob_to_regex(pattern).map(|r| parse_bre(&r, options).unwrap());
|
||||
Self { regex }
|
||||
}
|
||||
|
||||
/// Test if this pattern matches a string.
|
||||
pub fn matches(&self, string: &str) -> bool {
|
||||
self.regex.is_match(string)
|
||||
self.regex.as_ref().is_some_and(|r| r.is_match(string))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,47 +179,52 @@ impl Pattern {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[track_caller]
|
||||
fn assert_glob_regex(glob: &str, regex: &str) {
|
||||
assert_eq!(glob_to_regex(glob).as_deref(), Some(regex));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literals() {
|
||||
assert_eq!(glob_to_regex(r"foo.bar"), r"foo\.bar");
|
||||
assert_glob_regex(r"foo.bar", r"foo\.bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regex_special() {
|
||||
assert_eq!(glob_to_regex(r"^foo.bar$"), r"\^foo\.bar\$");
|
||||
assert_glob_regex(r"^foo.bar$", r"\^foo\.bar\$");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wildcards() {
|
||||
assert_eq!(glob_to_regex(r"foo?bar*baz"), r"foo.bar.*baz");
|
||||
assert_glob_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"$.");
|
||||
assert_glob_regex(r"fo\o\?bar\*baz\\", r"foo?bar\*baz\\");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_brackets() {
|
||||
assert_eq!(glob_to_regex(r"foo[bar][!baz]"), r"foo[bar][^baz]");
|
||||
assert_glob_regex(r"foo[bar][!baz]", r"foo[bar][^baz]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_brackets() {
|
||||
assert_eq!(
|
||||
glob_to_regex(r"[!]!.*[\[.].][=]=][:space:]-]"),
|
||||
r"[^]!.*[\[.].][=]=][:space:]-]"
|
||||
assert_glob_regex(
|
||||
r"[!]!.*[\[.].][=]=][:space:]-]",
|
||||
r"[^]!.*[\[.].][=]=][:space:]-]",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_brackets() {
|
||||
assert_eq!(glob_to_regex(r"foo[bar[!baz"), r"foo\[bar\[!baz");
|
||||
assert_glob_regex(r"foo[bar[!baz", r"foo\[bar\[!baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_escape() {
|
||||
assert_eq!(glob_to_regex(r"foo\"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -235,4 +240,9 @@ mod tests {
|
||||
|
||||
assert!(!Pattern::new(r"foo*BAR", true).matches("BAR--foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_escape_matches() {
|
||||
assert!(!Pattern::new(r"foo\", false).matches("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
+27
-55
@@ -3,7 +3,7 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use super::Matcher;
|
||||
use super::{ComparableValue, Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
#[cfg(unix)]
|
||||
use nix::unistd::Group;
|
||||
@@ -11,67 +11,43 @@ use nix::unistd::Group;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
pub struct GroupMatcher {
|
||||
gid: Option<u32>,
|
||||
gid: ComparableValue,
|
||||
}
|
||||
|
||||
impl GroupMatcher {
|
||||
#[cfg(unix)]
|
||||
pub fn from_group_name(group: &str) -> GroupMatcher {
|
||||
pub fn from_group_name(group: &str) -> Option<Self> {
|
||||
// get gid from group name
|
||||
let Ok(group) = Group::from_name(group) else {
|
||||
return GroupMatcher { gid: None };
|
||||
};
|
||||
|
||||
let Some(group) = group else {
|
||||
// This if branch is to determine whether a certain group exists in the system.
|
||||
// If a certain group does not exist in the system,
|
||||
// the result will need to be returned according to
|
||||
// the flag bit of whether to invert the result.
|
||||
return GroupMatcher { gid: None };
|
||||
};
|
||||
|
||||
GroupMatcher {
|
||||
gid: Some(group.gid.as_raw()),
|
||||
}
|
||||
let group = Group::from_name(group).ok()??;
|
||||
let gid = group.gid.as_raw();
|
||||
Some(Self::from_gid(gid))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn from_gid(gid: u32) -> GroupMatcher {
|
||||
GroupMatcher { gid: Some(gid) }
|
||||
pub fn from_gid(gid: u32) -> Self {
|
||||
Self::from_comparable(ComparableValue::EqualTo(gid as u64))
|
||||
}
|
||||
|
||||
pub fn from_comparable(gid: ComparableValue) -> Self {
|
||||
Self { gid }
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn from_group_name(_group: &str) -> GroupMatcher {
|
||||
GroupMatcher { gid: None }
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn from_gid(_gid: u32) -> GroupMatcher {
|
||||
GroupMatcher { gid: None }
|
||||
}
|
||||
|
||||
pub fn gid(&self) -> &Option<u32> {
|
||||
&self.gid
|
||||
pub fn from_group_name(_group: &str) -> Option<Self> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for GroupMatcher {
|
||||
#[cfg(unix)]
|
||||
fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool {
|
||||
let Ok(metadata) = file_info.path().metadata() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let file_gid = metadata.gid();
|
||||
|
||||
// When matching the -group parameter in find/matcher/mod.rs,
|
||||
// it has been judged that the group does not exist and an error is returned.
|
||||
// So use unwarp() directly here.
|
||||
self.gid.unwrap() == file_gid
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
match file_info.metadata() {
|
||||
Ok(metadata) => self.gid.matches(metadata.gid().into()),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn matches(&self, _file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool {
|
||||
fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
// The user group acquisition function for Windows systems is not implemented in MetadataExt,
|
||||
// so it is somewhat difficult to implement it. :(
|
||||
false
|
||||
@@ -82,14 +58,14 @@ pub struct NoGroupMatcher {}
|
||||
|
||||
impl Matcher for NoGroupMatcher {
|
||||
#[cfg(unix)]
|
||||
fn matches(&self, file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
use nix::unistd::Gid;
|
||||
|
||||
if file_info.path().is_symlink() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Ok(metadata) = file_info.path().metadata() else {
|
||||
let Ok(metadata) = file_info.metadata() else {
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -105,7 +81,7 @@ impl Matcher for NoGroupMatcher {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn matches(&self, _file_info: &walkdir::DirEntry, _: &mut super::MatcherIO) -> bool {
|
||||
fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -130,13 +106,14 @@ mod tests {
|
||||
let foo_path = temp_dir.path().join("foo");
|
||||
let _ = File::create(foo_path).expect("create temp file");
|
||||
let file_info = get_dir_entry_for(&temp_dir.path().to_string_lossy(), "foo");
|
||||
let file_gid = file_info.path().metadata().unwrap().gid();
|
||||
let file_gid = file_info.metadata().unwrap().gid();
|
||||
let file_group = Group::from_gid(Gid::from_raw(file_gid))
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.name;
|
||||
|
||||
let matcher = super::GroupMatcher::from_group_name(file_group.as_str());
|
||||
let matcher =
|
||||
super::GroupMatcher::from_group_name(file_group.as_str()).expect("group should exist");
|
||||
assert!(
|
||||
matcher.matches(&file_info, &mut matcher_io),
|
||||
"group should match"
|
||||
@@ -146,18 +123,13 @@ mod tests {
|
||||
let time_string = Local::now().format("%Y%m%d%H%M%S").to_string();
|
||||
let matcher = GroupMatcher::from_group_name(time_string.as_str());
|
||||
assert!(
|
||||
matcher.gid().is_none(),
|
||||
matcher.is_none(),
|
||||
"group name {} should not exist",
|
||||
time_string
|
||||
);
|
||||
|
||||
// Testing group id
|
||||
let matcher = GroupMatcher::from_gid(file_gid);
|
||||
assert!(
|
||||
matcher.gid().is_some(),
|
||||
"group id {} should exist",
|
||||
file_gid
|
||||
);
|
||||
assert!(
|
||||
matcher.matches(&file_info, &mut matcher_io),
|
||||
"group id should match"
|
||||
|
||||
@@ -7,12 +7,10 @@
|
||||
use std::io::{stderr, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::glob::Pattern;
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
fn read_link_target(file_info: &DirEntry) -> Option<PathBuf> {
|
||||
fn read_link_target(file_info: &WalkEntry) -> Option<PathBuf> {
|
||||
match file_info.path().read_link() {
|
||||
Ok(target) => Some(target),
|
||||
Err(err) => {
|
||||
@@ -40,14 +38,14 @@ pub struct LinkNameMatcher {
|
||||
}
|
||||
|
||||
impl LinkNameMatcher {
|
||||
pub fn new(pattern_string: &str, caseless: bool) -> LinkNameMatcher {
|
||||
pub fn new(pattern_string: &str, caseless: bool) -> Self {
|
||||
let pattern = Pattern::new(pattern_string, caseless);
|
||||
Self { pattern }
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for LinkNameMatcher {
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
if let Some(target) = read_link_target(file_info) {
|
||||
self.pattern.matches(&target.to_string_lossy())
|
||||
} else {
|
||||
|
||||
@@ -11,9 +11,8 @@
|
||||
//! to "-foo -o ( -bar -baz )", not "( -foo -o -bar ) -baz").
|
||||
use std::error::Error;
|
||||
use std::path::Path;
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use super::{Matcher, MatcherIO};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
/// This matcher contains a collection of other matchers. A file only matches
|
||||
/// if it matches ALL the contained sub-matchers. For sub-matchers that have
|
||||
@@ -33,7 +32,7 @@ impl Matcher for AndMatcher {
|
||||
/// Returns true if all sub-matchers return true. Short-circuiting does take
|
||||
/// 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 {
|
||||
fn matches(&self, dir_entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
for matcher in &self.submatchers {
|
||||
if !matcher.matches(dir_entry, matcher_io) {
|
||||
return false;
|
||||
@@ -52,15 +51,15 @@ impl Matcher for AndMatcher {
|
||||
.any(super::Matcher::has_side_effects)
|
||||
}
|
||||
|
||||
fn finished_dir(&self, dir: &Path) {
|
||||
fn finished_dir(&self, dir: &Path, matcher_io: &mut MatcherIO) {
|
||||
for m in &self.submatchers {
|
||||
m.finished_dir(dir);
|
||||
m.finished_dir(dir, matcher_io);
|
||||
}
|
||||
}
|
||||
|
||||
fn finished(&self) {
|
||||
fn finished(&self, matcher_io: &mut MatcherIO) {
|
||||
for m in &self.submatchers {
|
||||
m.finished();
|
||||
m.finished(matcher_io);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,7 +108,7 @@ impl Matcher for OrMatcher {
|
||||
/// Returns true if any sub-matcher returns true. Short-circuiting does take
|
||||
/// 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 {
|
||||
fn matches(&self, dir_entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
for matcher in &self.submatchers {
|
||||
if matcher.matches(dir_entry, matcher_io) {
|
||||
return true;
|
||||
@@ -128,15 +127,15 @@ impl Matcher for OrMatcher {
|
||||
.any(super::Matcher::has_side_effects)
|
||||
}
|
||||
|
||||
fn finished_dir(&self, dir: &Path) {
|
||||
fn finished_dir(&self, dir: &Path, matcher_io: &mut MatcherIO) {
|
||||
for m in &self.submatchers {
|
||||
m.finished_dir(dir);
|
||||
m.finished_dir(dir, matcher_io);
|
||||
}
|
||||
}
|
||||
|
||||
fn finished(&self) {
|
||||
fn finished(&self, matcher_io: &mut MatcherIO) {
|
||||
for m in &self.submatchers {
|
||||
m.finished();
|
||||
m.finished(matcher_io);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,7 +205,7 @@ impl ListMatcher {
|
||||
impl Matcher for ListMatcher {
|
||||
/// Calls matches on all submatcher objects, with no short-circuiting.
|
||||
/// Returns the result of the call to the final submatcher
|
||||
fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, dir_entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
let mut rc = false;
|
||||
for matcher in &self.submatchers {
|
||||
rc = matcher.matches(dir_entry, matcher_io);
|
||||
@@ -223,15 +222,15 @@ impl Matcher for ListMatcher {
|
||||
.any(super::Matcher::has_side_effects)
|
||||
}
|
||||
|
||||
fn finished_dir(&self, dir: &Path) {
|
||||
fn finished_dir(&self, dir: &Path, matcher_io: &mut MatcherIO) {
|
||||
for m in &self.submatchers {
|
||||
m.finished_dir(dir);
|
||||
m.finished_dir(dir, matcher_io);
|
||||
}
|
||||
}
|
||||
|
||||
fn finished(&self) {
|
||||
fn finished(&self, matcher_io: &mut MatcherIO) {
|
||||
for m in &self.submatchers {
|
||||
m.finished();
|
||||
m.finished(matcher_io);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,7 +310,7 @@ impl ListMatcherBuilder {
|
||||
pub struct TrueMatcher;
|
||||
|
||||
impl Matcher for TrueMatcher {
|
||||
fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, _dir_entry: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -320,7 +319,7 @@ impl Matcher for TrueMatcher {
|
||||
pub struct FalseMatcher;
|
||||
|
||||
impl Matcher for FalseMatcher {
|
||||
fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, _dir_entry: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -339,7 +338,7 @@ impl NotMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for NotMatcher {
|
||||
fn matches(&self, dir_entry: &DirEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, dir_entry: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
!self.submatcher.matches(dir_entry, matcher_io)
|
||||
}
|
||||
|
||||
@@ -347,17 +346,16 @@ impl Matcher for NotMatcher {
|
||||
self.submatcher.has_side_effects()
|
||||
}
|
||||
|
||||
fn finished_dir(&self, dir: &Path) {
|
||||
self.submatcher.finished_dir(dir);
|
||||
fn finished_dir(&self, dir: &Path, matcher_io: &mut MatcherIO) {
|
||||
self.submatcher.finished_dir(dir, matcher_io);
|
||||
}
|
||||
|
||||
fn finished(&self) {
|
||||
self.submatcher.finished();
|
||||
fn finished(&self, matcher_io: &mut MatcherIO) {
|
||||
self.submatcher.finished(matcher_io);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::find::matchers::quit::QuitMatcher;
|
||||
@@ -370,7 +368,7 @@ mod tests {
|
||||
pub struct HasSideEffects;
|
||||
|
||||
impl Matcher for HasSideEffects {
|
||||
fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, _: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -383,7 +381,7 @@ mod tests {
|
||||
struct Counter(Rc<RefCell<u32>>);
|
||||
|
||||
impl Matcher for Counter {
|
||||
fn matches(&self, _: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, _: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
*self.0.borrow_mut() += 1;
|
||||
true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
// This file is part of the uutils findutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use chrono::DateTime;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{stderr, Write},
|
||||
};
|
||||
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
#[cfg(unix)]
|
||||
fn format_permissions(mode: uucore::libc::mode_t) -> String {
|
||||
let file_type = match mode & (uucore::libc::S_IFMT as uucore::libc::mode_t) {
|
||||
uucore::libc::S_IFDIR => "d",
|
||||
uucore::libc::S_IFREG => "-",
|
||||
_ => "?",
|
||||
};
|
||||
|
||||
// S_$$USR means "user permissions"
|
||||
let user_perms = format!(
|
||||
"{}{}{}",
|
||||
if mode & uucore::libc::S_IRUSR != 0 {
|
||||
"r"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if mode & uucore::libc::S_IWUSR != 0 {
|
||||
"w"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if mode & uucore::libc::S_IXUSR != 0 {
|
||||
"x"
|
||||
} else {
|
||||
"-"
|
||||
}
|
||||
);
|
||||
|
||||
// S_$$GRP means "group permissions"
|
||||
let group_perms = format!(
|
||||
"{}{}{}",
|
||||
if mode & uucore::libc::S_IRGRP != 0 {
|
||||
"r"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if mode & uucore::libc::S_IWGRP != 0 {
|
||||
"w"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if mode & uucore::libc::S_IXGRP != 0 {
|
||||
"x"
|
||||
} else {
|
||||
"-"
|
||||
}
|
||||
);
|
||||
|
||||
// S_$$OTH means "other permissions"
|
||||
let other_perms = format!(
|
||||
"{}{}{}",
|
||||
if mode & uucore::libc::S_IROTH != 0 {
|
||||
"r"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if mode & uucore::libc::S_IWOTH != 0 {
|
||||
"w"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if mode & uucore::libc::S_IXOTH != 0 {
|
||||
"x"
|
||||
} else {
|
||||
"-"
|
||||
}
|
||||
);
|
||||
|
||||
format!("{}{}{}{}", file_type, user_perms, group_perms, other_perms)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn format_permissions(file_attributes: u32) -> String {
|
||||
let mut attributes = Vec::new();
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
|
||||
if file_attributes & 0x0001 != 0 {
|
||||
attributes.push("read-only");
|
||||
}
|
||||
if file_attributes & 0x0002 != 0 {
|
||||
attributes.push("hidden");
|
||||
}
|
||||
if file_attributes & 0x0004 != 0 {
|
||||
attributes.push("system");
|
||||
}
|
||||
if file_attributes & 0x0020 != 0 {
|
||||
attributes.push("archive");
|
||||
}
|
||||
if file_attributes & 0x0040 != 0 {
|
||||
attributes.push("compressed");
|
||||
}
|
||||
if file_attributes & 0x0080 != 0 {
|
||||
attributes.push("offline");
|
||||
}
|
||||
|
||||
attributes.join(", ")
|
||||
}
|
||||
|
||||
pub struct Ls {
|
||||
output_file: Option<File>,
|
||||
}
|
||||
|
||||
impl Ls {
|
||||
pub fn new(output_file: Option<File>) -> Self {
|
||||
Self { output_file }
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn print(
|
||||
&self,
|
||||
file_info: &WalkEntry,
|
||||
matcher_io: &mut MatcherIO,
|
||||
mut out: impl Write,
|
||||
print_error_message: bool,
|
||||
) {
|
||||
use nix::unistd::{Gid, Group, Uid, User};
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||
|
||||
let metadata = file_info.metadata().unwrap();
|
||||
|
||||
let inode_number = metadata.ino();
|
||||
let number_of_blocks = {
|
||||
let size = metadata.size();
|
||||
let number_of_blocks = size / 1024;
|
||||
let remainder = number_of_blocks % 4;
|
||||
|
||||
if remainder == 0 {
|
||||
if number_of_blocks == 0 {
|
||||
4
|
||||
} else {
|
||||
number_of_blocks
|
||||
}
|
||||
} else {
|
||||
number_of_blocks + (4 - (remainder))
|
||||
}
|
||||
};
|
||||
let permission =
|
||||
{ format_permissions(metadata.permissions().mode() as uucore::libc::mode_t) };
|
||||
let hard_links = metadata.nlink();
|
||||
let user = {
|
||||
let uid = metadata.uid();
|
||||
User::from_uid(Uid::from_raw(uid)).unwrap().unwrap().name
|
||||
};
|
||||
let group = {
|
||||
let gid = metadata.gid();
|
||||
Group::from_gid(Gid::from_raw(gid)).unwrap().unwrap().name
|
||||
};
|
||||
let size = metadata.size();
|
||||
let last_modified = {
|
||||
let system_time = metadata.modified().unwrap();
|
||||
let now_utc: DateTime<chrono::Utc> = system_time.into();
|
||||
now_utc.format("%b %e %H:%M")
|
||||
};
|
||||
let path = file_info.path().to_string_lossy();
|
||||
|
||||
match writeln!(
|
||||
out,
|
||||
" {:<4} {:>6} {:<10} {:>3} {:<8} {:<8} {:>8} {} {}",
|
||||
inode_number,
|
||||
number_of_blocks,
|
||||
permission,
|
||||
hard_links,
|
||||
user,
|
||||
group,
|
||||
size,
|
||||
last_modified,
|
||||
path,
|
||||
) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
if print_error_message {
|
||||
writeln!(
|
||||
&mut stderr(),
|
||||
"Error writing {:?} for {}",
|
||||
file_info.path().to_string_lossy(),
|
||||
e
|
||||
)
|
||||
.unwrap();
|
||||
matcher_io.set_exit_code(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn print(
|
||||
&self,
|
||||
file_info: &WalkEntry,
|
||||
matcher_io: &mut MatcherIO,
|
||||
mut out: impl Write,
|
||||
print_error_message: bool,
|
||||
) {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
|
||||
let metadata = file_info.metadata().unwrap();
|
||||
|
||||
let inode_number = 0;
|
||||
let number_of_blocks = {
|
||||
let size = metadata.file_size();
|
||||
let number_of_blocks = size / 1024;
|
||||
let remainder = number_of_blocks % 4;
|
||||
|
||||
if remainder == 0 {
|
||||
if number_of_blocks == 0 {
|
||||
4
|
||||
} else {
|
||||
number_of_blocks
|
||||
}
|
||||
} else {
|
||||
number_of_blocks + (4 - (remainder))
|
||||
}
|
||||
};
|
||||
let permission = { format_permissions(metadata.file_attributes()) };
|
||||
let hard_links = 0;
|
||||
let user = 0;
|
||||
let group = 0;
|
||||
let size = metadata.file_size();
|
||||
let last_modified = {
|
||||
let system_time = metadata.modified().unwrap();
|
||||
let now_utc: DateTime<chrono::Utc> = system_time.into();
|
||||
now_utc.format("%b %e %H:%M")
|
||||
};
|
||||
let path = file_info.path().to_string_lossy();
|
||||
|
||||
match write!(
|
||||
out,
|
||||
" {:<4} {:>6} {:<10} {:>3} {:<8} {:<8} {:>8} {} {}\n",
|
||||
inode_number,
|
||||
number_of_blocks,
|
||||
permission,
|
||||
hard_links,
|
||||
user,
|
||||
group,
|
||||
size,
|
||||
last_modified,
|
||||
path,
|
||||
) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
if print_error_message {
|
||||
writeln!(
|
||||
&mut stderr(),
|
||||
"Error writing {:?} for {}",
|
||||
file_info.path().to_string_lossy(),
|
||||
e
|
||||
)
|
||||
.unwrap();
|
||||
matcher_io.set_exit_code(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for Ls {
|
||||
fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
if let Some(file) = &self.output_file {
|
||||
self.print(file_info, matcher_io, file, true);
|
||||
} else {
|
||||
self.print(
|
||||
file_info,
|
||||
matcher_io,
|
||||
&mut *matcher_io.deps.get_output().borrow_mut(),
|
||||
false,
|
||||
);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn has_side_effects(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_format_permissions() {
|
||||
use super::format_permissions;
|
||||
|
||||
let mode: uucore::libc::mode_t = 0o100644;
|
||||
let expected = "-rw-r--r--";
|
||||
assert_eq!(format_permissions(mode), expected);
|
||||
|
||||
let mode: uucore::libc::mode_t = 0o040755;
|
||||
let expected = "drwxr-xr-x";
|
||||
assert_eq!(format_permissions(mode), expected);
|
||||
|
||||
let mode: uucore::libc::mode_t = 0o100777;
|
||||
let expected = "-rwxrwxrwx";
|
||||
assert_eq!(format_permissions(mode), expected);
|
||||
}
|
||||
}
|
||||
+472
-126
File diff suppressed because it is too large
Load Diff
@@ -4,10 +4,8 @@
|
||||
// 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};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
/// This matcher makes a comparison of the name against a shell wildcard
|
||||
/// pattern. See `glob::Pattern` for details on the exact syntax.
|
||||
@@ -23,8 +21,17 @@ impl NameMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for NameMatcher {
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
let name = file_info.file_name().to_string_lossy();
|
||||
|
||||
#[cfg(unix)]
|
||||
if name.len() > 1 && name.chars().all(|x| x == '/') {
|
||||
self.pattern.matches("/")
|
||||
} else {
|
||||
self.pattern.matches(&name)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
self.pattern.matches(&name)
|
||||
}
|
||||
}
|
||||
@@ -34,7 +41,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::find::matchers::tests::get_dir_entry_for;
|
||||
use crate::find::tests::FakeDependencies;
|
||||
|
||||
use std::io::ErrorKind;
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -128,4 +134,22 @@ mod tests {
|
||||
let deps = FakeDependencies::new();
|
||||
assert!(matcher.matches(&link_f, &mut deps.new_matcher_io()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn slash_match_returns_true() {
|
||||
let dir_to_match = get_dir_entry_for("///", "");
|
||||
let matcher = NameMatcher::new("/", true);
|
||||
let deps = FakeDependencies::new();
|
||||
assert!(matcher.matches(&dir_to_match, &mut deps.new_matcher_io()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn only_one_slash() {
|
||||
let dir_to_match = get_dir_entry_for("/", "");
|
||||
let matcher = NameMatcher::new("/", false);
|
||||
let deps = FakeDependencies::new();
|
||||
assert!(matcher.matches(&dir_to_match, &mut deps.new_matcher_io()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
// 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};
|
||||
use super::{Matcher, MatcherIO, WalkEntry};
|
||||
|
||||
/// This matcher makes a comparison of the path against a shell wildcard
|
||||
/// pattern. See `glob::Pattern` for details on the exact syntax.
|
||||
@@ -23,7 +21,7 @@ impl PathMatcher {
|
||||
}
|
||||
|
||||
impl Matcher for PathMatcher {
|
||||
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
let path = file_info.path().to_string_lossy();
|
||||
self.pattern.matches(&path)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user