userland: search and exec tools, spawn library (T14)
rg (ripgrep 15.2), grep (ours, on ripgrep's regex crates), find and xargs (ours), sed (uutils 0.3 wrapper), diff and cmp (uutils diffutils 0.6 with -r), fd 10.5, jaq 2.3, env and timeout. userland/spawn is a small Command-like client of the process interface. search-test.nu: 42 cases match native tools.
This commit is contained in:
Generated
+1198
-44
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,7 +1,7 @@
|
||||
# The default CLIs of a world, built for wasm32-wasip2. Its own workspace, outside the host's.
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["git", "coreutils", "procps", "cwd-init/probe"]
|
||||
members = ["git", "coreutils", "procps", "cwd-init/probe", "ripgrep", "diffutils", "jaq", "sed", "grep", "fd", "spawn", "findutils", "exec-tools"]
|
||||
|
||||
# WASI compatibility patches: git/vendor/PATCHES.md.
|
||||
[patch.crates-io]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"v":1}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "815ed7d8de89a236e5f636749f80a5b061017521"
|
||||
},
|
||||
"path_in_vcs": ""
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
name: GnuComment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["GnuTests"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
post-comment:
|
||||
permissions:
|
||||
actions: read # to list workflow runs artifacts
|
||||
pull-requests: write # to comment on pr
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.workflow_run.event == 'pull_request'
|
||||
steps:
|
||||
- name: 'Download artifact'
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
// List all artifacts from GnuTests
|
||||
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }},
|
||||
});
|
||||
|
||||
// Download the "comment" artifact, which contains a PR number (NR) and result.txt
|
||||
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "comment"
|
||||
})[0];
|
||||
|
||||
if (!matchArtifact) {
|
||||
console.log('No comment artifact found');
|
||||
return;
|
||||
}
|
||||
|
||||
var download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
var fs = require('fs');
|
||||
fs.writeFileSync('${{ github.workspace }}/comment.zip', Buffer.from(download.data));
|
||||
|
||||
- run: unzip comment.zip || echo "Failed to unzip comment artifact"
|
||||
|
||||
- name: 'Comment on PR'
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
|
||||
// Check if files exist
|
||||
if (!fs.existsSync('./NR')) {
|
||||
console.log('No NR file found, skipping comment');
|
||||
return;
|
||||
}
|
||||
if (!fs.existsSync('./result.txt')) {
|
||||
console.log('No result.txt file found, skipping comment');
|
||||
return;
|
||||
}
|
||||
|
||||
var issue_number = Number(fs.readFileSync('./NR'));
|
||||
var content = fs.readFileSync('./result.txt');
|
||||
|
||||
if (content.toString().trim().length > 7) { // 7 because we have backquote + \n
|
||||
// Update existing comment if present, otherwise create a new one
|
||||
var marker = '<!-- gnu-tests-bot -->';
|
||||
var body = marker + '\nGNU diffutils testsuite comparison:\n```\n' + content + '```';
|
||||
var comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
});
|
||||
var existing = comments.data.filter(c => c.body.includes(marker))[0];
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body: body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue_number,
|
||||
body: body,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log('Comment content too short, skipping');
|
||||
}
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
name: GnuTests
|
||||
|
||||
# Run GNU diffutils testsuite against the Rust diffutils implementation
|
||||
# and compare results against the main branch to catch regressions
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write # Publish diffutils instead of discarding
|
||||
|
||||
# End the current execution if there is a new changeset in the PR
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
TEST_FULL_SUMMARY_FILE: 'diffutils-gnu-full-result.json'
|
||||
|
||||
jobs:
|
||||
native:
|
||||
name: Run GNU diffutils testsuite
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
### Build
|
||||
- name: Build Rust diffutils binary
|
||||
shell: bash
|
||||
run: |
|
||||
## Build Rust diffutils binary
|
||||
cargo build --config=profile.release.strip=true --profile=release
|
||||
zstd -19 target/release/diffutils -o diffutils-x86_64-unknown-linux-gnu.zst
|
||||
|
||||
- name: Publish latest commit
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
with:
|
||||
tag_name: latest-commit
|
||||
body: |
|
||||
commit: ${{ github.sha }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
files: |
|
||||
diffutils-x86_64-unknown-linux-gnu.zst
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
### Run tests
|
||||
- name: Run GNU diffutils testsuite
|
||||
shell: bash
|
||||
run: |
|
||||
## Run GNU diffutils testsuite
|
||||
# Exit code 1 means some tests failed, which is expected and handled
|
||||
# by the comparison below; 2 means the suite couldn't be run at all
|
||||
# 'shell: bash' runs with -e, so don't let the expected exit code 1
|
||||
# abort the step before the exit code is inspected
|
||||
result=0
|
||||
./tests/run-upstream-testsuite.sh release || result=$?
|
||||
if [[ $result -ge 2 ]]; then
|
||||
echo "::error ::The GNU testsuite could not be run (exit code $result); see the log above"
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
TERM: xterm
|
||||
|
||||
- name: Upload full json results
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: diffutils-gnu-full-result
|
||||
path: tests/test-results.json
|
||||
if-no-files-found: warn
|
||||
|
||||
aggregate:
|
||||
needs: [native]
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: read
|
||||
name: Aggregate GNU test results
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Initialize workflow variables
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
## VARs setup
|
||||
outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; }
|
||||
|
||||
TEST_SUMMARY_FILE='diffutils-gnu-result.json'
|
||||
outputs TEST_SUMMARY_FILE
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Retrieve reference artifacts
|
||||
uses: dawidd6/action-download-artifact@v27
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: GnuTests.yml
|
||||
branch: "${{ env.DEFAULT_BRANCH }}"
|
||||
workflow_conclusion: completed
|
||||
path: "reference"
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Download full json results
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: diffutils-gnu-full-result
|
||||
path: results
|
||||
|
||||
- name: Extract/summarize testing info
|
||||
id: summary
|
||||
shell: bash
|
||||
run: |
|
||||
## Extract/summarize testing info
|
||||
outputs() { step_id="${{ github.action }}"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; }
|
||||
|
||||
RESULT_FILE="results/test-results.json"
|
||||
if [[ ! -f "$RESULT_FILE" ]]; then
|
||||
echo "::error ::Missing test results at $RESULT_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! jq -e . "$RESULT_FILE" > /dev/null; then
|
||||
echo "::error ::Test results at $RESULT_FILE are not valid JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOTAL=$(jq '[.tests[]] | length' "$RESULT_FILE")
|
||||
PASS=$(jq '[.tests[] | select(.result=="PASS")] | length' "$RESULT_FILE")
|
||||
FAIL=$(jq '[.tests[] | select(.result=="FAIL")] | length' "$RESULT_FILE")
|
||||
SKIP=$(jq '[.tests[] | select(.result=="SKIP")] | length' "$RESULT_FILE")
|
||||
ERROR=0
|
||||
|
||||
if [[ "$TOTAL" -eq 0 ]]; then
|
||||
echo "::error ::No test was run; refusing to report or compare an empty test run"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
output="GNU diffutils tests summary = TOTAL: $TOTAL / PASS: $PASS / FAIL: $FAIL / SKIP: $SKIP"
|
||||
echo "${output}"
|
||||
|
||||
if [[ "$FAIL" -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 error "$ERROR" \
|
||||
'{($date): { sha: $sha, total: $total, pass: $pass, skip: $skip, fail: $fail, error: $error }}' > '${{ steps.vars.outputs.TEST_SUMMARY_FILE }}'
|
||||
|
||||
HASH=$(sha1sum '${{ steps.vars.outputs.TEST_SUMMARY_FILE }}' | cut --delim=" " -f 1)
|
||||
outputs HASH TOTAL PASS FAIL SKIP
|
||||
|
||||
- name: Upload SHA1/ID of 'test-summary'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: "${{ steps.summary.outputs.HASH }}"
|
||||
path: "${{ steps.vars.outputs.TEST_SUMMARY_FILE }}"
|
||||
|
||||
- name: Upload test results summary
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: test-summary
|
||||
path: "${{ steps.vars.outputs.TEST_SUMMARY_FILE }}"
|
||||
|
||||
- name: Compare test failures VS reference
|
||||
shell: bash
|
||||
run: |
|
||||
## Compare test failures VS reference
|
||||
REF_SUMMARY_FILE='reference/diffutils-gnu-full-result/test-results.json'
|
||||
CURRENT_SUMMARY_FILE="results/test-results.json"
|
||||
|
||||
IGNORE_INTERMITTENT=".github/workflows/ignore-intermittent.txt"
|
||||
|
||||
# Build the comment in a directory of its own: 'reference/' holds the
|
||||
# artifacts downloaded from the reference run, including its own
|
||||
# 'comment' artifact, and re-uploading those would post a comparison
|
||||
# belonging to another run (see https://github.com/uutils/diffutils/pull/262)
|
||||
COMMENT_DIR="comment"
|
||||
rm -rf ${COMMENT_DIR}
|
||||
mkdir -p ${COMMENT_DIR}
|
||||
echo ${{ github.event.number }} > ${COMMENT_DIR}/NR
|
||||
COMMENT_LOG="${COMMENT_DIR}/result.txt"
|
||||
: > "${COMMENT_LOG}"
|
||||
|
||||
COMPARISON_RESULT=0
|
||||
if test -f "${CURRENT_SUMMARY_FILE}"; then
|
||||
if test -s "${REF_SUMMARY_FILE}"; then
|
||||
echo "Reference summary SHA1/ID: $(sha1sum -- "${REF_SUMMARY_FILE}")"
|
||||
echo "Current summary SHA1/ID: $(sha1sum -- "${CURRENT_SUMMARY_FILE}")"
|
||||
|
||||
python3 util/compare_test_results.py \
|
||||
--ignore-file "${IGNORE_INTERMITTENT}" \
|
||||
--output "${COMMENT_LOG}" \
|
||||
"${CURRENT_SUMMARY_FILE}" "${REF_SUMMARY_FILE}"
|
||||
|
||||
COMPARISON_RESULT=$?
|
||||
else
|
||||
echo "::warning ::Skipping test comparison; no usable reference summary is available at '${REF_SUMMARY_FILE}'."
|
||||
fi
|
||||
else
|
||||
echo "::error ::Failed to find summary of test results (missing '${CURRENT_SUMMARY_FILE}'); failing early"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ${COMPARISON_RESULT} -eq 1 ]; then
|
||||
echo "::error ::Found new non-intermittent test failures"
|
||||
exit 1
|
||||
elif [ ${COMPARISON_RESULT} -ge 2 ]; then
|
||||
# The comparison itself failed (e.g. an unusable reference summary).
|
||||
# Don't post a comment rather than post a misleading one.
|
||||
: > "${COMMENT_LOG}"
|
||||
echo "::warning ::Could not compare the test results against the reference"
|
||||
else
|
||||
echo "::notice ::No new test failures detected"
|
||||
fi
|
||||
|
||||
- name: Upload comparison log (for GnuComment workflow)
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comment
|
||||
path: comment/
|
||||
|
||||
- name: Report test results
|
||||
if: success() || failure()
|
||||
shell: bash
|
||||
run: |
|
||||
## Report final results
|
||||
echo "::notice ::GNU diffutils testsuite results:"
|
||||
echo "::notice :: Total tests: ${{ steps.summary.outputs.TOTAL }}"
|
||||
echo "::notice :: Passed: ${{ steps.summary.outputs.PASS }}"
|
||||
echo "::notice :: Failed: ${{ steps.summary.outputs.FAIL }}"
|
||||
echo "::notice :: Skipped: ${{ steps.summary.outputs.SKIP }}"
|
||||
|
||||
if [[ "${{ steps.summary.outputs.FAIL }}" -gt 0 ]]; then
|
||||
PASS_RATE=$(( ${{ steps.summary.outputs.PASS }} * 100 / (${{ steps.summary.outputs.PASS }} + ${{ steps.summary.outputs.FAIL }}) ))
|
||||
echo "::notice :: Pass rate: ${PASS_RATE}%"
|
||||
fi
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
name: Security audit
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rustsec/audit-check@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
on: [push, pull_request]
|
||||
|
||||
name: Basic CI
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: cargo check
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- run: cargo check
|
||||
|
||||
test:
|
||||
name: cargo test
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: install GNU patch on MacOS
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew install gpatch
|
||||
- name: set up PATH on Windows
|
||||
# Needed to use GNU's patch.exe instead of Strawberry Perl patch
|
||||
if: runner.os == 'Windows'
|
||||
run: echo "C:\Program Files\Git\usr\bin" >> $env:GITHUB_PATH
|
||||
- run: cargo test
|
||||
|
||||
fmt:
|
||||
name: cargo fmt --all -- --check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
name: cargo clippy -- -D warnings
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- run: cargo clippy -- -D warnings
|
||||
|
||||
coverage:
|
||||
name: Code Coverage
|
||||
env:
|
||||
RUSTC_BOOTSTRAP: 1
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { os: ubuntu-latest , features: unix }
|
||||
- { os: macos-latest , features: macos }
|
||||
- { os: windows-latest , features: windows }
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Initialize workflow variables
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
## VARs setup
|
||||
outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; }
|
||||
# target-specific options
|
||||
# * CODECOV_FLAGS
|
||||
CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' )
|
||||
outputs CODECOV_FLAGS
|
||||
|
||||
- name: install GNU patch on MacOS
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew install gpatch
|
||||
- name: set up PATH on Windows
|
||||
# Needed to use GNU's patch.exe instead of Strawberry Perl patch
|
||||
if: runner.os == 'Windows'
|
||||
run: echo "C:\Program Files\Git\usr\bin" >> $env:GITHUB_PATH
|
||||
- name: Test
|
||||
run: cargo test --all-features --no-fail-fast
|
||||
env:
|
||||
RUSTC_WRAPPER: ""
|
||||
RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
|
||||
RUSTDOCFLAGS: "-Cpanic=abort"
|
||||
LLVM_PROFILE_FILE: "diffutils-%p-%m.profraw"
|
||||
- name: "`grcov` ~ install"
|
||||
id: build_grcov
|
||||
shell: bash
|
||||
run: |
|
||||
git clone --depth=1 https://github.com/mozilla/grcov.git ~/grcov/
|
||||
cd ~/grcov
|
||||
# Hardcode the version of crossbeam-epoch. See
|
||||
# https://github.com/uutils/coreutils/issues/3680
|
||||
sed -i -e "s|tempfile =|crossbeam-epoch = \"=0.9.8\"\ntempfile =|" Cargo.toml
|
||||
cargo install --path .
|
||||
cd -
|
||||
# Uncomment when the upstream issue
|
||||
# https://github.com/mozilla/grcov/issues/849 is fixed
|
||||
# uses: actions-rs/install@v0.1
|
||||
# with:
|
||||
# crate: grcov
|
||||
# version: latest
|
||||
# use-tool-cache: false
|
||||
- name: Generate coverage data (via `grcov`)
|
||||
id: coverage
|
||||
shell: bash
|
||||
run: |
|
||||
## Generate coverage data
|
||||
COVERAGE_REPORT_DIR="target/debug"
|
||||
COVERAGE_REPORT_FILE="${COVERAGE_REPORT_DIR}/lcov.info"
|
||||
mkdir -p "${COVERAGE_REPORT_DIR}"
|
||||
# display coverage files
|
||||
grcov . --output-type files --binary-path "${COVERAGE_REPORT_DIR}" | sort --unique
|
||||
# generate coverage report
|
||||
grcov . --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --binary-path "${COVERAGE_REPORT_DIR}" --branch
|
||||
echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT
|
||||
- name: Upload coverage results (to Codecov.io)
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ${{ steps.coverage.outputs.report }}
|
||||
## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }}
|
||||
flags: ${{ steps.vars.outputs.CODECOV_FLAGS }}
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: false
|
||||
@@ -0,0 +1,37 @@
|
||||
name: CodSpeed
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
pull_request:
|
||||
# `workflow_dispatch` allows CodSpeed to trigger backtest
|
||||
# performance analysis in order to generate initial data.
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
codspeed:
|
||||
name: Run benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup rust toolchain, cache and cargo-codspeed binary
|
||||
uses: moonrepo/setup-rust@v1
|
||||
with:
|
||||
channel: stable
|
||||
cache-target: release
|
||||
bins: cargo-codspeed
|
||||
|
||||
- name: Build the benchmark target(s)
|
||||
run: cargo codspeed build -m simulation
|
||||
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@v5
|
||||
with:
|
||||
mode: simulation
|
||||
run: cargo codspeed run
|
||||
@@ -0,0 +1,82 @@
|
||||
name: Fuzzing
|
||||
|
||||
# spell-checker:ignore fuzzer
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUSTC_BOOTSTRAP: 1
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
fuzz-build:
|
||||
name: Build the fuzzers
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Install `cargo-fuzz`
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-fuzz
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: "cargo-fuzz-cache-key"
|
||||
cache-directories: "fuzz/target"
|
||||
- name: Run `cargo-fuzz build`
|
||||
# https://github.com/rust-fuzz/cargo-fuzz/issues/398
|
||||
run: cargo fuzz build --target host-tuple
|
||||
|
||||
fuzz-run:
|
||||
needs: fuzz-build
|
||||
name: Run the fuzzers
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
RUN_FOR: 60
|
||||
strategy:
|
||||
matrix:
|
||||
test-target:
|
||||
- { name: fuzz_cmp, should_pass: true }
|
||||
- { name: fuzz_cmp_args, should_pass: true }
|
||||
- { name: fuzz_ed, should_pass: true }
|
||||
- { name: fuzz_normal, should_pass: true }
|
||||
- { name: fuzz_patch, should_pass: true }
|
||||
- { name: fuzz_side, should_pass: true }
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Install `cargo-fuzz`
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-fuzz
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: "cargo-fuzz-cache-key"
|
||||
cache-directories: "fuzz/target"
|
||||
- name: Restore Cached Corpus
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
key: corpus-cache-${{ matrix.test-target.name }}
|
||||
path: |
|
||||
fuzz/corpus/${{ matrix.test-target.name }}
|
||||
- name: Run ${{ matrix.test-target.name }} for XX seconds
|
||||
shell: bash
|
||||
continue-on-error: ${{ !matrix.test-target.should_pass }}
|
||||
run: |
|
||||
cargo fuzz run --target host-tuple ${{ matrix.test-target.name }} -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
|
||||
- name: Save Corpus Cache
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
key: corpus-cache-${{ matrix.test-target.name }}
|
||||
path: |
|
||||
fuzz/corpus/${{ matrix.test-target.name }}
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
# This file was autogenerated by dist: https://axodotdev.github.io/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 dist (archives, installers, hashes)
|
||||
# * uploads those artifacts to temporary workflow zip
|
||||
# * on success, uploads the artifacts to a GitHub Release
|
||||
#
|
||||
# Note that the GitHub Release will be created with a generated
|
||||
# title/body based on your changelogs.
|
||||
|
||||
name: Release
|
||||
permissions:
|
||||
"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.
|
||||
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
|
||||
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
|
||||
# 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 dist-able).
|
||||
#
|
||||
# If PACKAGE_NAME isn't specified, then the announcement will be for all
|
||||
# (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
|
||||
# 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]+*'
|
||||
|
||||
jobs:
|
||||
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||
plan:
|
||||
runs-on: "ubuntu-22.04"
|
||||
outputs:
|
||||
val: ${{ steps.plan.outputs.manifest }}
|
||||
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
|
||||
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
|
||||
publishing: ${{ !github.event.pull_request }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- 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.33.0/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@v7
|
||||
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.
|
||||
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
|
||||
# but also really annoying to build CI around when it needs secrets to work right.)
|
||||
- id: plan
|
||||
run: |
|
||||
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"
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-plan-dist-manifest
|
||||
path: plan-dist-manifest.json
|
||||
|
||||
# Build and packages all the platform-specific things
|
||||
build-local-artifacts:
|
||||
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
|
||||
# Let the initial task tell us to not run (currently very blunt)
|
||||
needs:
|
||||
- plan
|
||||
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 dist in create-release.
|
||||
# Each member of the matrix has the following arguments:
|
||||
#
|
||||
# - runner: the github 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@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- 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@v8
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
${{ matrix.packages_install }}
|
||||
- name: Build artifacts
|
||||
run: |
|
||||
# Actually do builds and make zips and whatnot
|
||||
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
|
||||
# to "real" actions without writing to env-vars, and writing to env-vars has
|
||||
# inconsistent syntax between shell and powershell.
|
||||
shell: bash
|
||||
run: |
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$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"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
|
||||
path: |
|
||||
${{ steps.cargo-dist.outputs.paths }}
|
||||
${{ env.BUILD_MANIFEST_NAME }}
|
||||
|
||||
# Build and package all the platform-agnostic(ish) things
|
||||
build-global-artifacts:
|
||||
needs:
|
||||
- plan
|
||||
- build-local-artifacts
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v8
|
||||
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@v8
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- id: cargo-dist
|
||||
shell: bash
|
||||
run: |
|
||||
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 ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-build-global
|
||||
path: |
|
||||
${{ steps.cargo-dist.outputs.paths }}
|
||||
${{ env.BUILD_MANIFEST_NAME }}
|
||||
# Determines if we should publish/announce
|
||||
host:
|
||||
needs:
|
||||
- plan
|
||||
- build-local-artifacts
|
||||
- build-global-artifacts
|
||||
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
|
||||
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
outputs:
|
||||
val: ${{ steps.host.outputs.manifest }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v8
|
||||
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@v8
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- id: host
|
||||
shell: bash
|
||||
run: |
|
||||
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"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
# 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@v8
|
||||
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/*
|
||||
|
||||
announce:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
# use "always() && ..." to allow us to wait for all publish jobs while
|
||||
# still allowing individual publish jobs to skip themselves (for prereleases).
|
||||
# "host" however must run to completion, no skipping allowed!
|
||||
if: ${{ always() && needs.host.result == 'success' }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# spell-checker:ignore wasip
|
||||
name: WASI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# End the current execution if there is a new changeset in the PR.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
test_wasi:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-wasip1
|
||||
- name: check
|
||||
run: cargo check --target wasm32-wasip1
|
||||
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
*.swp
|
||||
/tests/test-results.json
|
||||
@@ -0,0 +1,48 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
exclude: ^tests/fixtures/
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-json
|
||||
exclude: '\.vscode/(cSpell|extensions)\.json' # cSpell.json and extensions.json use comments
|
||||
- id: check-shebang-scripts-are-executable
|
||||
exclude: '.+\.rs' # would be triggered by #![some_attribute]
|
||||
- id: check-symlinks
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
args: [ --allow-multiple-documents ]
|
||||
- id: destroyed-symlinks
|
||||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
args: [ --fix=lf ]
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: rust-linting
|
||||
name: Rust linting
|
||||
description: Run cargo fmt on files included in the commit.
|
||||
entry: cargo +stable 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 +stable clippy --workspace --all-targets --all-features -- -D warnings
|
||||
pass_filenames: false
|
||||
types: [file, rust]
|
||||
language: system
|
||||
- id: cspell
|
||||
name: Code spell checker (cspell)
|
||||
description: Run cspell to check for spelling errors (if available).
|
||||
entry: bash -c 'if command -v cspell >/dev/null 2>&1; then cspell --no-must-find-files -- "$@"; else echo "cspell not found, skipping spell check"; exit 0; fi' --
|
||||
pass_filenames: true
|
||||
language: system
|
||||
|
||||
ci:
|
||||
skip: [rust-linting, rust-clippy, cspell]
|
||||
@@ -0,0 +1,32 @@
|
||||
# Contributing to diffutils
|
||||
|
||||
Hi! Welcome to uutils/diffutils, and thanks for wanting to contribute!
|
||||
|
||||
This project follows the shared conventions of the [uutils](https://github.com/uutils)
|
||||
organization. Before opening a pull request, please read:
|
||||
|
||||
- Our **[Review Guidelines](https://uutils.github.io/reviews/)** — what we expect
|
||||
from a pull request and how reviews are carried out.
|
||||
- Our community's [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md), if present.
|
||||
|
||||
Finally, feel free to join our [Discord](https://discord.gg/wQVJbvJ)!
|
||||
|
||||
> [!WARNING]
|
||||
> uutils is original code and cannot contain any code from GNU or other
|
||||
> strongly-licensed (GPL/LGPL) implementations. We **cannot** accept changes
|
||||
> based on the GNU source code, and you **must not link** to it either. You may
|
||||
> look at permissively-licensed implementations (MIT/BSD) and read the GNU
|
||||
> *manuals* — never the GNU *source*.
|
||||
|
||||
## In short
|
||||
|
||||
- Discuss non-trivial changes in an issue **before** writing the code.
|
||||
- Keep pull requests **small, self-contained, and descriptively titled**
|
||||
(e.g. `diffutils: fix ...`).
|
||||
- Make sure CI passes: tests are green, `rustfmt` is satisfied, and there are
|
||||
no `clippy` warnings.
|
||||
- Add tests for new behavior; don't let coverage regress.
|
||||
- Write small, atomic commits annotated with the component you touched.
|
||||
|
||||
See the [Review Guidelines](https://uutils.github.io/reviews/) for the full
|
||||
details.
|
||||
@@ -0,0 +1,8 @@
|
||||
Copyright (c) Michael Howell
|
||||
Copyright (c) uutils developers
|
||||
|
||||
Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
<LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
option. All files in the project carrying such notice may not be
|
||||
copied, modified, or distributed except according to those terms.
|
||||
Generated
+917
@@ -0,0 +1,917 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assert_cmd"
|
||||
version = "2.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"bstr",
|
||||
"libc",
|
||||
"predicates",
|
||||
"predicates-core",
|
||||
"predicates-tree",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"terminal_size",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "codspeed"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e7c21807b937b8e535f2745cd83c525fc250524021cd662b9ba4d3e399bb504"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cc",
|
||||
"colored",
|
||||
"getrandom",
|
||||
"glob",
|
||||
"nix",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"statrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codspeed-divan-compat"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc7e48c4e555e60b894144b14df1e893a7317b9f4000b1492ace3041479b12cc"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"codspeed",
|
||||
"codspeed-divan-compat-macros",
|
||||
"codspeed-divan-compat-walltime",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codspeed-divan-compat-macros"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1eb62a7a8d4cc6b79b18dcbac670b3202f4c8c901ca8ea701ab42190f0b14799"
|
||||
dependencies = [
|
||||
"divan-macros",
|
||||
"itertools",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codspeed-divan-compat-walltime"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15c4e77a97571b6214b976cee7e2a564215d0a5bee9520a3cdab86106d0b31a3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"clap",
|
||||
"codspeed",
|
||||
"condtype",
|
||||
"divan-macros",
|
||||
"libc",
|
||||
"regex-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "condtype"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "difflib"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
|
||||
|
||||
[[package]]
|
||||
name = "diffutils"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"assert_cmd",
|
||||
"chrono",
|
||||
"codspeed-divan-compat",
|
||||
"diff",
|
||||
"itoa",
|
||||
"predicates",
|
||||
"pretty_assertions",
|
||||
"rand",
|
||||
"regex",
|
||||
"same-file",
|
||||
"tempfile",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "divan-macros"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8dc51d98e636f5e3b0759a39257458b22619cac7e96d932da6eeb052891bb67c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "float-cmp"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.184"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "normalize-line-endings"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "3.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"difflib",
|
||||
"float-cmp",
|
||||
"normalize-line-endings",
|
||||
"predicates-core",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "predicates-core"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144"
|
||||
|
||||
[[package]]
|
||||
name = "predicates-tree"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2"
|
||||
dependencies = [
|
||||
"predicates-core",
|
||||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||
dependencies = [
|
||||
"diff",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-lite"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "statrs"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termtree"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.11+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
@@ -0,0 +1,91 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "diffutils"
|
||||
version = "0.6.0"
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "A CLI app for generating diff files"
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/uutils/diffutils"
|
||||
|
||||
[features]
|
||||
feat_bench_not_cmp = []
|
||||
feat_bench_not_diff = []
|
||||
|
||||
[lib]
|
||||
name = "diffutilslib"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "diffutils"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[test]]
|
||||
name = "integration"
|
||||
path = "tests/integration.rs"
|
||||
|
||||
[[bench]]
|
||||
name = "bench_diffutils"
|
||||
path = "benches/bench-diffutils.rs"
|
||||
harness = false
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4.38"
|
||||
|
||||
[dependencies.diff]
|
||||
version = "0.1.13"
|
||||
|
||||
[dependencies.itoa]
|
||||
version = "1.0.11"
|
||||
|
||||
[dependencies.regex]
|
||||
version = "1.10.4"
|
||||
|
||||
[dependencies.same-file]
|
||||
version = "1.0.6"
|
||||
|
||||
[dependencies.unicode-width]
|
||||
version = "0.2.0"
|
||||
|
||||
[dev-dependencies.assert_cmd]
|
||||
version = "2.0.14"
|
||||
|
||||
[dev-dependencies.divan]
|
||||
version = "6.0.0"
|
||||
package = "codspeed-divan-compat"
|
||||
|
||||
[dev-dependencies.predicates]
|
||||
version = "3.1.0"
|
||||
|
||||
[dev-dependencies.pretty_assertions]
|
||||
version = "1.4.0"
|
||||
|
||||
[dev-dependencies.rand]
|
||||
version = "0.10.0"
|
||||
|
||||
[dev-dependencies.tempfile]
|
||||
version = "3.26.0"
|
||||
|
||||
[profile.dist]
|
||||
inherits = "release"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
Generated
+52
@@ -0,0 +1,52 @@
|
||||
[package]
|
||||
name = "diffutils"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
description = "A CLI app for generating diff files"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/uutils/diffutils"
|
||||
|
||||
[lib]
|
||||
name = "diffutilslib"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "diffutils"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.38"
|
||||
diff = "0.1.13"
|
||||
itoa = "1.0.11"
|
||||
regex = "1.10.4"
|
||||
same-file = "1.0.6"
|
||||
unicode-width = "0.2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.14"
|
||||
divan = { version = "6.0.0", package = "codspeed-divan-compat" }
|
||||
pretty_assertions = "1.4.0"
|
||||
predicates = "3.1.0"
|
||||
rand = "0.10.0"
|
||||
tempfile = "3.26.0"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
# alias profile for 'dist'
|
||||
[profile.dist]
|
||||
inherits = "release"
|
||||
|
||||
[[bench]]
|
||||
name = "bench_diffutils"
|
||||
path = "benches/bench-diffutils.rs"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
# default = ["feat_bench_not_diff"]
|
||||
# Turn bench for diffutils cmp off
|
||||
feat_bench_not_cmp = []
|
||||
# Turn bench for diffutils diff off
|
||||
feat_bench_not_diff = []
|
||||
@@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -0,0 +1,23 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Recursive directory comparison
|
||||
|
||||
Source: crates.io `diffutils` 0.6.0, https://github.com/uutils/diffutils.
|
||||
The vendor copy retains the upstream licenses.
|
||||
|
||||
The public library exposes comparison functions but not the diff CLI entry point.
|
||||
The vendor copy retains that CLI parser and dispatcher.
|
||||
|
||||
- `src/params.rs` accepts `-r` and `--recursive`.
|
||||
- `src/diff.rs` recursively compares directory entries in lexical order.
|
||||
- The recursive path reuses the upstream file comparison implementation.
|
||||
- `tools.txt` exposes the component as `diff` and `cmp`.
|
||||
|
||||
`search-test.nu` compares identical directories and changed nested files against GNU diff.
|
||||
@@ -0,0 +1,61 @@
|
||||
[](https://crates.io/crates/diffutils)
|
||||
[](https://discord.gg/wQVJbvJ)
|
||||
[](https://github.com/uutils/diffutils/blob/main/LICENSE)
|
||||
[](https://deps.rs/repo/github/uutils/diffutils)
|
||||
[](https://codspeed.io/uutils/diffutils?utm_source=badge)
|
||||
|
||||
[](https://codecov.io/gh/uutils/diffutils)
|
||||
|
||||
The goal of this package is to be a drop-in replacement for the [diffutils commands](https://www.gnu.org/software/diffutils/) (diff, cmp, diff3, sdiff) in Rust.
|
||||
|
||||
Based on the incomplete diff generator in https://github.com/rust-lang/rust/blob/master/src/tools/compiletest/src/runtest.rs, and made to be compatible with GNU's diff and patch tools.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Ensure you have Rust installed on your system. You can install Rust through [rustup](https://rustup.rs/).
|
||||
|
||||
Clone the repository and build the project using Cargo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/uutils/diffutils.git
|
||||
cd diffutils
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```bash
|
||||
|
||||
cat <<EOF >fruits_old.txt
|
||||
Apple
|
||||
Banana
|
||||
Cherry
|
||||
EOF
|
||||
|
||||
cat <<EOF >fruits_new.txt
|
||||
Apple
|
||||
Fig
|
||||
Cherry
|
||||
EOF
|
||||
|
||||
$ cargo run -- diff -u fruits_old.txt fruits_new.txt
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
|
||||
Running `target/debug/diffutils -u fruits_old.txt fruits_new.txt`
|
||||
--- fruits_old.txt
|
||||
+++ fruits_new.txt
|
||||
@@ -1,3 +1,3 @@
|
||||
Apple
|
||||
-Banana
|
||||
+Fig
|
||||
Cherry
|
||||
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is distributed under the terms of both the MIT license and the
|
||||
Apache License (Version 2.0).
|
||||
|
||||
See [LICENSE-APACHE](LICENSE-APACHE), [LICENSE-MIT](LICENSE-MIT), and
|
||||
[COPYRIGHT](COPYRIGHT) for details.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We provide security updates only for the latest released version of `uutils/diffutils`.
|
||||
Older versions may not receive patches.
|
||||
If you are using a version packaged by your Linux distribution, please check with your distribution maintainers for their update policy.
|
||||
|
||||
---
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
**Do not open public GitHub issues for security vulnerabilities.**
|
||||
This prevents accidental disclosure before a fix is available.
|
||||
|
||||
Instead, please use the following method:
|
||||
|
||||
- **Email:** [sylvestre@debian.org](mailto:Sylvestre@debian.org)
|
||||
- **Encryption (optional):** You may encrypt your report using our PGP key:
|
||||
Fingerprint: B60D B599 4D39 BEC4 D1A9 5CCF 7E65 28DA 752F 1BE1
|
||||
---
|
||||
|
||||
### What to Include in Your Report
|
||||
|
||||
To help us investigate and resolve the issue quickly, please include as much detail as possible:
|
||||
|
||||
- **Type of issue:** e.g. privilege escalation, information disclosure.
|
||||
- **Location in the source:** file path, commit hash, branch, or tag.
|
||||
- **Steps to reproduce:** exact commands, test cases, or scripts.
|
||||
- **Special configuration:** any flags, environment variables, or system setup required.
|
||||
- **Affected systems:** OS/distribution and version(s) where the issue occurs.
|
||||
- **Impact:** your assessment of the potential severity (DoS, RCE, data leak, etc.).
|
||||
|
||||
---
|
||||
|
||||
## Disclosure Policy
|
||||
|
||||
We follow a **Coordinated Vulnerability Disclosure (CVD)** process:
|
||||
|
||||
1. We will acknowledge receipt of your report within **10 days**.
|
||||
2. We will investigate, reproduce, and assess the issue.
|
||||
3. We will provide a timeline for developing and releasing a fix.
|
||||
4. Once a fix is available, we will publish a GitHub Security Advisory.
|
||||
5. You will be credited in the advisory unless you request anonymity.
|
||||
@@ -0,0 +1,377 @@
|
||||
// This file is part of the uutils diffutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE-*
|
||||
// files that was distributed with this source code.
|
||||
|
||||
//! Benches for all utils in diffutils.
|
||||
//!
|
||||
//! There is a file generator included to create files of different sizes for comparison. \
|
||||
//! Set the TEMP_DIR const to keep the files. df_to_ files have small changes in them, search for '#'. \
|
||||
//! File generation up to 1 GB is really fast, Benchmarking above 100 MB takes very long.
|
||||
|
||||
/// Generate test files with these sizes in KB.
|
||||
const FILE_SIZE_KILO_BYTES: [u64; 4] = [100, 1 * MB, 10 * MB, 25 * MB];
|
||||
// const FILE_SIZE_KILO_BYTES: [u64; 3] = [100, 1 * MB, 5 * MB];
|
||||
// Empty String to use TempDir (files will be removed after test) or specify dir to keep generated files
|
||||
const TEMP_DIR: &str = "";
|
||||
const NUM_DIFF: u64 = 4;
|
||||
// just for FILE_SIZE_KILO_BYTES
|
||||
const MB: u64 = 1_000;
|
||||
const CHANGE_CHAR: u8 = b'#';
|
||||
|
||||
#[cfg(not(feature = "feat_bench_not_cmp"))]
|
||||
mod diffutils_cmp {
|
||||
use std::hint::black_box;
|
||||
|
||||
use diffutilslib::cmp;
|
||||
use divan::Bencher;
|
||||
|
||||
use crate::{binary, prepare::*, FILE_SIZE_KILO_BYTES};
|
||||
|
||||
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
|
||||
fn cmp_compare_files_equal(bencher: Bencher, kb: u64) {
|
||||
let (from, to) = get_context().get_test_files_equal(kb);
|
||||
let cmd = format!("cmp {from} {to}");
|
||||
let opts = str_to_options(&cmd).into_iter().peekable();
|
||||
let params = cmp::parse_params(opts).unwrap();
|
||||
|
||||
bencher
|
||||
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
|
||||
.with_inputs(|| params.clone())
|
||||
.bench_refs(|params| black_box(cmp::cmp(¶ms).unwrap()));
|
||||
}
|
||||
|
||||
// bench the actual compare; cmp exits on first difference
|
||||
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
|
||||
fn cmp_compare_files_different(bencher: Bencher, bytes: u64) {
|
||||
let (from, to) = get_context().get_test_files_different(bytes);
|
||||
let cmd = format!("cmp {from} {to} -s");
|
||||
let opts = str_to_options(&cmd).into_iter().peekable();
|
||||
let params = cmp::parse_params(opts).unwrap();
|
||||
|
||||
bencher
|
||||
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
|
||||
.with_inputs(|| params.clone())
|
||||
.bench_refs(|params| black_box(cmp::cmp(¶ms).unwrap()));
|
||||
}
|
||||
|
||||
// bench original GNU cmp
|
||||
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
|
||||
fn cmd_cmp_gnu_equal(bencher: Bencher, bytes: u64) {
|
||||
let (from, to) = get_context().get_test_files_equal(bytes);
|
||||
let args_str = format!("{from} {to}");
|
||||
bencher
|
||||
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
|
||||
.with_inputs(|| args_str.clone())
|
||||
.bench_refs(|cmd_args| binary::bench_binary("cmp", cmd_args));
|
||||
}
|
||||
|
||||
// bench the compiled release version
|
||||
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
|
||||
fn cmd_cmp_release_equal(bencher: Bencher, bytes: u64) {
|
||||
let (from, to) = get_context().get_test_files_equal(bytes);
|
||||
let args_str = format!("cmp {from} {to}");
|
||||
|
||||
bencher
|
||||
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
|
||||
.with_inputs(|| args_str.clone())
|
||||
.bench_refs(|cmd_args| binary::bench_binary("target/release/diffutils", cmd_args));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "feat_bench_not_diff"))]
|
||||
mod diffutils_diff {
|
||||
// use std::hint::black_box;
|
||||
|
||||
use crate::{binary, prepare::*, FILE_SIZE_KILO_BYTES};
|
||||
// use diffutilslib::params;
|
||||
use divan::Bencher;
|
||||
|
||||
// bench the actual compare
|
||||
// TODO diff does not have a diff function
|
||||
// #[divan::bench(args = [100_000,10_000])]
|
||||
// fn diff_compare_files(bencher: Bencher, bytes: u64) {
|
||||
// let (from, to) = gen_testfiles(lines, 0, "id");
|
||||
// let cmd = format!("cmp {from} {to}");
|
||||
// let opts = str_to_options(&cmd).into_iter().peekable();
|
||||
// let params = params::parse_params(opts).unwrap();
|
||||
//
|
||||
// bencher
|
||||
// // .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
|
||||
// .with_inputs(|| params.clone())
|
||||
// .bench_refs(|params| diff::diff(¶ms).unwrap());
|
||||
// }
|
||||
|
||||
// bench original GNU diff
|
||||
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
|
||||
fn cmd_diff_gnu_equal(bencher: Bencher, bytes: u64) {
|
||||
let (from, to) = get_context().get_test_files_equal(bytes);
|
||||
let args_str = format!("{from} {to}");
|
||||
bencher
|
||||
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
|
||||
.with_inputs(|| args_str.clone())
|
||||
.bench_refs(|cmd_args| binary::bench_binary("diff", cmd_args));
|
||||
}
|
||||
|
||||
// bench the compiled release version
|
||||
#[divan::bench(args = FILE_SIZE_KILO_BYTES)]
|
||||
fn cmd_diff_release_equal(bencher: Bencher, bytes: u64) {
|
||||
let (from, to) = get_context().get_test_files_equal(bytes);
|
||||
let args_str = format!("diff {from} {to}");
|
||||
|
||||
bencher
|
||||
// .with_inputs(|| prepare::cmp_params_identical_testfiles(lines))
|
||||
.with_inputs(|| args_str.clone())
|
||||
.bench_refs(|cmd_args| binary::bench_binary("target/release/diffutils", cmd_args));
|
||||
}
|
||||
}
|
||||
|
||||
mod parser {
|
||||
use std::hint::black_box;
|
||||
|
||||
use diffutilslib::{cmp, params};
|
||||
use divan::Bencher;
|
||||
|
||||
use crate::prepare::str_to_options;
|
||||
|
||||
// bench the time it takes to parse the command line arguments
|
||||
#[divan::bench]
|
||||
fn cmp_parser(bencher: Bencher) {
|
||||
let cmd = "cmd file_1.txt file_2.txt -bl n10M --ignore-initial=100KiB:1MiB";
|
||||
let args = str_to_options(&cmd).into_iter().peekable();
|
||||
bencher
|
||||
.with_inputs(|| args.clone())
|
||||
.bench_values(|data| black_box(cmp::parse_params(data)));
|
||||
}
|
||||
|
||||
// // test the impact on the benchmark if not converting the cmd to Vec<OsString> (doubles for parse)
|
||||
// #[divan::bench]
|
||||
// fn cmp_parser_no_prepare() {
|
||||
// let cmd = "cmd file_1.txt file_2.txt -bl n10M --ignore-initial=100KiB:1MiB";
|
||||
// let args = str_to_options(&cmd).into_iter().peekable();
|
||||
// let _ = cmp::parse_params(args);
|
||||
// }
|
||||
|
||||
// bench the time it takes to parse the command line arguments
|
||||
#[divan::bench]
|
||||
fn diff_parser(bencher: Bencher) {
|
||||
let cmd = "diff file_1.txt file_2.txt -s --brief --expand-tabs --width=100";
|
||||
let args = str_to_options(&cmd).into_iter().peekable();
|
||||
bencher
|
||||
.with_inputs(|| args.clone())
|
||||
.bench_values(|data| black_box(params::parse_params(data)));
|
||||
}
|
||||
}
|
||||
|
||||
mod prepare {
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
fs::{self, File},
|
||||
io::{BufWriter, Write},
|
||||
path::Path,
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use rand::RngExt;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::{CHANGE_CHAR, FILE_SIZE_KILO_BYTES, NUM_DIFF, TEMP_DIR};
|
||||
|
||||
// file lines and .txt will be added
|
||||
const FROM_FILE: &str = "from_file";
|
||||
const TO_FILE: &str = "to_file";
|
||||
const LINE_LENGTH: usize = 60;
|
||||
|
||||
/// Contains test data (file names) which only needs to be created once.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BenchContext {
|
||||
pub tmp_dir: Option<TempDir>,
|
||||
pub dir: String,
|
||||
pub files_equal: Vec<(String, String)>,
|
||||
pub files_different: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl BenchContext {
|
||||
pub fn get_path(&self) -> &Path {
|
||||
match &self.tmp_dir {
|
||||
Some(tmp) => tmp.path(),
|
||||
None => Path::new(&self.dir),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_test_files_equal(&self, kb: u64) -> &(String, String) {
|
||||
let p = FILE_SIZE_KILO_BYTES.iter().position(|f| *f == kb).unwrap();
|
||||
&self.files_equal[p]
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn get_test_files_different(&self, kb: u64) -> &(String, String) {
|
||||
let p = FILE_SIZE_KILO_BYTES.iter().position(|f| *f == kb).unwrap();
|
||||
&self.files_different[p]
|
||||
}
|
||||
}
|
||||
|
||||
// Since each bench function is separate in Divan it is more difficult to dynamically create test data.
|
||||
// This keeps the TempDir alive until the program exits and generates the files only once.
|
||||
static SHARED_CONTEXT: OnceLock<BenchContext> = OnceLock::new();
|
||||
/// Creates the test files once and provides them to all tests.
|
||||
pub fn get_context() -> &'static BenchContext {
|
||||
SHARED_CONTEXT.get_or_init(|| {
|
||||
let mut ctx = BenchContext::default();
|
||||
if TEMP_DIR.is_empty() {
|
||||
let tmp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
ctx.tmp_dir = Some(tmp_dir);
|
||||
} else {
|
||||
// uses current directory, the generated files are kept
|
||||
let path = Path::new(TEMP_DIR);
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(path).expect("Path {path} could not be created");
|
||||
}
|
||||
ctx.dir = TEMP_DIR.to_string();
|
||||
};
|
||||
|
||||
// generate test bytes
|
||||
for kb in FILE_SIZE_KILO_BYTES {
|
||||
let f = generate_test_files_bytes(ctx.get_path(), kb * 1000, 0, "eq")
|
||||
.expect("generate_test_files failed");
|
||||
ctx.files_equal.push(f);
|
||||
let f = generate_test_files_bytes(ctx.get_path(), kb * 1000, NUM_DIFF, "df")
|
||||
.expect("generate_test_files failed");
|
||||
ctx.files_different.push(f);
|
||||
}
|
||||
|
||||
ctx
|
||||
})
|
||||
}
|
||||
|
||||
pub fn str_to_options(opt: &str) -> Vec<OsString> {
|
||||
let s: Vec<OsString> = opt
|
||||
.split(" ")
|
||||
.into_iter()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| OsString::from(s))
|
||||
.collect();
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
/// Generates two test files for comparison with <bytes> size.
|
||||
///
|
||||
/// Each line consists of 10 words with 5 letters, giving a line length of 60 bytes.
|
||||
/// If num_differences is set, '#' will be inserted between the first two words of a line,
|
||||
/// evenly spaced in the file. 1 will add the change in the last line, so the comparison takes longest.
|
||||
fn generate_test_files_bytes(
|
||||
dir: &Path,
|
||||
bytes: u64,
|
||||
num_differences: u64,
|
||||
id: &str,
|
||||
) -> std::io::Result<(String, String)> {
|
||||
let id = if id.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!("{id}_")
|
||||
};
|
||||
let f1 = format!("{id}{FROM_FILE}_{bytes}.txt");
|
||||
let f2 = format!("{id}{TO_FILE}_{bytes}.txt");
|
||||
let from_path = dir.join(f1);
|
||||
let to_path = dir.join(f2);
|
||||
|
||||
generate_file_bytes(&from_path, &to_path, bytes, num_differences)?;
|
||||
|
||||
Ok((
|
||||
from_path.to_string_lossy().to_string(),
|
||||
to_path.to_string_lossy().to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn generate_file_bytes(
|
||||
from_name: &Path,
|
||||
to_name: &Path,
|
||||
bytes: u64,
|
||||
num_differences: u64,
|
||||
) -> std::io::Result<()> {
|
||||
let file_from = File::create(from_name)?;
|
||||
let file_to = File::create(to_name)?;
|
||||
// for int division, lines will be smaller than requested bytes
|
||||
let n_lines = bytes / LINE_LENGTH as u64;
|
||||
let change_every_n_lines = if num_differences == 0 {
|
||||
0
|
||||
} else {
|
||||
let c = n_lines / num_differences;
|
||||
if c == 0 {
|
||||
1
|
||||
} else {
|
||||
c
|
||||
}
|
||||
};
|
||||
// Use a larger 128KB buffer for massive files
|
||||
let mut writer_from = BufWriter::with_capacity(128 * 1024, file_from);
|
||||
let mut writer_to = BufWriter::with_capacity(128 * 1024, file_to);
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// Each line: (5 chars * 10 words) + 9 spaces + 1 newline = 60 bytes
|
||||
let mut line_buffer = [b' '; 60];
|
||||
line_buffer[59] = b'\n'; // Set the newline once at the end
|
||||
|
||||
for i in (0..n_lines).rev() {
|
||||
// Fill only the letter positions, skipping spaces and the newline
|
||||
for word_idx in 0..10 {
|
||||
let start = word_idx * 6; // Each word + space block is 6 bytes
|
||||
for i in 0..5 {
|
||||
line_buffer[start + i] = rng.random_range(b'a'..b'z' + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Write the raw bytes directly to both files
|
||||
writer_from.write_all(&line_buffer)?;
|
||||
// make changes in the file
|
||||
if num_differences == 0 {
|
||||
writer_to.write_all(&line_buffer)?;
|
||||
} else {
|
||||
if i % change_every_n_lines == 0 && n_lines - i > 2 {
|
||||
line_buffer[5] = CHANGE_CHAR;
|
||||
}
|
||||
writer_to.write_all(&line_buffer)?;
|
||||
line_buffer[5] = b' ';
|
||||
}
|
||||
}
|
||||
|
||||
// create last line
|
||||
let missing = (bytes - n_lines as u64 * LINE_LENGTH as u64) as usize;
|
||||
if missing > 0 {
|
||||
for word_idx in 0..10 {
|
||||
let start = word_idx * 6; // Each word + space block is 6 bytes
|
||||
for i in 0..5 {
|
||||
line_buffer[start + i] = rng.random_range(b'a'..b'z' + 1);
|
||||
}
|
||||
}
|
||||
line_buffer[missing - 1] = b'\n';
|
||||
writer_from.write_all(&line_buffer[0..missing])?;
|
||||
writer_to.write_all(&line_buffer[0..missing])?;
|
||||
}
|
||||
|
||||
writer_from.flush()?;
|
||||
writer_to.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod binary {
|
||||
use std::process::Command;
|
||||
|
||||
use crate::prepare::str_to_options;
|
||||
|
||||
pub fn bench_binary(program: &str, cmd_args: &str) -> std::process::ExitStatus {
|
||||
let args = str_to_options(cmd_args);
|
||||
Command::new(program)
|
||||
.args(args)
|
||||
.status()
|
||||
.expect("Failed to execute binary")
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Run registered benchmarks.
|
||||
divan::main();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[workspace]
|
||||
members = ["cargo:."]
|
||||
|
||||
# Config for 'dist'
|
||||
[dist]
|
||||
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.33.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", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
|
||||
# Skip checking whether the specified configuration files are up to date
|
||||
allow-dirty = ["ci"]
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,998 @@
|
||||
// This file is part of the uutils diffutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE-*
|
||||
// files that was distributed with this source code.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Write;
|
||||
|
||||
use crate::params::Params;
|
||||
use crate::utils::do_write_line;
|
||||
use crate::utils::get_modification_time;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum DiffLine {
|
||||
Context(Vec<u8>),
|
||||
Change(Vec<u8>),
|
||||
Add(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Mismatch {
|
||||
pub line_number_expected: usize,
|
||||
pub line_number_actual: usize,
|
||||
pub expected: Vec<DiffLine>,
|
||||
pub actual: Vec<DiffLine>,
|
||||
pub expected_missing_nl: bool,
|
||||
pub actual_missing_nl: bool,
|
||||
pub expected_all_context: bool,
|
||||
pub actual_all_context: bool,
|
||||
}
|
||||
|
||||
impl Mismatch {
|
||||
fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch {
|
||||
Mismatch {
|
||||
line_number_expected,
|
||||
line_number_actual,
|
||||
expected: Vec::new(),
|
||||
actual: Vec::new(),
|
||||
expected_missing_nl: false,
|
||||
actual_missing_nl: false,
|
||||
expected_all_context: false,
|
||||
actual_all_context: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A run of changed lines with no common line between them.
|
||||
struct ChangeBlock {
|
||||
expected_start: usize,
|
||||
actual_start: usize,
|
||||
deletes: bool,
|
||||
inserts: bool,
|
||||
}
|
||||
|
||||
impl ChangeBlock {
|
||||
fn new(mismatch: &Mismatch) -> Self {
|
||||
Self {
|
||||
expected_start: mismatch.expected.len(),
|
||||
actual_start: mismatch.actual.len(),
|
||||
deletes: false,
|
||||
inserts: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a block at the end of `mismatch` if none is open, and records
|
||||
/// whether it deletes or inserts lines.
|
||||
fn mark(block: &mut Option<Self>, mismatch: &Mismatch, deletes: bool, inserts: bool) {
|
||||
let block = block.get_or_insert_with(|| Self::new(mismatch));
|
||||
block.deletes |= deletes;
|
||||
block.inserts |= inserts;
|
||||
}
|
||||
|
||||
fn close(block: &mut Option<Self>, mismatch: &mut Mismatch) {
|
||||
if let Some(block) = block.take() {
|
||||
block.finish(mismatch);
|
||||
}
|
||||
}
|
||||
|
||||
/// Like GNU diff, a block that both deletes and inserts lines marks all of
|
||||
/// them as changed (`!`), not only as many as pair up.
|
||||
fn finish(self, mismatch: &mut Mismatch) {
|
||||
if !(self.deletes && self.inserts) {
|
||||
return;
|
||||
}
|
||||
let expected = mismatch.expected[self.expected_start..].iter_mut();
|
||||
let actual = mismatch.actual[self.actual_start..].iter_mut();
|
||||
for line in expected.chain(actual) {
|
||||
if let DiffLine::Add(content) = line {
|
||||
*line = DiffLine::Change(std::mem::take(content));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Produces a diff between the expected output and actual output.
|
||||
fn make_diff(
|
||||
expected: &[u8],
|
||||
actual: &[u8],
|
||||
context_size: usize,
|
||||
stop_early: bool,
|
||||
) -> Vec<Mismatch> {
|
||||
let mut line_number_expected = 1;
|
||||
let mut line_number_actual = 1;
|
||||
let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size);
|
||||
// Like GNU diff, keep changes in one hunk while at most twice the context
|
||||
// size of unchanged lines separates them.
|
||||
let max_gap = 2 * context_size;
|
||||
let mut lines_since_mismatch = max_gap + 1;
|
||||
let mut results = Vec::new();
|
||||
let mut mismatch = Mismatch::new(0, 0);
|
||||
|
||||
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
|
||||
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
|
||||
|
||||
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
|
||||
// ^ means that underflow here is impossible
|
||||
let expected_lines_count = expected_lines.len() - 1;
|
||||
let actual_lines_count = actual_lines.len() - 1;
|
||||
|
||||
if expected_lines.last() == Some(&&b""[..]) {
|
||||
expected_lines.pop();
|
||||
}
|
||||
|
||||
if actual_lines.last() == Some(&&b""[..]) {
|
||||
actual_lines.pop();
|
||||
}
|
||||
|
||||
let mut block: Option<ChangeBlock> = None;
|
||||
|
||||
for result in diff::slice(&expected_lines, &actual_lines) {
|
||||
match result {
|
||||
diff::Result::Left(str) => {
|
||||
if lines_since_mismatch > max_gap {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len(),
|
||||
line_number_actual - context_queue.len(),
|
||||
);
|
||||
}
|
||||
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
|
||||
ChangeBlock::mark(&mut block, &mismatch, true, false);
|
||||
mismatch.expected.push(DiffLine::Add(str.to_vec()));
|
||||
if line_number_expected > expected_lines_count {
|
||||
mismatch.expected_missing_nl = true;
|
||||
}
|
||||
line_number_expected += 1;
|
||||
lines_since_mismatch = 0;
|
||||
}
|
||||
diff::Result::Right(str) => {
|
||||
if lines_since_mismatch > max_gap {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len(),
|
||||
line_number_actual - context_queue.len(),
|
||||
);
|
||||
}
|
||||
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
|
||||
ChangeBlock::mark(&mut block, &mismatch, false, true);
|
||||
mismatch.actual.push(DiffLine::Add(str.to_vec()));
|
||||
if line_number_actual > actual_lines_count {
|
||||
mismatch.actual_missing_nl = true;
|
||||
}
|
||||
line_number_actual += 1;
|
||||
lines_since_mismatch = 0;
|
||||
}
|
||||
diff::Result::Both(str, _) => {
|
||||
// if one of them is missing a newline and the other isn't, then they don't actually match
|
||||
if (line_number_actual > actual_lines_count)
|
||||
&& (line_number_expected > expected_lines_count)
|
||||
{
|
||||
ChangeBlock::close(&mut block, &mut mismatch);
|
||||
if context_queue.len() < context_size {
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
if lines_since_mismatch < context_size {
|
||||
mismatch.expected.push(DiffLine::Context(str.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Context(str.to_vec()));
|
||||
mismatch.expected_missing_nl = true;
|
||||
mismatch.actual_missing_nl = true;
|
||||
}
|
||||
}
|
||||
lines_since_mismatch = 0;
|
||||
} else if line_number_actual > actual_lines_count {
|
||||
if lines_since_mismatch > max_gap {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len(),
|
||||
line_number_actual - context_queue.len(),
|
||||
);
|
||||
}
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
ChangeBlock::mark(&mut block, &mismatch, true, true);
|
||||
mismatch.expected.push(DiffLine::Change(str.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Change(str.to_vec()));
|
||||
mismatch.actual_missing_nl = true;
|
||||
lines_since_mismatch = 0;
|
||||
} else if line_number_expected > expected_lines_count {
|
||||
if lines_since_mismatch > max_gap {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(
|
||||
line_number_expected - context_queue.len(),
|
||||
line_number_actual - context_queue.len(),
|
||||
);
|
||||
}
|
||||
while let Some(line) = context_queue.pop_front() {
|
||||
mismatch.expected.push(DiffLine::Context(line.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Context(line.to_vec()));
|
||||
}
|
||||
ChangeBlock::mark(&mut block, &mismatch, true, true);
|
||||
mismatch.expected.push(DiffLine::Change(str.to_vec()));
|
||||
mismatch.expected_missing_nl = true;
|
||||
mismatch.actual.push(DiffLine::Change(str.to_vec()));
|
||||
lines_since_mismatch = 0;
|
||||
} else {
|
||||
ChangeBlock::close(&mut block, &mut mismatch);
|
||||
debug_assert!(context_queue.len() <= context_size);
|
||||
if context_queue.len() >= context_size {
|
||||
let _ = context_queue.pop_front();
|
||||
}
|
||||
if lines_since_mismatch < context_size {
|
||||
mismatch.expected.push(DiffLine::Context(str.to_vec()));
|
||||
mismatch.actual.push(DiffLine::Context(str.to_vec()));
|
||||
} else if context_size > 0 {
|
||||
context_queue.push_back(str);
|
||||
}
|
||||
lines_since_mismatch += 1;
|
||||
}
|
||||
line_number_expected += 1;
|
||||
line_number_actual += 1;
|
||||
}
|
||||
}
|
||||
if stop_early && !results.is_empty() {
|
||||
// Optimization: stop analyzing the files as soon as there are any differences
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(block) = block {
|
||||
block.finish(&mut mismatch);
|
||||
}
|
||||
results.push(mismatch);
|
||||
results.remove(0);
|
||||
|
||||
if results.is_empty() && expected_lines_count != actual_lines_count {
|
||||
let mut mismatch = Mismatch::new(expected_lines.len(), actual_lines.len());
|
||||
// empty diff and only expected lines has a missing line at end
|
||||
if expected_lines_count != expected_lines.len() {
|
||||
mismatch.expected.push(DiffLine::Change(
|
||||
expected_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
mismatch.expected_missing_nl = true;
|
||||
mismatch.actual.push(DiffLine::Change(
|
||||
actual_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
results.push(mismatch);
|
||||
} else if actual_lines_count != actual_lines.len() {
|
||||
mismatch.expected.push(DiffLine::Change(
|
||||
expected_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
mismatch.actual.push(DiffLine::Change(
|
||||
actual_lines
|
||||
.pop()
|
||||
.expect("can't be empty; produced by split()")
|
||||
.to_vec(),
|
||||
));
|
||||
mismatch.actual_missing_nl = true;
|
||||
results.push(mismatch);
|
||||
}
|
||||
}
|
||||
|
||||
// hunks with pure context lines get truncated to empty
|
||||
for mismatch in &mut results {
|
||||
if !mismatch
|
||||
.expected
|
||||
.iter()
|
||||
.any(|x| !matches!(&x, DiffLine::Context(_)))
|
||||
{
|
||||
mismatch.expected_all_context = true;
|
||||
}
|
||||
if !mismatch
|
||||
.actual
|
||||
.iter()
|
||||
.any(|x| !matches!(&x, DiffLine::Context(_)))
|
||||
{
|
||||
mismatch.actual_all_context = true;
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
|
||||
let from_modified_time = get_modification_time(¶ms.from.to_string_lossy());
|
||||
let to_modified_time = get_modification_time(¶ms.to.to_string_lossy());
|
||||
let mut output = format!(
|
||||
"*** {0}\t{1}\n--- {2}\t{3}\n",
|
||||
params.from.to_string_lossy(),
|
||||
from_modified_time,
|
||||
params.to.to_string_lossy(),
|
||||
to_modified_time
|
||||
)
|
||||
.into_bytes();
|
||||
let diff_results = make_diff(expected, actual, params.context_count, params.brief);
|
||||
if diff_results.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
if params.brief {
|
||||
return output;
|
||||
}
|
||||
for result in diff_results {
|
||||
let mut line_number_expected = result.line_number_expected;
|
||||
let mut line_number_actual = result.line_number_actual;
|
||||
let mut expected_count = result.expected.len();
|
||||
let mut actual_count = result.actual.len();
|
||||
if expected_count == 0 {
|
||||
line_number_expected -= 1;
|
||||
expected_count = 1;
|
||||
}
|
||||
if actual_count == 0 {
|
||||
line_number_actual -= 1;
|
||||
actual_count = 1;
|
||||
}
|
||||
let end_line_number_expected = expected_count + line_number_expected - 1;
|
||||
let end_line_number_actual = actual_count + line_number_actual - 1;
|
||||
let exp_start = if end_line_number_expected == line_number_expected {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{line_number_expected},")
|
||||
};
|
||||
let act_start = if end_line_number_actual == line_number_actual {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{line_number_actual},")
|
||||
};
|
||||
writeln!(
|
||||
output,
|
||||
"***************\n*** {exp_start}{end_line_number_expected} ****"
|
||||
)
|
||||
.expect("write to Vec is infallible");
|
||||
if !result.expected_all_context {
|
||||
for line in result.expected {
|
||||
match line {
|
||||
DiffLine::Context(e) => {
|
||||
write!(output, " ").expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Change(e) => {
|
||||
write!(output, "! ").expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Add(e) => {
|
||||
write!(output, "- ").expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.expected_missing_nl {
|
||||
writeln!(output, r"\ No newline at end of file")
|
||||
.expect("write to Vec is infallible");
|
||||
}
|
||||
}
|
||||
writeln!(output, "--- {act_start}{end_line_number_actual} ----")
|
||||
.expect("write to Vec is infallible");
|
||||
if !result.actual_all_context {
|
||||
for line in result.actual {
|
||||
match line {
|
||||
DiffLine::Context(e) => {
|
||||
write!(output, " ").expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Change(e) => {
|
||||
write!(output, "! ").expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
DiffLine::Add(e) => {
|
||||
write!(output, "+ ").expect("write to Vec is infallible");
|
||||
do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
|
||||
.expect("write to Vec is infallible");
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.actual_missing_nl {
|
||||
writeln!(output, r"\ No newline at end of file")
|
||||
.expect("write to Vec is infallible");
|
||||
}
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::utils::testcmds::PATCH_CMD;
|
||||
|
||||
#[test]
|
||||
fn test_permutations() {
|
||||
// test all possible six-line files.
|
||||
let target = "target/context-diff/";
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let patched = &format!("{target}/alef");
|
||||
let diff = diff(
|
||||
&alef,
|
||||
&bet,
|
||||
&Params {
|
||||
from: patched.into(),
|
||||
to: patched.into(),
|
||||
context_count: 2,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
File::create(format!("{target}/ab.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(format!("{target}/alef")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(format!("{target}/bet")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = PATCH_CMD
|
||||
.new()
|
||||
.arg("-p0")
|
||||
.arg("--context")
|
||||
.stdin(File::open(format!("{target}/ab.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{output:?}");
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(format!("{target}/alef")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_empty_lines() {
|
||||
let target = "target/context-diff/";
|
||||
// test all possible six-line files with missing newlines.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let patched = &format!("{target}/alef_");
|
||||
let diff = diff(
|
||||
&alef,
|
||||
&bet,
|
||||
&Params {
|
||||
from: patched.into(),
|
||||
to: patched.into(),
|
||||
context_count: 2,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
File::create(format!("{target}/ab_.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(format!("{target}/alef_")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(format!("{target}/bet_")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = PATCH_CMD
|
||||
.new()
|
||||
.arg("-p0")
|
||||
.arg("--context")
|
||||
.stdin(File::open(format!("{target}/ab_.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{output:?}");
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(format!("{target}/alef_")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_missing_lines() {
|
||||
let target = "target/context-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
if alef.is_empty() && bet.is_empty() {
|
||||
continue;
|
||||
};
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let patched = &format!("{target}/alefx");
|
||||
let diff = diff(
|
||||
&alef,
|
||||
&bet,
|
||||
&Params {
|
||||
from: patched.into(),
|
||||
to: patched.into(),
|
||||
context_count: 2,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
File::create(format!("{target}/abx.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(format!("{target}/alefx")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(format!("{target}/betx")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = PATCH_CMD
|
||||
.new()
|
||||
.arg("-p0")
|
||||
.arg("--context")
|
||||
.stdin(File::open(format!("{target}/abx.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{output:?}");
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(format!("{target}/alefx")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_reverse() {
|
||||
let target = "target/context-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"a\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"c\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"e\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let alefr_path = &format!("{target}/alefr");
|
||||
let diff = diff(
|
||||
&alef,
|
||||
&bet,
|
||||
&Params {
|
||||
from: alefr_path.into(),
|
||||
to: alefr_path.into(),
|
||||
context_count: 2,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
File::create(format!("{target}/abr.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(format!("{target}/alefr")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(format!("{target}/betr")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = PATCH_CMD
|
||||
.new()
|
||||
.arg("-p0")
|
||||
.arg("--context")
|
||||
.stdin(File::open(format!("{target}/abr.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{output:?}");
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(format!("{target}/alefr")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stop_early() {
|
||||
use crate::assert_diff_eq;
|
||||
|
||||
let from_filename = "foo";
|
||||
let from = ["a", "b", "c", ""].join("\n");
|
||||
let to_filename = "bar";
|
||||
let to = ["a", "d", "c", ""].join("\n");
|
||||
|
||||
let diff_full = diff(
|
||||
from.as_bytes(),
|
||||
to.as_bytes(),
|
||||
&Params {
|
||||
from: from_filename.into(),
|
||||
to: to_filename.into(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let expected_full = [
|
||||
"*** foo\tTIMESTAMP",
|
||||
"--- bar\tTIMESTAMP",
|
||||
"***************",
|
||||
"*** 1,3 ****",
|
||||
" a",
|
||||
"! b",
|
||||
" c",
|
||||
"--- 1,3 ----",
|
||||
" a",
|
||||
"! d",
|
||||
" c",
|
||||
"",
|
||||
]
|
||||
.join("\n");
|
||||
assert_diff_eq!(diff_full, expected_full);
|
||||
|
||||
let diff_brief = diff(
|
||||
from.as_bytes(),
|
||||
to.as_bytes(),
|
||||
&Params {
|
||||
from: from_filename.into(),
|
||||
to: to_filename.into(),
|
||||
brief: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let expected_brief = ["*** foo\tTIMESTAMP", "--- bar\tTIMESTAMP", ""].join("\n");
|
||||
assert_diff_eq!(diff_brief, expected_brief);
|
||||
|
||||
let nodiff_full = diff(
|
||||
from.as_bytes(),
|
||||
from.as_bytes(),
|
||||
&Params {
|
||||
from: from_filename.into(),
|
||||
to: to_filename.into(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(nodiff_full.is_empty());
|
||||
|
||||
let nodiff_brief = diff(
|
||||
from.as_bytes(),
|
||||
from.as_bytes(),
|
||||
&Params {
|
||||
from: from_filename.into(),
|
||||
to: to_filename.into(),
|
||||
brief: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(nodiff_brief.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_change_block_lines_all_changed() {
|
||||
use crate::assert_diff_eq;
|
||||
|
||||
let params = Params {
|
||||
from: "foo".into(),
|
||||
to: "bar".into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Two lines replaced by one: both old lines are changed, not one deleted.
|
||||
let from = ["a", "b", "c", "d", ""].join("\n");
|
||||
let to = ["a", "x", "d", ""].join("\n");
|
||||
let expected = [
|
||||
"*** foo\tTIMESTAMP",
|
||||
"--- bar\tTIMESTAMP",
|
||||
"***************",
|
||||
"*** 1,4 ****",
|
||||
" a",
|
||||
"! b",
|
||||
"! c",
|
||||
" d",
|
||||
"--- 1,3 ----",
|
||||
" a",
|
||||
"! x",
|
||||
" d",
|
||||
"",
|
||||
]
|
||||
.join("\n");
|
||||
let output = diff(from.as_bytes(), to.as_bytes(), ¶ms);
|
||||
assert_diff_eq!(output, expected);
|
||||
|
||||
// One line replaced by three: all three new lines are changed.
|
||||
let from = ["a", "b", ""].join("\n");
|
||||
let to = ["a", "x", "y", "z", ""].join("\n");
|
||||
let expected = [
|
||||
"*** foo\tTIMESTAMP",
|
||||
"--- bar\tTIMESTAMP",
|
||||
"***************",
|
||||
"*** 1,2 ****",
|
||||
" a",
|
||||
"! b",
|
||||
"--- 1,4 ----",
|
||||
" a",
|
||||
"! x",
|
||||
"! y",
|
||||
"! z",
|
||||
"",
|
||||
]
|
||||
.join("\n");
|
||||
let output = diff(from.as_bytes(), to.as_bytes(), ¶ms);
|
||||
assert_diff_eq!(output, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hunk_merging() {
|
||||
use crate::assert_diff_eq;
|
||||
|
||||
let params = Params {
|
||||
from: "foo".into(),
|
||||
to: "bar".into(),
|
||||
context_count: 1,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Changes separated by up to twice the context size share one hunk.
|
||||
let from = ["a", "b", "c", "d", "e", "f", ""].join("\n");
|
||||
let to = ["a", "B", "c", "d", "E", "f", ""].join("\n");
|
||||
let expected = [
|
||||
"*** foo\tTIMESTAMP",
|
||||
"--- bar\tTIMESTAMP",
|
||||
"***************",
|
||||
"*** 1,6 ****",
|
||||
" a",
|
||||
"! b",
|
||||
" c",
|
||||
" d",
|
||||
"! e",
|
||||
" f",
|
||||
"--- 1,6 ----",
|
||||
" a",
|
||||
"! B",
|
||||
" c",
|
||||
" d",
|
||||
"! E",
|
||||
" f",
|
||||
"",
|
||||
]
|
||||
.join("\n");
|
||||
let output = diff(from.as_bytes(), to.as_bytes(), ¶ms);
|
||||
assert_diff_eq!(output, expected);
|
||||
|
||||
// One more unchanged line between them splits the hunk.
|
||||
let from = ["a", "b", "c", "d", "e", "f", "g", ""].join("\n");
|
||||
let to = ["a", "B", "c", "d", "e", "F", "g", ""].join("\n");
|
||||
let expected = [
|
||||
"*** foo\tTIMESTAMP",
|
||||
"--- bar\tTIMESTAMP",
|
||||
"***************",
|
||||
"*** 1,3 ****",
|
||||
" a",
|
||||
"! b",
|
||||
" c",
|
||||
"--- 1,3 ----",
|
||||
" a",
|
||||
"! B",
|
||||
" c",
|
||||
"***************",
|
||||
"*** 5,7 ****",
|
||||
" e",
|
||||
"! f",
|
||||
" g",
|
||||
"--- 5,7 ----",
|
||||
" e",
|
||||
"! F",
|
||||
" g",
|
||||
"",
|
||||
]
|
||||
.join("\n");
|
||||
let output = diff(from.as_bytes(), to.as_bytes(), ¶ms);
|
||||
assert_diff_eq!(output, expected);
|
||||
|
||||
// An insertion before a nearby change gives the two sides different
|
||||
// line counts; the hunks must still be merged (issue #268).
|
||||
let from = ["milk", "eggs", "bread", "butter", "apples", ""].join("\n");
|
||||
let to = ["milk", "eggs", "yogurt", "bread", "honey", "apples", ""].join("\n");
|
||||
let expected = [
|
||||
"*** foo\tTIMESTAMP",
|
||||
"--- bar\tTIMESTAMP",
|
||||
"***************",
|
||||
"*** 2,5 ****",
|
||||
" eggs",
|
||||
" bread",
|
||||
"! butter",
|
||||
" apples",
|
||||
"--- 2,6 ----",
|
||||
" eggs",
|
||||
"+ yogurt",
|
||||
" bread",
|
||||
"! honey",
|
||||
" apples",
|
||||
"",
|
||||
]
|
||||
.join("\n");
|
||||
let output = diff(from.as_bytes(), to.as_bytes(), ¶ms);
|
||||
assert_diff_eq!(output, expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// This file is part of the uutils diffutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE-*
|
||||
// files that was distributed with this source code.
|
||||
|
||||
use crate::params::{parse_params, Format};
|
||||
use crate::utils::report_failure_to_read_input_file;
|
||||
use crate::{context_diff, ed_diff, normal_diff, side_diff, unified_diff};
|
||||
use std::env::ArgsOs;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::io::{self, stdout, Read, Write};
|
||||
use std::iter::Peekable;
|
||||
use std::process::{exit, ExitCode};
|
||||
|
||||
// Exit codes are documented at
|
||||
// https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html.
|
||||
// An exit status of 0 means no differences were found,
|
||||
// 1 means some differences were found,
|
||||
// and 2 means trouble.
|
||||
pub fn main(opts: Peekable<ArgsOs>) -> ExitCode {
|
||||
let params = parse_params(opts).unwrap_or_else(|error| {
|
||||
eprintln!("{error}");
|
||||
exit(2);
|
||||
});
|
||||
if params.recursive {
|
||||
return recursive(¶ms).unwrap_or_else(|error| {
|
||||
eprintln!("diff: {error}");
|
||||
ExitCode::from(2)
|
||||
});
|
||||
}
|
||||
compare(¶ms)
|
||||
}
|
||||
|
||||
/// Compare directory entries in lexical order, with GNU diff's recursive output.
|
||||
fn recursive(params: &crate::params::Params) -> io::Result<ExitCode> {
|
||||
let from = std::path::Path::new(¶ms.from);
|
||||
let to = std::path::Path::new(¶ms.to);
|
||||
if !from.is_dir() || !to.is_dir() {
|
||||
if fs::read(from)? != fs::read(to)? {
|
||||
println!("diff -r {} {}", from.display(), to.display());
|
||||
}
|
||||
return Ok(compare(params));
|
||||
}
|
||||
let mut names = std::collections::BTreeSet::new();
|
||||
for dir in [from, to] {
|
||||
for entry in fs::read_dir(dir)? { names.insert(entry?.file_name()); }
|
||||
}
|
||||
let mut status = ExitCode::SUCCESS;
|
||||
for name in names {
|
||||
let left = from.join(&name);
|
||||
let right = to.join(&name);
|
||||
let result = if !left.exists() || !right.exists() {
|
||||
let parent = if left.exists() { from } else { to };
|
||||
println!("Only in {}: {}", parent.display(), name.to_string_lossy());
|
||||
ExitCode::from(1)
|
||||
} else if left.is_dir() != right.is_dir() {
|
||||
println!("File {} is a {} while file {} is a {}", left.display(),
|
||||
if left.is_dir() { "directory" } else { "regular file" }, right.display(),
|
||||
if right.is_dir() { "directory" } else { "regular file" });
|
||||
ExitCode::from(1)
|
||||
} else {
|
||||
let mut child = params.clone();
|
||||
child.from = left.into_os_string();
|
||||
child.to = right.into_os_string();
|
||||
recursive(&child)?
|
||||
};
|
||||
if result == ExitCode::from(2) { status = result; }
|
||||
else if result == ExitCode::from(1) && status == ExitCode::SUCCESS { status = result; }
|
||||
}
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
fn compare(params: &crate::params::Params) -> ExitCode {
|
||||
// if from and to are the same file, no need to perform any comparison
|
||||
let maybe_report_identical_files = || {
|
||||
if params.report_identical_files {
|
||||
println!(
|
||||
"Files {} and {} are identical",
|
||||
params.from.to_string_lossy(),
|
||||
params.to.to_string_lossy(),
|
||||
);
|
||||
}
|
||||
};
|
||||
if params.from == "-" && params.to == "-"
|
||||
|| same_file::is_same_file(¶ms.from, ¶ms.to).unwrap_or(false)
|
||||
{
|
||||
maybe_report_identical_files();
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
// read files
|
||||
fn read_file_contents(filepath: &OsString) -> io::Result<Vec<u8>> {
|
||||
if filepath == "-" {
|
||||
let mut content = Vec::new();
|
||||
io::stdin().read_to_end(&mut content).and(Ok(content))
|
||||
} else {
|
||||
fs::read(filepath)
|
||||
}
|
||||
}
|
||||
let mut io_error = false;
|
||||
let from_content = match read_file_contents(¶ms.from) {
|
||||
Ok(from_content) => from_content,
|
||||
Err(e) => {
|
||||
report_failure_to_read_input_file(¶ms.executable, ¶ms.from, &e);
|
||||
io_error = true;
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
let to_content = match read_file_contents(¶ms.to) {
|
||||
Ok(to_content) => to_content,
|
||||
Err(e) => {
|
||||
report_failure_to_read_input_file(¶ms.executable, ¶ms.to, &e);
|
||||
io_error = true;
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
if io_error {
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
|
||||
// run diff
|
||||
let result: Vec<u8> = match params.format {
|
||||
Format::Normal => normal_diff::diff(&from_content, &to_content, ¶ms),
|
||||
Format::Unified => unified_diff::diff(&from_content, &to_content, ¶ms),
|
||||
Format::Context => context_diff::diff(&from_content, &to_content, ¶ms),
|
||||
Format::Ed => ed_diff::diff(&from_content, &to_content, ¶ms).unwrap_or_else(|error| {
|
||||
eprintln!("{error}");
|
||||
exit(2);
|
||||
}),
|
||||
Format::SideBySide => {
|
||||
let mut output = stdout().lock();
|
||||
side_diff::diff(&from_content, &to_content, &mut output, ¶ms)
|
||||
}
|
||||
};
|
||||
if params.brief && !result.is_empty() {
|
||||
println!(
|
||||
"Files {} and {} differ",
|
||||
params.from.to_string_lossy(),
|
||||
params.to.to_string_lossy()
|
||||
);
|
||||
} else {
|
||||
io::stdout().write_all(&result).unwrap();
|
||||
}
|
||||
if result.is_empty() {
|
||||
maybe_report_identical_files();
|
||||
ExitCode::SUCCESS
|
||||
} else {
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
// This file is part of the uutils diffutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE-*
|
||||
// files that was distributed with this source code.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use crate::params::Params;
|
||||
use crate::utils::do_write_line;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Mismatch {
|
||||
pub line_number_expected: usize,
|
||||
pub line_number_actual: usize,
|
||||
pub expected: Vec<Vec<u8>>,
|
||||
pub actual: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum DiffError {
|
||||
MissingNL,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DiffError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||
std::fmt::Display::fmt("No newline at end of file", f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DiffError> for String {
|
||||
fn from(_: DiffError) -> String {
|
||||
"No newline at end of file".into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Mismatch {
|
||||
fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch {
|
||||
Mismatch {
|
||||
line_number_expected,
|
||||
line_number_actual,
|
||||
expected: Vec::new(),
|
||||
actual: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Produces a diff between the expected output and actual output.
|
||||
fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result<Vec<Mismatch>, DiffError> {
|
||||
let mut line_number_expected = 1;
|
||||
let mut line_number_actual = 1;
|
||||
let mut results = Vec::new();
|
||||
let mut mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||
|
||||
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
|
||||
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
|
||||
|
||||
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
|
||||
// ^ means that underflow here is impossible
|
||||
let _expected_lines_count = expected_lines.len() - 1;
|
||||
let _actual_lines_count = actual_lines.len() - 1;
|
||||
|
||||
if expected_lines.last() == Some(&&b""[..]) {
|
||||
expected_lines.pop();
|
||||
} else {
|
||||
return Err(DiffError::MissingNL);
|
||||
}
|
||||
|
||||
if actual_lines.last() == Some(&&b""[..]) {
|
||||
actual_lines.pop();
|
||||
} else {
|
||||
return Err(DiffError::MissingNL);
|
||||
}
|
||||
|
||||
for result in diff::slice(&expected_lines, &actual_lines) {
|
||||
match result {
|
||||
diff::Result::Left(str) => {
|
||||
if !mismatch.actual.is_empty() {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||
}
|
||||
mismatch.expected.push(str.to_vec());
|
||||
line_number_expected += 1;
|
||||
}
|
||||
diff::Result::Right(str) => {
|
||||
mismatch.actual.push(str.to_vec());
|
||||
line_number_actual += 1;
|
||||
}
|
||||
diff::Result::Both(_str, _) => {
|
||||
line_number_expected += 1;
|
||||
line_number_actual += 1;
|
||||
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||
} else {
|
||||
mismatch.line_number_expected = line_number_expected;
|
||||
mismatch.line_number_actual = line_number_actual;
|
||||
}
|
||||
}
|
||||
}
|
||||
if stop_early && !results.is_empty() {
|
||||
// Optimization: stop analyzing the files as soon as there are any differences
|
||||
return Ok(results);
|
||||
}
|
||||
}
|
||||
|
||||
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
|
||||
results.push(mismatch);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Result<Vec<u8>, DiffError> {
|
||||
let mut output = Vec::new();
|
||||
let diff_results = make_diff(expected, actual, params.brief)?;
|
||||
if params.brief && !diff_results.is_empty() {
|
||||
write!(&mut output, "\0").unwrap();
|
||||
return Ok(output);
|
||||
}
|
||||
let mut lines_offset = 0;
|
||||
for result in diff_results {
|
||||
let line_number_expected: isize = result.line_number_expected as isize + lines_offset;
|
||||
let _line_number_actual: isize = result.line_number_actual as isize + lines_offset;
|
||||
let expected_count: isize = result.expected.len() as isize;
|
||||
let actual_count: isize = result.actual.len() as isize;
|
||||
match (expected_count, actual_count) {
|
||||
(0, 0) => unreachable!(),
|
||||
(0, _) => writeln!(&mut output, "{}a", line_number_expected - 1).unwrap(),
|
||||
(_, 0) => writeln!(
|
||||
&mut output,
|
||||
"{},{}d",
|
||||
line_number_expected,
|
||||
expected_count + line_number_expected - 1
|
||||
)
|
||||
.unwrap(),
|
||||
(1, _) => writeln!(&mut output, "{line_number_expected}c").unwrap(),
|
||||
_ => writeln!(
|
||||
&mut output,
|
||||
"{},{}c",
|
||||
line_number_expected,
|
||||
expected_count + line_number_expected - 1
|
||||
)
|
||||
.unwrap(),
|
||||
}
|
||||
lines_offset += actual_count - expected_count;
|
||||
if actual_count != 0 {
|
||||
for actual in &result.actual {
|
||||
if actual == b"." {
|
||||
writeln!(&mut output, "..\n.\ns/.//\na").unwrap();
|
||||
} else {
|
||||
do_write_line(&mut output, actual, params.expand_tabs, params.tabsize).unwrap();
|
||||
writeln!(&mut output).unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(&mut output, ".").unwrap();
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::utils::testcmds::ED_CMD;
|
||||
|
||||
pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result<Vec<u8>, DiffError> {
|
||||
let mut output = diff(expected, actual, &Params::default())?;
|
||||
writeln!(&mut output, "w {filename}").unwrap();
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic() {
|
||||
let from = b"a\n";
|
||||
let to = b"b\n";
|
||||
let diff = diff(from, to, &Params::default()).unwrap();
|
||||
let expected = ["1c", "b", ".", ""].join("\n");
|
||||
assert_eq!(diff, expected.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations() {
|
||||
let target = "target/ed-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff_w(&alef, &bet, &format!("{target}/alef")).unwrap();
|
||||
File::create(format!("{target}/ab.ed"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(format!("{target}/alef")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(format!("{target}/bet")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
#[cfg(not(windows))] // there's no ed on windows
|
||||
{
|
||||
let output = ED_CMD
|
||||
.new()
|
||||
.arg(format!("{target}/alef"))
|
||||
.stdin(File::open(format!("{target}/ab.ed")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{output:?}");
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = std::fs::read(format!("{target}/alef")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_empty_lines() {
|
||||
let target = "target/ed-diff/";
|
||||
// test all possible six-line files with missing newlines.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff_w(&alef, &bet, &format!("{target}/alef_")).unwrap();
|
||||
File::create(format!("{target}/ab_.ed"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(format!("{target}/alef_")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(format!("{target}/bet_")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
#[cfg(not(windows))] // there's no ed on windows
|
||||
{
|
||||
let output = ED_CMD
|
||||
.new()
|
||||
.arg(format!("{target}/alef_"))
|
||||
.stdin(File::open(format!("{target}/ab_.ed")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{output:?}");
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = std::fs::read(format!("{target}/alef_")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_reverse() {
|
||||
let target = "target/ed-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"a\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"c\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"e\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff_w(&alef, &bet, &format!("{target}/alefr")).unwrap();
|
||||
File::create(format!("{target}/abr.ed"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(format!("{target}/alefr")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(format!("{target}/betr")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
#[cfg(not(windows))] // there's no ed on windows
|
||||
{
|
||||
let output = ED_CMD
|
||||
.new()
|
||||
.arg(format!("{target}/alefr"))
|
||||
.stdin(File::open(format!("{target}/abr.ed")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{output:?}");
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = std::fs::read(format!("{target}/alefr")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stop_early() {
|
||||
let from = ["a", "b", "c", ""].join("\n");
|
||||
let to = ["a", "d", "c", ""].join("\n");
|
||||
|
||||
let diff_full = diff(from.as_bytes(), to.as_bytes(), &Params::default()).unwrap();
|
||||
let expected_full = ["2c", "d", ".", ""].join("\n");
|
||||
assert_eq!(diff_full, expected_full.as_bytes());
|
||||
|
||||
let diff_brief = diff(
|
||||
from.as_bytes(),
|
||||
to.as_bytes(),
|
||||
&Params {
|
||||
brief: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let expected_brief = "\0".as_bytes();
|
||||
assert_eq!(diff_brief, expected_brief);
|
||||
|
||||
let nodiff_full = diff(from.as_bytes(), from.as_bytes(), &Params::default()).unwrap();
|
||||
assert!(nodiff_full.is_empty());
|
||||
|
||||
let nodiff_brief = diff(
|
||||
from.as_bytes(),
|
||||
from.as_bytes(),
|
||||
&Params {
|
||||
brief: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(nodiff_brief.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
pub mod cmp;
|
||||
pub mod context_diff;
|
||||
pub mod ed_diff;
|
||||
pub mod macros;
|
||||
pub mod normal_diff;
|
||||
pub mod params;
|
||||
pub mod side_diff;
|
||||
pub mod unified_diff;
|
||||
pub mod utils;
|
||||
|
||||
// Re-export the public functions/types you need
|
||||
pub use context_diff::diff as context_diff;
|
||||
pub use ed_diff::diff as ed_diff;
|
||||
pub use normal_diff::diff as normal_diff;
|
||||
pub use side_diff::diff as side_by_side_diff;
|
||||
pub use unified_diff::diff as unified_diff;
|
||||
@@ -0,0 +1,25 @@
|
||||
// asserts equality of the actual diff and expected diff
|
||||
// considering datetime varitations
|
||||
//
|
||||
// It replaces the modification time in the actual diff
|
||||
// with placeholder "TIMESTAMP" and then asserts the equality
|
||||
//
|
||||
// For eg.
|
||||
// let brief = "*** fruits_old.txt\t2024-03-24 23:43:05.189597645 +0530\n
|
||||
// --- fruits_new.txt\t2024-03-24 23:35:08.922581904 +0530\n";
|
||||
//
|
||||
// replaced = "*** fruits_old.txt\tTIMESTAMP\n
|
||||
// --- fruits_new.txt\tTIMESTAMP\n";
|
||||
#[macro_export]
|
||||
macro_rules! assert_diff_eq {
|
||||
($actual:expr, $expected:expr) => {{
|
||||
use regex::Regex;
|
||||
use std::str;
|
||||
|
||||
let diff = str::from_utf8(&$actual).unwrap();
|
||||
let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+ [+-]\d{4}").unwrap();
|
||||
let actual = re.replacen(diff, 2, "TIMESTAMP");
|
||||
|
||||
assert_eq!(actual, $expected);
|
||||
}};
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// This file is part of the uutils diffutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE-*
|
||||
// files that was distributed with this source code.
|
||||
|
||||
use std::{
|
||||
env::ArgsOs,
|
||||
ffi::{OsStr, OsString},
|
||||
iter::Peekable,
|
||||
path::{Path, PathBuf},
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
mod cmp;
|
||||
mod context_diff;
|
||||
mod diff;
|
||||
mod ed_diff;
|
||||
mod macros;
|
||||
mod normal_diff;
|
||||
mod params;
|
||||
mod side_diff;
|
||||
mod unified_diff;
|
||||
mod utils;
|
||||
|
||||
/// # Panics
|
||||
/// Panics if the binary path cannot be determined
|
||||
fn binary_path(args: &mut Peekable<ArgsOs>) -> PathBuf {
|
||||
match args.peek() {
|
||||
Some(ref s) if !s.is_empty() => PathBuf::from(s),
|
||||
_ => std::env::current_exe().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// #Panics
|
||||
/// Panics if path has no UTF-8 valid name
|
||||
fn name(binary_path: &Path) -> &OsStr {
|
||||
binary_path.file_stem().unwrap()
|
||||
}
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn usage(name: &str) {
|
||||
println!("{name} {VERSION} (multi-call binary)\n");
|
||||
println!("Usage: {name} [function [arguments...]]\n");
|
||||
println!("Currently defined functions:\n");
|
||||
println!(" cmp, diff\n");
|
||||
}
|
||||
|
||||
fn second_arg_error(name: &OsStr) -> ! {
|
||||
eprintln!("Expected utility name as second argument, got nothing.");
|
||||
usage(&name.to_string_lossy());
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let mut args = std::env::args_os().peekable();
|
||||
|
||||
let exe_path = binary_path(&mut args);
|
||||
let exe_name = name(&exe_path);
|
||||
|
||||
let util_name = if exe_name.as_encoded_bytes().ends_with(b"diffutils") {
|
||||
// Discard the item we peeked.
|
||||
let _ = args.next();
|
||||
|
||||
args.peek()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| second_arg_error(exe_name))
|
||||
} else {
|
||||
OsString::from(exe_name)
|
||||
};
|
||||
|
||||
match util_name.as_encoded_bytes() {
|
||||
name if name.ends_with(b"diff") => diff::main(args),
|
||||
name if name.ends_with(b"cmp") => cmp::main(args),
|
||||
name => {
|
||||
use std::io::{stderr, Write as _};
|
||||
let _ = writeln!(
|
||||
stderr(),
|
||||
"{}: utility not supported",
|
||||
String::from_utf8_lossy(name)
|
||||
);
|
||||
ExitCode::from(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
// This file is part of the uutils diffutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE-*
|
||||
// files that was distributed with this source code.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use crate::params::Params;
|
||||
use crate::utils::do_write_line;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Mismatch {
|
||||
pub line_number_expected: usize,
|
||||
pub line_number_actual: usize,
|
||||
pub expected: Vec<Vec<u8>>,
|
||||
pub actual: Vec<Vec<u8>>,
|
||||
pub expected_missing_nl: bool,
|
||||
pub actual_missing_nl: bool,
|
||||
}
|
||||
|
||||
impl Mismatch {
|
||||
fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch {
|
||||
Mismatch {
|
||||
line_number_expected,
|
||||
line_number_actual,
|
||||
expected: Vec::new(),
|
||||
actual: Vec::new(),
|
||||
expected_missing_nl: false,
|
||||
actual_missing_nl: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Produces a diff between the expected output and actual output.
|
||||
fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec<Mismatch> {
|
||||
let mut line_number_expected = 1;
|
||||
let mut line_number_actual = 1;
|
||||
let mut results = Vec::new();
|
||||
let mut mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||
|
||||
let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
|
||||
let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
|
||||
|
||||
debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
|
||||
// ^ means that underflow here is impossible
|
||||
let expected_lines_count = expected_lines.len() - 1;
|
||||
let actual_lines_count = actual_lines.len() - 1;
|
||||
|
||||
if expected_lines.last() == Some(&&b""[..]) {
|
||||
expected_lines.pop();
|
||||
}
|
||||
|
||||
if actual_lines.last() == Some(&&b""[..]) {
|
||||
actual_lines.pop();
|
||||
}
|
||||
|
||||
for result in diff::slice(&expected_lines, &actual_lines) {
|
||||
match result {
|
||||
diff::Result::Left(str) => {
|
||||
if !mismatch.actual.is_empty() && !mismatch.actual_missing_nl {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||
}
|
||||
mismatch.expected.push(str.to_vec());
|
||||
mismatch.expected_missing_nl = line_number_expected > expected_lines_count;
|
||||
line_number_expected += 1;
|
||||
}
|
||||
diff::Result::Right(str) => {
|
||||
mismatch.actual.push(str.to_vec());
|
||||
mismatch.actual_missing_nl = line_number_actual > actual_lines_count;
|
||||
line_number_actual += 1;
|
||||
}
|
||||
diff::Result::Both(str, _) => {
|
||||
match (
|
||||
line_number_expected > expected_lines_count,
|
||||
line_number_actual > actual_lines_count,
|
||||
) {
|
||||
(true, false) => {
|
||||
line_number_expected += 1;
|
||||
line_number_actual += 1;
|
||||
mismatch.expected.push(str.to_vec());
|
||||
mismatch.expected_missing_nl = true;
|
||||
mismatch.actual.push(str.to_vec());
|
||||
}
|
||||
(false, true) => {
|
||||
line_number_expected += 1;
|
||||
line_number_actual += 1;
|
||||
mismatch.actual.push(str.to_vec());
|
||||
mismatch.actual_missing_nl = true;
|
||||
mismatch.expected.push(str.to_vec());
|
||||
}
|
||||
(true, true) | (false, false) => {
|
||||
line_number_expected += 1;
|
||||
line_number_actual += 1;
|
||||
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
|
||||
results.push(mismatch);
|
||||
mismatch = Mismatch::new(line_number_expected, line_number_actual);
|
||||
} else {
|
||||
mismatch.line_number_expected = line_number_expected;
|
||||
mismatch.line_number_actual = line_number_actual;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if stop_early && !results.is_empty() {
|
||||
// Optimization: stop analyzing the files as soon as there are any differences
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
if !mismatch.actual.is_empty() || !mismatch.expected.is_empty() {
|
||||
results.push(mismatch);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
|
||||
// See https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Normal.html
|
||||
// for details on the syntax of the normal format.
|
||||
let mut output = Vec::new();
|
||||
let diff_results = make_diff(expected, actual, params.brief);
|
||||
if params.brief && !diff_results.is_empty() {
|
||||
write!(&mut output, "\0").unwrap();
|
||||
return output;
|
||||
}
|
||||
for result in diff_results {
|
||||
let line_number_expected = result.line_number_expected;
|
||||
let line_number_actual = result.line_number_actual;
|
||||
let expected_count = result.expected.len();
|
||||
let actual_count = result.actual.len();
|
||||
match (expected_count, actual_count) {
|
||||
(0, 0) => unreachable!(),
|
||||
(0, _) => writeln!(
|
||||
// 'a' stands for "Add lines"
|
||||
&mut output,
|
||||
"{}a{},{}",
|
||||
line_number_expected - 1,
|
||||
line_number_actual,
|
||||
line_number_actual + actual_count - 1
|
||||
)
|
||||
.unwrap(),
|
||||
(_, 0) => writeln!(
|
||||
// 'd' stands for "Delete lines"
|
||||
&mut output,
|
||||
"{},{}d{}",
|
||||
line_number_expected,
|
||||
expected_count + line_number_expected - 1,
|
||||
line_number_actual - 1
|
||||
)
|
||||
.unwrap(),
|
||||
(1, 1) => writeln!(
|
||||
// 'c' stands for "Change lines"
|
||||
// exactly one line replaced by one line
|
||||
&mut output,
|
||||
"{line_number_expected}c{line_number_actual}"
|
||||
)
|
||||
.unwrap(),
|
||||
(1, _) => writeln!(
|
||||
// one line replaced by multiple lines
|
||||
&mut output,
|
||||
"{}c{},{}",
|
||||
line_number_expected,
|
||||
line_number_actual,
|
||||
actual_count + line_number_actual - 1
|
||||
)
|
||||
.unwrap(),
|
||||
(_, 1) => writeln!(
|
||||
// multiple lines replaced by one line
|
||||
&mut output,
|
||||
"{},{}c{}",
|
||||
line_number_expected,
|
||||
expected_count + line_number_expected - 1,
|
||||
line_number_actual
|
||||
)
|
||||
.unwrap(),
|
||||
_ => writeln!(
|
||||
// general case: multiple lines replaced by multiple lines
|
||||
&mut output,
|
||||
"{},{}c{},{}",
|
||||
line_number_expected,
|
||||
expected_count + line_number_expected - 1,
|
||||
line_number_actual,
|
||||
actual_count + line_number_actual - 1
|
||||
)
|
||||
.unwrap(),
|
||||
}
|
||||
for expected in &result.expected {
|
||||
write!(&mut output, "< ").unwrap();
|
||||
do_write_line(&mut output, expected, params.expand_tabs, params.tabsize).unwrap();
|
||||
writeln!(&mut output).unwrap();
|
||||
}
|
||||
if result.expected_missing_nl {
|
||||
writeln!(&mut output, r"\ No newline at end of file").unwrap();
|
||||
}
|
||||
if expected_count != 0 && actual_count != 0 {
|
||||
writeln!(&mut output, "---").unwrap();
|
||||
}
|
||||
for actual in &result.actual {
|
||||
write!(&mut output, "> ").unwrap();
|
||||
do_write_line(&mut output, actual, params.expand_tabs, params.tabsize).unwrap();
|
||||
writeln!(&mut output).unwrap();
|
||||
}
|
||||
if result.actual_missing_nl {
|
||||
writeln!(&mut output, r"\ No newline at end of file").unwrap();
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::utils::testcmds::PATCH_CMD;
|
||||
|
||||
#[test]
|
||||
fn test_basic() {
|
||||
let mut a = Vec::new();
|
||||
a.write_all(b"a\n").unwrap();
|
||||
let mut b = Vec::new();
|
||||
b.write_all(b"b\n").unwrap();
|
||||
let diff = diff(&a, &b, &Params::default());
|
||||
let expected = b"1c1\n< a\n---\n> b\n".to_vec();
|
||||
assert_eq!(diff, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations() {
|
||||
let target = "target/normal-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet, &Params::default());
|
||||
File::create(format!("{target}/ab.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(format!("{target}/alef")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(format!("{target}/bet")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = PATCH_CMD
|
||||
.new()
|
||||
.arg("-p0")
|
||||
.arg(format!("{target}/alef"))
|
||||
.stdin(File::open(format!("{target}/ab.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{output:?}");
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(format!("{target}/alef")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_missing_line_ending() {
|
||||
let target = "target/normal-diff/";
|
||||
// test all possible six-line files with missing newlines.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
for &g in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
match g {
|
||||
0 => {
|
||||
alef.pop();
|
||||
}
|
||||
1 => {
|
||||
bet.pop();
|
||||
}
|
||||
2 => {
|
||||
alef.pop();
|
||||
bet.pop();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet, &Params::default());
|
||||
File::create(format!("{target}/abn.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(format!("{target}/alefn")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(format!("{target}/betn")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = PATCH_CMD
|
||||
.new()
|
||||
.arg("-p0")
|
||||
.arg("--normal")
|
||||
.arg(format!("{target}/alefn"))
|
||||
.stdin(File::open(format!("{target}/abn.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{output:?}");
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(format!("{target}/alefn")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_empty_lines() {
|
||||
let target = "target/normal-diff/";
|
||||
// test all possible six-line files with missing newlines.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"h\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"j\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"l\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet, &Params::default());
|
||||
File::create(format!("{target}/ab_.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(format!("{target}/alef_")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(format!("{target}/bet_")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = PATCH_CMD
|
||||
.new()
|
||||
.arg("-p0")
|
||||
.arg(format!("{target}/alef_"))
|
||||
.stdin(File::open(format!("{target}/ab_.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{output:?}");
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(format!("{target}/alef_")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permutations_reverse() {
|
||||
let target = "target/normal-diff/";
|
||||
// test all possible six-line files.
|
||||
let _ = std::fs::create_dir(target);
|
||||
for &a in &[0, 1, 2] {
|
||||
for &b in &[0, 1, 2] {
|
||||
for &c in &[0, 1, 2] {
|
||||
for &d in &[0, 1, 2] {
|
||||
for &e in &[0, 1, 2] {
|
||||
for &f in &[0, 1, 2] {
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
let mut alef = Vec::new();
|
||||
let mut bet = Vec::new();
|
||||
alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
|
||||
.unwrap();
|
||||
if a != 2 {
|
||||
bet.write_all(b"a\n").unwrap();
|
||||
}
|
||||
alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
|
||||
.unwrap();
|
||||
if b != 2 {
|
||||
bet.write_all(b"b\n").unwrap();
|
||||
}
|
||||
alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
|
||||
.unwrap();
|
||||
if c != 2 {
|
||||
bet.write_all(b"c\n").unwrap();
|
||||
}
|
||||
alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
|
||||
.unwrap();
|
||||
if d != 2 {
|
||||
bet.write_all(b"d\n").unwrap();
|
||||
}
|
||||
alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
|
||||
.unwrap();
|
||||
if e != 2 {
|
||||
bet.write_all(b"e\n").unwrap();
|
||||
}
|
||||
alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
|
||||
.unwrap();
|
||||
if f != 2 {
|
||||
bet.write_all(b"f\n").unwrap();
|
||||
}
|
||||
// This test diff is intentionally reversed.
|
||||
// We want it to turn the alef into bet.
|
||||
let diff = diff(&alef, &bet, &Params::default());
|
||||
File::create(format!("{target}/abr.diff"))
|
||||
.unwrap()
|
||||
.write_all(&diff)
|
||||
.unwrap();
|
||||
let mut fa = File::create(format!("{target}/alefr")).unwrap();
|
||||
fa.write_all(&alef[..]).unwrap();
|
||||
let mut fb = File::create(format!("{target}/betr")).unwrap();
|
||||
fb.write_all(&bet[..]).unwrap();
|
||||
let _ = fa;
|
||||
let _ = fb;
|
||||
let output = PATCH_CMD
|
||||
.new()
|
||||
.arg("-p0")
|
||||
.arg(format!("{target}/alefr"))
|
||||
.stdin(File::open(format!("{target}/abr.diff")).unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{output:?}");
|
||||
//println!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
//println!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
let alef = fs::read(format!("{target}/alefr")).unwrap();
|
||||
assert_eq!(alef, bet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stop_early() {
|
||||
let from = ["a", "b", "c"].join("\n");
|
||||
let to = ["a", "d", "c"].join("\n");
|
||||
|
||||
let diff_full = diff(from.as_bytes(), to.as_bytes(), &Params::default());
|
||||
let expected_full = ["2c2", "< b", "---", "> d", ""].join("\n");
|
||||
assert_eq!(diff_full, expected_full.as_bytes());
|
||||
|
||||
let diff_brief = diff(
|
||||
from.as_bytes(),
|
||||
to.as_bytes(),
|
||||
&Params {
|
||||
brief: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let expected_brief = "\0".as_bytes();
|
||||
assert_eq!(diff_brief, expected_brief);
|
||||
|
||||
let nodiff_full = diff(from.as_bytes(), from.as_bytes(), &Params::default());
|
||||
assert!(nodiff_full.is_empty());
|
||||
|
||||
let nodiff_brief = diff(
|
||||
from.as_bytes(),
|
||||
from.as_bytes(),
|
||||
&Params {
|
||||
brief: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(nodiff_brief.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,975 @@
|
||||
use std::ffi::OsString;
|
||||
use std::iter::Peekable;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum Format {
|
||||
#[default]
|
||||
Normal,
|
||||
Unified,
|
||||
Context,
|
||||
Ed,
|
||||
SideBySide,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Params {
|
||||
pub executable: OsString,
|
||||
pub from: OsString,
|
||||
pub to: OsString,
|
||||
pub format: Format,
|
||||
pub context_count: usize,
|
||||
pub report_identical_files: bool,
|
||||
pub brief: bool,
|
||||
pub recursive: bool,
|
||||
pub expand_tabs: bool,
|
||||
pub tabsize: usize,
|
||||
pub width: usize,
|
||||
}
|
||||
|
||||
impl Default for Params {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
executable: OsString::default(),
|
||||
from: OsString::default(),
|
||||
to: OsString::default(),
|
||||
format: Format::default(),
|
||||
context_count: 3,
|
||||
report_identical_files: false,
|
||||
brief: false,
|
||||
recursive: false,
|
||||
expand_tabs: false,
|
||||
tabsize: 8,
|
||||
width: 130,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Result<Params, String> {
|
||||
// parse CLI
|
||||
|
||||
let Some(executable) = opts.next() else {
|
||||
return Err("Usage: <exe> <from> <to>".to_string());
|
||||
};
|
||||
let mut params = Params {
|
||||
executable,
|
||||
..Default::default()
|
||||
};
|
||||
let mut from = None;
|
||||
let mut to = None;
|
||||
let mut format = None;
|
||||
let mut context = None;
|
||||
let tabsize_re = Regex::new(r"^--tabsize=(?<num>\d+)$").unwrap();
|
||||
let width_re = Regex::new(r"^--width=(?P<long>\d+)$").unwrap();
|
||||
while let Some(param) = opts.next() {
|
||||
let next_param = opts.peek();
|
||||
if param == "--" {
|
||||
break;
|
||||
}
|
||||
if param == "-" {
|
||||
if from.is_none() {
|
||||
from = Some(param);
|
||||
} else if to.is_none() {
|
||||
to = Some(param);
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Usage: {} <from> <to>",
|
||||
params.executable.to_string_lossy()
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if param == "-r" || param == "--recursive" {
|
||||
params.recursive = true;
|
||||
continue;
|
||||
}
|
||||
if param == "-s" || param == "--report-identical-files" {
|
||||
params.report_identical_files = true;
|
||||
continue;
|
||||
}
|
||||
if param == "-q" || param == "--brief" {
|
||||
params.brief = true;
|
||||
continue;
|
||||
}
|
||||
if param == "-t" || param == "--expand-tabs" {
|
||||
params.expand_tabs = true;
|
||||
continue;
|
||||
}
|
||||
if param == "--normal" {
|
||||
if format.is_some() && format != Some(Format::Normal) {
|
||||
return Err("Conflicting output style options".to_string());
|
||||
}
|
||||
format = Some(Format::Normal);
|
||||
continue;
|
||||
}
|
||||
if param == "-e" || param == "--ed" {
|
||||
if format.is_some() && format != Some(Format::Ed) {
|
||||
return Err("Conflicting output style options".to_string());
|
||||
}
|
||||
format = Some(Format::Ed);
|
||||
continue;
|
||||
}
|
||||
if param == "-y" || param == "--side-by-side" {
|
||||
if format.is_some() && format != Some(Format::SideBySide) {
|
||||
return Err("Conflicting output style option".to_string());
|
||||
}
|
||||
format = Some(Format::SideBySide);
|
||||
continue;
|
||||
}
|
||||
if width_re.is_match(param.to_string_lossy().as_ref()) {
|
||||
let param = param.into_string().unwrap();
|
||||
let width_str: &str = width_re
|
||||
.captures(param.as_str())
|
||||
.unwrap()
|
||||
.name("long")
|
||||
.unwrap()
|
||||
.as_str();
|
||||
|
||||
params.width = match width_str.parse::<usize>() {
|
||||
Ok(num) => {
|
||||
if num == 0 {
|
||||
return Err("invalid width «0»".to_string());
|
||||
}
|
||||
|
||||
num
|
||||
}
|
||||
Err(_) => return Err(format!("invalid width «{width_str}»")),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if tabsize_re.is_match(param.to_string_lossy().as_ref()) {
|
||||
// Because param matches the regular expression,
|
||||
// it is safe to assume it is valid UTF-8.
|
||||
let param = param.into_string().unwrap();
|
||||
let tabsize_str = tabsize_re
|
||||
.captures(param.as_str())
|
||||
.unwrap()
|
||||
.name("num")
|
||||
.unwrap()
|
||||
.as_str();
|
||||
params.tabsize = match tabsize_str.parse::<usize>() {
|
||||
Ok(num) => {
|
||||
if num == 0 {
|
||||
return Err("invalid tabsize «0»".to_string());
|
||||
}
|
||||
|
||||
num
|
||||
}
|
||||
Err(_) => return Err(format!("invalid tabsize «{tabsize_str}»")),
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
match match_context_diff_params(¶m, next_param, format) {
|
||||
Ok(DiffStyleMatch {
|
||||
is_match,
|
||||
context_count,
|
||||
next_param_consumed,
|
||||
}) => {
|
||||
if is_match {
|
||||
format = Some(Format::Context);
|
||||
if context_count.is_some() {
|
||||
context = context_count;
|
||||
}
|
||||
if next_param_consumed {
|
||||
opts.next();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
match match_unified_diff_params(¶m, next_param, format) {
|
||||
Ok(DiffStyleMatch {
|
||||
is_match,
|
||||
context_count,
|
||||
next_param_consumed,
|
||||
}) => {
|
||||
if is_match {
|
||||
format = Some(Format::Unified);
|
||||
if context_count.is_some() {
|
||||
context = context_count;
|
||||
}
|
||||
if next_param_consumed {
|
||||
opts.next();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
if param.to_string_lossy().starts_with('-') {
|
||||
return Err(format!("unrecognized option '{}'", param.to_string_lossy()));
|
||||
}
|
||||
if from.is_none() {
|
||||
from = Some(param);
|
||||
} else if to.is_none() {
|
||||
to = Some(param);
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Usage: {} <from> <to>",
|
||||
params.executable.to_string_lossy()
|
||||
));
|
||||
}
|
||||
}
|
||||
params.from = if let Some(from) = from {
|
||||
from
|
||||
} else if let Some(param) = opts.next() {
|
||||
param
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Usage: {} <from> <to>",
|
||||
params.executable.to_string_lossy()
|
||||
));
|
||||
};
|
||||
params.to = if let Some(to) = to {
|
||||
to
|
||||
} else if let Some(param) = opts.next() {
|
||||
param
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Usage: {} <from> <to>",
|
||||
params.executable.to_string_lossy()
|
||||
));
|
||||
};
|
||||
|
||||
// diff DIRECTORY FILE => diff DIRECTORY/FILE FILE
|
||||
// diff FILE DIRECTORY => diff FILE DIRECTORY/FILE
|
||||
let mut from_path: PathBuf = PathBuf::from(¶ms.from);
|
||||
let mut to_path: PathBuf = PathBuf::from(¶ms.to);
|
||||
|
||||
if from_path.is_dir() && to_path.is_file() {
|
||||
from_path.push(to_path.file_name().unwrap());
|
||||
params.from = from_path.into_os_string();
|
||||
} else if from_path.is_file() && to_path.is_dir() {
|
||||
to_path.push(from_path.file_name().unwrap());
|
||||
params.to = to_path.into_os_string();
|
||||
}
|
||||
|
||||
params.format = format.unwrap_or(Format::default());
|
||||
if let Some(context_count) = context {
|
||||
params.context_count = context_count;
|
||||
}
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
struct DiffStyleMatch {
|
||||
is_match: bool,
|
||||
context_count: Option<usize>,
|
||||
next_param_consumed: bool,
|
||||
}
|
||||
|
||||
fn match_context_diff_params(
|
||||
param: &OsString,
|
||||
next_param: Option<&OsString>,
|
||||
format: Option<Format>,
|
||||
) -> Result<DiffStyleMatch, String> {
|
||||
const CONTEXT_RE: &str = r"^(-[cC](?<num1>\d*)|--context(=(?<num2>\d*))?|-(?<num3>\d+)c)$";
|
||||
let regex = Regex::new(CONTEXT_RE).unwrap();
|
||||
let is_match = regex.is_match(param.to_string_lossy().as_ref());
|
||||
let mut context_count = None;
|
||||
let mut next_param_consumed = false;
|
||||
if is_match {
|
||||
if format.is_some() && format != Some(Format::Context) {
|
||||
return Err("Conflicting output style options".to_string());
|
||||
}
|
||||
let captures = regex.captures(param.to_str().unwrap()).unwrap();
|
||||
let num = captures
|
||||
.name("num1")
|
||||
.or(captures.name("num2"))
|
||||
.or(captures.name("num3"));
|
||||
if let Some(numvalue) = num {
|
||||
if !numvalue.as_str().is_empty() {
|
||||
context_count = Some(numvalue.as_str().parse::<usize>().unwrap());
|
||||
}
|
||||
}
|
||||
if param == "-C" {
|
||||
if let Some(p) = next_param {
|
||||
let size_str = p.to_string_lossy();
|
||||
match size_str.parse::<usize>() {
|
||||
Ok(context_size) => {
|
||||
context_count = Some(context_size);
|
||||
next_param_consumed = true;
|
||||
}
|
||||
Err(_) => return Err(format!("invalid context length '{size_str}'")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(DiffStyleMatch {
|
||||
is_match,
|
||||
context_count,
|
||||
next_param_consumed,
|
||||
})
|
||||
}
|
||||
|
||||
fn match_unified_diff_params(
|
||||
param: &OsString,
|
||||
next_param: Option<&OsString>,
|
||||
format: Option<Format>,
|
||||
) -> Result<DiffStyleMatch, String> {
|
||||
const UNIFIED_RE: &str = r"^(-[uU](?<num1>\d*)|--unified(=(?<num2>\d*))?|-(?<num3>\d+)u)$";
|
||||
let regex = Regex::new(UNIFIED_RE).unwrap();
|
||||
let is_match = regex.is_match(param.to_string_lossy().as_ref());
|
||||
let mut context_count = None;
|
||||
let mut next_param_consumed = false;
|
||||
if is_match {
|
||||
if format.is_some() && format != Some(Format::Unified) {
|
||||
return Err("Conflicting output style options".to_string());
|
||||
}
|
||||
let captures = regex.captures(param.to_str().unwrap()).unwrap();
|
||||
let num = captures
|
||||
.name("num1")
|
||||
.or(captures.name("num2"))
|
||||
.or(captures.name("num3"));
|
||||
if let Some(numvalue) = num {
|
||||
if !numvalue.as_str().is_empty() {
|
||||
context_count = Some(numvalue.as_str().parse::<usize>().unwrap());
|
||||
}
|
||||
}
|
||||
if param == "-U" {
|
||||
if let Some(p) = next_param {
|
||||
let size_str = p.to_string_lossy();
|
||||
match size_str.parse::<usize>() {
|
||||
Ok(context_size) => {
|
||||
context_count = Some(context_size);
|
||||
next_param_consumed = true;
|
||||
}
|
||||
Err(_) => return Err(format!("invalid context length '{size_str}'")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(DiffStyleMatch {
|
||||
is_match,
|
||||
context_count,
|
||||
next_param_consumed,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn os(s: &str) -> OsString {
|
||||
OsString::from(s)
|
||||
}
|
||||
#[test]
|
||||
fn basics() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("--normal"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn basics_ed() {
|
||||
for arg in ["-e", "--ed"] {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Ed,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os(arg), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn context_valid() {
|
||||
for args in [vec!["-c"], vec!["--context"], vec!["--context="]] {
|
||||
let mut params = vec!["diff"];
|
||||
params.extend(args);
|
||||
params.extend(["foo", "bar"]);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Context,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(params.iter().map(|x| os(x)).peekable())
|
||||
);
|
||||
}
|
||||
for args in [
|
||||
vec!["-c42"],
|
||||
vec!["-C42"],
|
||||
vec!["-C", "42"],
|
||||
vec!["--context=42"],
|
||||
vec!["-42c"],
|
||||
] {
|
||||
let mut params = vec!["diff"];
|
||||
params.extend(args);
|
||||
params.extend(["foo", "bar"]);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Context,
|
||||
context_count: 42,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(params.iter().map(|x| os(x)).peekable())
|
||||
);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn context_invalid() {
|
||||
for args in [
|
||||
vec!["-c", "42"],
|
||||
vec!["-c=42"],
|
||||
vec!["-c="],
|
||||
vec!["-C"],
|
||||
vec!["-C=42"],
|
||||
vec!["-C="],
|
||||
vec!["--context42"],
|
||||
vec!["--context", "42"],
|
||||
vec!["-42C"],
|
||||
] {
|
||||
let mut params = vec!["diff"];
|
||||
params.extend(args);
|
||||
params.extend(["foo", "bar"]);
|
||||
assert!(parse_params(params.iter().map(|x| os(x)).peekable()).is_err());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn unified_valid() {
|
||||
for args in [vec!["-u"], vec!["--unified"], vec!["--unified="]] {
|
||||
let mut params = vec!["diff"];
|
||||
params.extend(args);
|
||||
params.extend(["foo", "bar"]);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Unified,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(params.iter().map(|x| os(x)).peekable())
|
||||
);
|
||||
}
|
||||
for args in [
|
||||
vec!["-u42"],
|
||||
vec!["-U42"],
|
||||
vec!["-U", "42"],
|
||||
vec!["--unified=42"],
|
||||
vec!["-42u"],
|
||||
] {
|
||||
let mut params = vec!["diff"];
|
||||
params.extend(args);
|
||||
params.extend(["foo", "bar"]);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Unified,
|
||||
context_count: 42,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(params.iter().map(|x| os(x)).peekable())
|
||||
);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn unified_invalid() {
|
||||
for args in [
|
||||
vec!["-u", "42"],
|
||||
vec!["-u=42"],
|
||||
vec!["-u="],
|
||||
vec!["-U"],
|
||||
vec!["-U=42"],
|
||||
vec!["-U="],
|
||||
vec!["--unified42"],
|
||||
vec!["--unified", "42"],
|
||||
vec!["-42U"],
|
||||
] {
|
||||
let mut params = vec!["diff"];
|
||||
params.extend(args);
|
||||
params.extend(["foo", "bar"]);
|
||||
assert!(parse_params(params.iter().map(|x| os(x)).peekable()).is_err());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn context_count() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Unified,
|
||||
context_count: 54,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-u54"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Unified,
|
||||
context_count: 54,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-U54"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Unified,
|
||||
context_count: 54,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-U"), os("54"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
format: Format::Context,
|
||||
context_count: 54,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-c54"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn report_identical_files() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
report_identical_files: true,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-s"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
report_identical_files: true,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[
|
||||
os("diff"),
|
||||
os("--report-identical-files"),
|
||||
os("foo"),
|
||||
os("bar"),
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn brief() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
brief: true,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("-q"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
brief: true,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("--brief"), os("foo"), os("bar"),]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn expand_tabs() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
for option in ["-t", "--expand-tabs"] {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
expand_tabs: true,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os(option), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn tabsize() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
tabsize: 1,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("--tabsize=1"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
tabsize: 42,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("--tabsize=42"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
assert!(parse_params(
|
||||
[os("diff"), os("--tabsize"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
.is_err());
|
||||
assert!(parse_params(
|
||||
[os("diff"), os("--tabsize="), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
.is_err());
|
||||
assert!(parse_params(
|
||||
[os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
.is_err());
|
||||
assert!(parse_params(
|
||||
[os("diff"), os("--tabsize=-1"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
.is_err());
|
||||
assert!(parse_params(
|
||||
[os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
.is_err());
|
||||
assert!(parse_params(
|
||||
[
|
||||
os("diff"),
|
||||
os("--tabsize=92233720368547758088"),
|
||||
os("foo"),
|
||||
os("bar")
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
#[test]
|
||||
fn width() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("bar"),
|
||||
width: 100,
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("--width=100"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn width_suffix_is_an_operand() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("xyz--width=5"),
|
||||
to: os("foo"),
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("xyz--width=5"), os("foo")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
}
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn width_non_utf8_is_not_an_option() {
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
// used to panic in into_string().unwrap()
|
||||
let bad = OsString::from_vec(b"\xff--width=5".to_vec());
|
||||
let params = parse_params(
|
||||
[os("diff"), bad.clone(), os("foo")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(params.from, bad);
|
||||
}
|
||||
#[test]
|
||||
fn double_dash() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("-g"),
|
||||
to: os("-h"),
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params(
|
||||
[os("diff"), os("--"), os("-g"), os("-h")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn default_to_stdin() {
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("foo"),
|
||||
to: os("-"),
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params([os("diff"), os("foo"), os("-")].iter().cloned().peekable())
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("-"),
|
||||
to: os("bar"),
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params([os("diff"), os("-"), os("bar")].iter().cloned().peekable())
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(Params {
|
||||
executable: os("diff"),
|
||||
from: os("-"),
|
||||
to: os("-"),
|
||||
..Default::default()
|
||||
}),
|
||||
parse_params([os("diff"), os("-"), os("-")].iter().cloned().peekable())
|
||||
);
|
||||
assert!(parse_params(
|
||||
[os("diff"), os("foo"), os("bar"), os("-")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
.is_err());
|
||||
assert!(parse_params(
|
||||
[os("diff"), os("-"), os("-"), os("-")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
#[test]
|
||||
fn missing_arguments() {
|
||||
assert!(parse_params([os("diff")].iter().cloned().peekable()).is_err());
|
||||
assert!(parse_params([os("diff"), os("foo")].iter().cloned().peekable()).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn unknown_argument() {
|
||||
assert!(parse_params(
|
||||
[os("diff"), os("-g"), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
.is_err());
|
||||
assert!(
|
||||
parse_params([os("diff"), os("-g"), os("bar")].iter().cloned().peekable()).is_err()
|
||||
);
|
||||
assert!(parse_params([os("diff"), os("-g")].iter().cloned().peekable()).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert!(parse_params([].iter().cloned().peekable()).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn conflicting_output_styles() {
|
||||
for (arg1, arg2) in [
|
||||
("-u", "-c"),
|
||||
("-u", "-e"),
|
||||
("-c", "-u"),
|
||||
("-c", "-U42"),
|
||||
("-u", "--normal"),
|
||||
("--normal", "-e"),
|
||||
("--context", "--normal"),
|
||||
] {
|
||||
assert!(parse_params(
|
||||
[os("diff"), os(arg1), os(arg2), os("foo"), os("bar")]
|
||||
.iter()
|
||||
.cloned()
|
||||
.peekable()
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,300 @@
|
||||
// This file is part of the uutils diffutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE-*
|
||||
// files that was distributed with this source code.
|
||||
|
||||
use regex::Regex;
|
||||
use std::{ffi::OsString, io::Write};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// Replace tabs by spaces in the input line.
|
||||
/// Correctly handle multi-bytes characters.
|
||||
/// This assumes that line does not contain any line breaks (if it does, the result is undefined).
|
||||
#[must_use]
|
||||
pub fn do_expand_tabs(line: &[u8], tabsize: usize) -> Vec<u8> {
|
||||
let tab = b'\t';
|
||||
let ntabs = line.iter().filter(|c| **c == tab).count();
|
||||
if ntabs == 0 {
|
||||
return line.to_vec();
|
||||
}
|
||||
let mut result = Vec::with_capacity(line.len() + ntabs * (tabsize - 1));
|
||||
let mut offset = 0;
|
||||
|
||||
let mut iter = line.split(|c| *c == tab).peekable();
|
||||
while let Some(chunk) = iter.next() {
|
||||
match String::from_utf8(chunk.to_vec()) {
|
||||
Ok(s) => offset += UnicodeWidthStr::width(s.as_str()),
|
||||
Err(_) => offset += chunk.len(),
|
||||
}
|
||||
result.extend_from_slice(chunk);
|
||||
if iter.peek().is_some() {
|
||||
result.resize(result.len() + tabsize - offset % tabsize, b' ');
|
||||
offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Write a single line to an output stream, expanding tabs to space if necessary.
|
||||
/// This assumes that line does not contain any line breaks
|
||||
/// (if it does and tabs are to be expanded to spaces, the result is undefined).
|
||||
pub fn do_write_line(
|
||||
output: &mut Vec<u8>,
|
||||
line: &[u8],
|
||||
expand_tabs: bool,
|
||||
tabsize: usize,
|
||||
) -> std::io::Result<()> {
|
||||
if expand_tabs {
|
||||
output.write_all(do_expand_tabs(line, tabsize).as_slice())
|
||||
} else {
|
||||
output.write_all(line)
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the modification time of the input file specified by file path
|
||||
/// If an error occurs, it returns the current system time
|
||||
pub fn get_modification_time(file_path: &str) -> String {
|
||||
use chrono::{DateTime, Local};
|
||||
use std::fs;
|
||||
use std::time::SystemTime;
|
||||
|
||||
let modification_time: SystemTime = fs::metadata(file_path)
|
||||
.and_then(|m| m.modified())
|
||||
.unwrap_or(SystemTime::now());
|
||||
|
||||
let modification_time: DateTime<Local> = modification_time.into();
|
||||
let modification_time: String = modification_time
|
||||
.format("%Y-%m-%d %H:%M:%S%.9f %z")
|
||||
.to_string();
|
||||
|
||||
modification_time
|
||||
}
|
||||
|
||||
pub fn format_failure_to_read_input_file(
|
||||
executable: &OsString,
|
||||
filepath: &OsString,
|
||||
error: &std::io::Error,
|
||||
) -> String {
|
||||
// std::io::Error's display trait outputs "{detail} (os error {code})"
|
||||
// but we want only the {detail} (error string) part
|
||||
let error_code_re = Regex::new(r"\ \(os\ error\ \d+\)$").unwrap();
|
||||
format!(
|
||||
"{}: {}: {}",
|
||||
executable.to_string_lossy(),
|
||||
filepath.to_string_lossy(),
|
||||
error_code_re.replace(error.to_string().as_str(), ""),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn report_failure_to_read_input_file(
|
||||
executable: &OsString,
|
||||
filepath: &OsString,
|
||||
error: &std::io::Error,
|
||||
) {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format_failure_to_read_input_file(executable, filepath, error)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod testcmds {
|
||||
// Command construction wrapper that provides some validation and non-obscure, "fail fast"
|
||||
// feedback and error messages.
|
||||
use std::any::Any;
|
||||
use std::io::Write;
|
||||
use std::panic::catch_unwind;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub struct CmdFactory {
|
||||
cmd: &'static str,
|
||||
validated_once: LazyLock<Result<(), String>>,
|
||||
validate: fn(&CmdFactory) -> (),
|
||||
}
|
||||
|
||||
impl CmdFactory {
|
||||
pub fn new(&self) -> Command {
|
||||
match &*self.validated_once {
|
||||
Ok(()) => Command::new(self.cmd),
|
||||
Err(errmsg) => panic!(
|
||||
"'{}' validation failed in earlier thread/test: {}",
|
||||
self.cmd, errmsg
|
||||
),
|
||||
}
|
||||
}
|
||||
// "self" is not compatible with static initialization
|
||||
fn try_catch_validate(cmd: &CmdFactory) -> Result<(), String> {
|
||||
// Note catch_unwind() does _not_ hide error messages, stack traces, etc.
|
||||
catch_unwind(|| {
|
||||
let _ = (cmd.validate)(cmd);
|
||||
})
|
||||
.map_err(find_panic_message)
|
||||
}
|
||||
}
|
||||
|
||||
fn find_panic_message(payload: Box<dyn Any + Send>) -> String {
|
||||
// https://github.com/rust-lang/rust/blob/1.95.0/library/std/src/panicking.rs#L771
|
||||
if let Some(&s) = payload.downcast_ref::<&'static str>() {
|
||||
String::from(s)
|
||||
} else if let Some(s) = payload.downcast_ref::<String>() {
|
||||
s.clone()
|
||||
} else {
|
||||
format!(
|
||||
"Unusual panic payload type {:?}, look for the first thread/test that failed",
|
||||
payload.type_id(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub static PATCH_CMD: CmdFactory = CmdFactory {
|
||||
cmd: if cfg!(target_os = "macos") {
|
||||
"gpatch" // brew install gpatch
|
||||
} else {
|
||||
"patch"
|
||||
},
|
||||
validated_once: LazyLock::new(|| CmdFactory::try_catch_validate(&PATCH_CMD)),
|
||||
|
||||
validate: (|myself| {
|
||||
let output = Command::new(myself.cmd)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.expect(format!("`{} --version` failed", myself.cmd).as_str());
|
||||
// Non-GNU versions have subtle differences. When some newlines are missing in some test
|
||||
// patches, the macOS version can even stall the whole test run.
|
||||
assert!(output.stdout.starts_with(b"GNU patch"));
|
||||
assert!(output.status.success());
|
||||
}),
|
||||
};
|
||||
|
||||
pub static ED_CMD: CmdFactory = CmdFactory {
|
||||
cmd: "ed",
|
||||
validated_once: LazyLock::new(|| CmdFactory::try_catch_validate(&ED_CMD)),
|
||||
|
||||
validate: (|myself| {
|
||||
let mut child = Command::new(myself.cmd)
|
||||
.arg("!echo hello_ed")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("Failed to start 'ed' command");
|
||||
|
||||
let mut stdin = child.stdin.take().unwrap();
|
||||
writeln!(stdin, "1p\nq").expect("Failed to send command to 'ed'");
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.expect("Failed to read 'ed' stdout");
|
||||
assert_eq!(String::from_utf8_lossy(&output.stdout), "9\nhello_ed\n");
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod expand_tabs {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn assert_tab_expansion(line: &str, tabsize: usize, expected: &str) {
|
||||
assert_eq!(
|
||||
do_expand_tabs(line.as_bytes(), tabsize),
|
||||
expected.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basics() {
|
||||
assert_tab_expansion("foo barr baz", 8, "foo barr baz");
|
||||
assert_tab_expansion("foo\tbarr\tbaz", 8, "foo barr baz");
|
||||
assert_tab_expansion("foo\tbarr\tbaz", 5, "foo barr baz");
|
||||
assert_tab_expansion("foo\tbarr\tbaz", 2, "foo barr baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multibyte_chars() {
|
||||
assert_tab_expansion("foo\tépée\tbaz", 8, "foo épée baz");
|
||||
assert_tab_expansion("foo\t😉\tbaz", 5, "foo 😉 baz");
|
||||
|
||||
// Note: The Woman Scientist emoji (👩🔬) is a ZWJ sequence combining
|
||||
// the Woman emoji (👩) and the Microscope emoji (🔬). On supported platforms
|
||||
// it is displayed as a single emoji and has a print size of 2 columns.
|
||||
// Terminal emulators tend to not support this, and display the two emojis
|
||||
// side by side, thus accounting for a print size of 4 columns, but the
|
||||
// unicode_width crate reports a correct size of 2.
|
||||
assert_tab_expansion("foo\t👩🔬\tbaz", 6, "foo 👩🔬 baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_utf8() {
|
||||
// [240, 240, 152, 137] is an invalid UTF-8 sequence, so it is handled as 4 bytes
|
||||
assert_eq!(
|
||||
do_expand_tabs(&[240, 240, 152, 137, 9, 102, 111, 111], 8),
|
||||
&[240, 240, 152, 137, 32, 32, 32, 32, 102, 111, 111]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod write_line {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn assert_line_written(line: &str, expand_tabs: bool, tabsize: usize, expected: &str) {
|
||||
let mut output: Vec<u8> = Vec::new();
|
||||
assert!(do_write_line(&mut output, line.as_bytes(), expand_tabs, tabsize).is_ok());
|
||||
assert_eq!(output, expected.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basics() {
|
||||
assert_line_written("foo bar baz", false, 8, "foo bar baz");
|
||||
assert_line_written("foo bar\tbaz", false, 8, "foo bar\tbaz");
|
||||
assert_line_written("foo bar\tbaz", true, 8, "foo bar baz");
|
||||
}
|
||||
}
|
||||
|
||||
mod modification_time {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn set_time() {
|
||||
use chrono::{DateTime, Local};
|
||||
use std::time::SystemTime;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
let temp = NamedTempFile::new().unwrap();
|
||||
// set file modification time equal to current time
|
||||
let current = SystemTime::now();
|
||||
let _ = temp.as_file().set_modified(current);
|
||||
|
||||
// format current time
|
||||
let current: DateTime<Local> = current.into();
|
||||
let current: String = current.format("%Y-%m-%d %H:%M:%S%.9f %z").to_string();
|
||||
|
||||
// verify
|
||||
assert_eq!(
|
||||
current,
|
||||
get_modification_time(&temp.path().to_string_lossy())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_file() {
|
||||
use chrono::{DateTime, Local};
|
||||
use std::time::SystemTime;
|
||||
|
||||
let invalid_file = "target/utils/invalid-file";
|
||||
|
||||
// store current time before calling `get_modification_time`
|
||||
// Because the file is invalid, it will return SystemTime::now()
|
||||
// which will be greater than previously saved time
|
||||
let current_time: DateTime<Local> = SystemTime::now().into();
|
||||
let m_time: DateTime<Local> = get_modification_time(invalid_file).parse().unwrap();
|
||||
|
||||
assert!(m_time > current_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,945 @@
|
||||
// This file is part of the uutils diffutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE-*
|
||||
// files that was distributed with this source code.
|
||||
|
||||
use assert_cmd::cargo::cargo_bin_cmd;
|
||||
use predicates::prelude::*;
|
||||
use std::fs::File;
|
||||
#[cfg(not(windows))]
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use tempfile::{tempdir, NamedTempFile};
|
||||
|
||||
// Integration tests for the diffutils command
|
||||
mod common {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unknown_param() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("patch");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(2))
|
||||
.failure()
|
||||
.stderr(predicate::eq("patch: utility not supported\n"));
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(0))
|
||||
.success()
|
||||
.stderr(predicate::str::starts_with(
|
||||
"Expected utility name as second argument, got nothing.\n",
|
||||
));
|
||||
|
||||
for subcmd in ["diff", "cmp", "uu-diff", "uucmp"] {
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg(subcmd);
|
||||
cmd.arg("--foobar");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(2))
|
||||
.failure()
|
||||
.stderr(predicate::str::contains("unrecognized option '--foobar'"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_read_files() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file = NamedTempFile::new()?;
|
||||
|
||||
let nofile = NamedTempFile::new()?;
|
||||
let nopath = nofile.into_temp_path();
|
||||
std::fs::remove_file(&nopath)?;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let error_message = "No such file or directory";
|
||||
#[cfg(windows)]
|
||||
let error_message = "The system cannot find the file specified.";
|
||||
|
||||
for subcmd in ["diff", "cmp"] {
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg(subcmd);
|
||||
cmd.arg(&nopath).arg(file.path());
|
||||
cmd.assert()
|
||||
.code(predicate::eq(2))
|
||||
.failure()
|
||||
.stderr(predicate::str::ends_with(format!(
|
||||
": {}: {error_message}\n",
|
||||
&nopath.as_os_str().to_string_lossy()
|
||||
)));
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg(subcmd);
|
||||
cmd.arg(file.path()).arg(&nopath);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(2))
|
||||
.failure()
|
||||
.stderr(predicate::str::ends_with(format!(
|
||||
": {}: {error_message}\n",
|
||||
&nopath.as_os_str().to_string_lossy()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff");
|
||||
cmd.arg(&nopath).arg(&nopath);
|
||||
cmd.assert().code(predicate::eq(2)).failure().stderr(
|
||||
predicate::str::contains(format!(
|
||||
": {}: {error_message}\n",
|
||||
&nopath.as_os_str().to_string_lossy()
|
||||
))
|
||||
.count(2),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod diff {
|
||||
use diffutilslib::assert_diff_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn no_differences() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file = NamedTempFile::new()?;
|
||||
for option in ["", "-u", "-c", "-e"] {
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff");
|
||||
if !option.is_empty() {
|
||||
cmd.arg(option);
|
||||
}
|
||||
cmd.arg(file.path()).arg(file.path());
|
||||
cmd.assert()
|
||||
.code(predicate::eq(0))
|
||||
.success()
|
||||
.stdout(predicate::str::is_empty());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_differences_report_identical_files() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// same file
|
||||
let mut file1 = NamedTempFile::new()?;
|
||||
file1.write_all("foo\n".as_bytes())?;
|
||||
for option in ["", "-u", "-c", "-e"] {
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff");
|
||||
if !option.is_empty() {
|
||||
cmd.arg(option);
|
||||
}
|
||||
cmd.arg("-s").arg(file1.path()).arg(file1.path());
|
||||
cmd.assert()
|
||||
.code(predicate::eq(0))
|
||||
.success()
|
||||
.stdout(predicate::eq(format!(
|
||||
"Files {} and {} are identical\n",
|
||||
file1.path().to_string_lossy(),
|
||||
file1.path().to_string_lossy(),
|
||||
)));
|
||||
}
|
||||
// two files with the same content
|
||||
let mut file2 = NamedTempFile::new()?;
|
||||
file2.write_all("foo\n".as_bytes())?;
|
||||
for option in ["", "-u", "-c", "-e"] {
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff");
|
||||
if !option.is_empty() {
|
||||
cmd.arg(option);
|
||||
}
|
||||
cmd.arg("-s").arg(file1.path()).arg(file2.path());
|
||||
cmd.assert()
|
||||
.code(predicate::eq(0))
|
||||
.success()
|
||||
.stdout(predicate::eq(format!(
|
||||
"Files {} and {} are identical\n",
|
||||
file1.path().to_string_lossy(),
|
||||
file2.path().to_string_lossy(),
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn differences() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut file1 = NamedTempFile::new()?;
|
||||
file1.write_all("foo\n".as_bytes())?;
|
||||
let mut file2 = NamedTempFile::new()?;
|
||||
file2.write_all("bar\n".as_bytes())?;
|
||||
for option in ["", "-u", "-c", "-e"] {
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff");
|
||||
if !option.is_empty() {
|
||||
cmd.arg(option);
|
||||
}
|
||||
cmd.arg(file1.path()).arg(file2.path());
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stdout(predicate::str::is_empty().not());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn differences_brief() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut file1 = NamedTempFile::new()?;
|
||||
file1.write_all("foo\n".as_bytes())?;
|
||||
let mut file2 = NamedTempFile::new()?;
|
||||
file2.write_all("bar\n".as_bytes())?;
|
||||
for option in ["", "-u", "-c", "-e"] {
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff");
|
||||
if !option.is_empty() {
|
||||
cmd.arg(option);
|
||||
}
|
||||
cmd.arg("-q").arg(file1.path()).arg(file2.path());
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stdout(predicate::eq(format!(
|
||||
"Files {} and {} differ\n",
|
||||
file1.path().to_string_lossy(),
|
||||
file2.path().to_string_lossy()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_newline() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut file1 = NamedTempFile::new()?;
|
||||
file1.write_all("foo".as_bytes())?;
|
||||
let mut file2 = NamedTempFile::new()?;
|
||||
file2.write_all("bar".as_bytes())?;
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff");
|
||||
cmd.arg("-e").arg(file1.path()).arg(file2.path());
|
||||
cmd.assert()
|
||||
.code(predicate::eq(2))
|
||||
.failure()
|
||||
.stderr(predicate::str::starts_with("No newline at end of file"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_from_stdin() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut file1 = NamedTempFile::new()?;
|
||||
file1.write_all("foo\n".as_bytes())?;
|
||||
let mut file2 = NamedTempFile::new()?;
|
||||
file2.write_all("bar\n".as_bytes())?;
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff");
|
||||
cmd.arg("-u")
|
||||
.arg(file1.path())
|
||||
.arg("-")
|
||||
.write_stdin("bar\n");
|
||||
cmd.assert().code(predicate::eq(1)).failure();
|
||||
|
||||
let output = cmd.output().unwrap().stdout;
|
||||
assert_diff_eq!(
|
||||
output,
|
||||
format!(
|
||||
"--- {}\tTIMESTAMP\n+++ -\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n",
|
||||
file1.path().to_string_lossy()
|
||||
)
|
||||
);
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff");
|
||||
cmd.arg("-u")
|
||||
.arg("-")
|
||||
.arg(file2.path())
|
||||
.write_stdin("foo\n");
|
||||
cmd.assert().code(predicate::eq(1)).failure();
|
||||
|
||||
let output = cmd.output().unwrap().stdout;
|
||||
assert_diff_eq!(
|
||||
output,
|
||||
format!(
|
||||
"--- -\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n",
|
||||
file2.path().to_string_lossy()
|
||||
)
|
||||
);
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff");
|
||||
cmd.arg("-u").arg("-").arg("-");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(0))
|
||||
.success()
|
||||
.stdout(predicate::str::is_empty());
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff");
|
||||
cmd.arg("-u")
|
||||
.arg(file1.path())
|
||||
.arg("/dev/stdin")
|
||||
.write_stdin("bar\n");
|
||||
cmd.assert().code(predicate::eq(1)).failure();
|
||||
|
||||
let output = cmd.output().unwrap().stdout;
|
||||
assert_diff_eq!(
|
||||
output,
|
||||
format!(
|
||||
"--- {}\tTIMESTAMP\n+++ /dev/stdin\tTIMESTAMP\n@@ -1 +1 @@\n-foo\n+bar\n",
|
||||
file1.path().to_string_lossy()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compare_file_to_directory() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tmp_dir = tempdir()?;
|
||||
|
||||
let directory = tmp_dir.path().join("d");
|
||||
let _ = std::fs::create_dir(&directory);
|
||||
|
||||
let a_path = tmp_dir.path().join("a");
|
||||
let mut a = File::create(&a_path).unwrap();
|
||||
a.write_all(b"a\n").unwrap();
|
||||
|
||||
let da_path = directory.join("a");
|
||||
let mut da = File::create(&da_path).unwrap();
|
||||
da.write_all(b"da\n").unwrap();
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff");
|
||||
cmd.arg("-u").arg(&directory).arg(&a_path);
|
||||
cmd.assert().code(predicate::eq(1)).failure();
|
||||
|
||||
let output = cmd.output().unwrap().stdout;
|
||||
assert_diff_eq!(
|
||||
output,
|
||||
format!(
|
||||
"--- {}\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-da\n+a\n",
|
||||
da_path.display(),
|
||||
a_path.display()
|
||||
)
|
||||
);
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff");
|
||||
cmd.arg("-u").arg(&a_path).arg(&directory);
|
||||
cmd.assert().code(predicate::eq(1)).failure();
|
||||
|
||||
let output = cmd.output().unwrap().stdout;
|
||||
assert_diff_eq!(
|
||||
output,
|
||||
format!(
|
||||
"--- {}\tTIMESTAMP\n+++ {}\tTIMESTAMP\n@@ -1 +1 @@\n-a\n+da\n",
|
||||
a_path.display(),
|
||||
da_path.display()
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Regression for #269: the `-y` gutter marker must sit at the middle of the gutter.
|
||||
#[test]
|
||||
fn sdiff_gutter_marker_column() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut file1 = NamedTempFile::new()?;
|
||||
file1.write_all("aa\n".as_bytes())?;
|
||||
let mut file2 = NamedTempFile::new()?;
|
||||
file2.write_all("bb\n".as_bytes())?;
|
||||
|
||||
// --expand-tabs: padding is spaces.
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff")
|
||||
.arg("-y")
|
||||
.arg("--width=40")
|
||||
.arg("--expand-tabs")
|
||||
.arg(file1.path())
|
||||
.arg(file2.path());
|
||||
let output = cmd.output().unwrap().stdout;
|
||||
assert_eq!(
|
||||
output,
|
||||
b"aa <\n > bb\n"
|
||||
);
|
||||
|
||||
// Default (expand_tabs: false): padding goes through the tab path.
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("diff")
|
||||
.arg("-y")
|
||||
.arg("--width=40")
|
||||
.arg(file1.path())
|
||||
.arg(file2.path());
|
||||
let output = cmd.output().unwrap().stdout;
|
||||
assert_eq!(output, b"aa\t\t <\n\t\t >\tbb\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod cmp {
|
||||
use super::*;
|
||||
|
||||
// A file whose metadata length is 0 but which still yields bytes (/dev/zero)
|
||||
// collapses the offset column to two characters, so the padding subtraction
|
||||
// underflowed once the running offset reached three digits.
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn cmp_verbose_zero_length_metadata() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut file = NamedTempFile::new()?;
|
||||
file.write_all(&[0xffu8; 150])?;
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("cmp")
|
||||
.arg("--verbose")
|
||||
.arg("/dev/zero")
|
||||
.arg(file.path());
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.stdout(predicate::str::contains("150 0 377"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_incompatible_params() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-l");
|
||||
cmd.arg("-s");
|
||||
cmd.arg("/etc/passwd").arg("/etc/group");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(2))
|
||||
.failure()
|
||||
.stderr(predicate::str::ends_with(
|
||||
": options -l and -s are incompatible\n",
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_stdin() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tmp_dir = tempdir()?;
|
||||
|
||||
let a_path = tmp_dir.path().join("a");
|
||||
let mut a = File::create(&a_path).unwrap();
|
||||
a.write_all(b"a\n").unwrap();
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg(&a_path);
|
||||
cmd.write_stdin("a\n");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(0))
|
||||
.success()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::str::is_empty());
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.env("LC_ALL", "C");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg(&a_path);
|
||||
cmd.write_stdin("b\n");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::str::ends_with(" - differ: char 1, line 1\n"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_equal_files() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tmp_dir = tempdir()?;
|
||||
|
||||
let a_path = tmp_dir.path().join("a");
|
||||
let mut a = File::create(&a_path).unwrap();
|
||||
a.write_all(b"a\n").unwrap();
|
||||
|
||||
let b_path = tmp_dir.path().join("b");
|
||||
let mut b = File::create(&b_path).unwrap();
|
||||
b.write_all(b"a\n").unwrap();
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(0))
|
||||
.success()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::str::is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_one_file_empty() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tmp_dir = tempdir()?;
|
||||
|
||||
let a_path = tmp_dir.path().join("a");
|
||||
let mut a = File::create(&a_path).unwrap();
|
||||
a.write_all(b"a\n").unwrap();
|
||||
|
||||
let b_path = tmp_dir.path().join("b");
|
||||
let _ = File::create(&b_path).unwrap();
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stderr(predicate::str::contains(" EOF on "))
|
||||
.stderr(predicate::str::ends_with(" which is empty\n"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_immediate_difference() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tmp_dir = tempdir()?;
|
||||
|
||||
let a_path = tmp_dir.path().join("a");
|
||||
let mut a = File::create(&a_path).unwrap();
|
||||
a.write_all(b"abc\n").unwrap();
|
||||
|
||||
let b_path = tmp_dir.path().join("b");
|
||||
let mut b = File::create(&b_path).unwrap();
|
||||
b.write_all(b"bcd\n").unwrap();
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.env("LC_ALL", "C");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stdout(predicate::str::ends_with(" differ: char 1, line 1\n"));
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.env("LC_ALL", "C");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-b");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::str::ends_with(
|
||||
" differ: byte 1, line 1 is 141 a 142 b\n",
|
||||
));
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.env("LC_ALL", "C");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-l");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::eq("1 141 142\n2 142 143\n3 143 144\n"));
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.env("LC_ALL", "C");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-l");
|
||||
cmd.arg("-b");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::eq(
|
||||
"1 141 a 142 b\n2 142 b 143 c\n3 143 c 144 d\n",
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_newline_difference() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tmp_dir = tempdir()?;
|
||||
|
||||
let a_path = tmp_dir.path().join("a");
|
||||
let mut a = File::create(&a_path).unwrap();
|
||||
a.write_all(b"abc\ndefg").unwrap();
|
||||
|
||||
let b_path = tmp_dir.path().join("b");
|
||||
let mut b = File::create(&b_path).unwrap();
|
||||
b.write_all(b"abc\ndef\ng").unwrap();
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.env("LC_ALL", "C");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::str::ends_with(" differ: char 8, line 2\n"));
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.env("LC_ALL", "C");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-b");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::str::ends_with(
|
||||
" differ: byte 8, line 2 is 147 g 12 ^J\n",
|
||||
));
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.env("LC_ALL", "C");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-l");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stdout(predicate::str::starts_with("8 147 12\n"))
|
||||
.stderr(predicate::str::contains(" EOF on"))
|
||||
.stderr(predicate::str::ends_with(" after byte 8\n"));
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.env("LC_ALL", "C");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-b");
|
||||
cmd.arg("-l");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stdout(predicate::str::starts_with("8 147 g 12 ^J\n"))
|
||||
.stderr(predicate::str::contains(" EOF on"))
|
||||
.stderr(predicate::str::ends_with(" after byte 8\n"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_max_bytes() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tmp_dir = tempdir()?;
|
||||
|
||||
let a_path = tmp_dir.path().join("a");
|
||||
let mut a = File::create(&a_path).unwrap();
|
||||
a.write_all(b"abc efg ijkl\n").unwrap();
|
||||
|
||||
let b_path = tmp_dir.path().join("b");
|
||||
let mut b = File::create(&b_path).unwrap();
|
||||
b.write_all(b"abcdefghijkl\n").unwrap();
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-l");
|
||||
cmd.arg("-b");
|
||||
cmd.arg("-n");
|
||||
cmd.arg("3");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(0))
|
||||
.success()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::str::is_empty());
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-l");
|
||||
cmd.arg("-b");
|
||||
cmd.arg("-n");
|
||||
cmd.arg("4");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::eq("4 40 144 d\n"));
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-l");
|
||||
cmd.arg("-b");
|
||||
cmd.arg("-n");
|
||||
cmd.arg("13");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::eq(" 4 40 144 d\n 8 40 150 h\n"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_skip_args_parsing() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tmp_dir = tempdir()?;
|
||||
|
||||
let a_path = tmp_dir.path().join("a");
|
||||
let mut a = File::create(&a_path).unwrap();
|
||||
a.write_all(b"---abc\n").unwrap();
|
||||
|
||||
let b_path = tmp_dir.path().join("b");
|
||||
let mut b = File::create(&b_path).unwrap();
|
||||
b.write_all(b"###abc\n").unwrap();
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.env("LC_ALL", "C");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-i");
|
||||
cmd.arg("3");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(0))
|
||||
.success()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::str::is_empty());
|
||||
|
||||
// Positional skips should be ignored
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.env("LC_ALL", "C");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-i");
|
||||
cmd.arg("3");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.arg("1").arg("1");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(0))
|
||||
.success()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::str::is_empty());
|
||||
|
||||
// Single positional argument should only affect first file.
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.env("LC_ALL", "C");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.arg("3");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::str::ends_with(" differ: char 1, line 1\n"));
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.env("LC_ALL", "C");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.arg("3");
|
||||
cmd.arg("3");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(0))
|
||||
.success()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::str::is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_skip_suffix_parsing() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tmp_dir = tempdir()?;
|
||||
|
||||
let a_path = tmp_dir.path().join("a");
|
||||
let mut a = File::create(&a_path).unwrap();
|
||||
writeln!(a, "{}c", "a".repeat(1024)).unwrap();
|
||||
a.flush().unwrap();
|
||||
|
||||
let b_path = tmp_dir.path().join("b");
|
||||
let mut b = File::create(&b_path).unwrap();
|
||||
writeln!(b, "{}c", "b".repeat(1024)).unwrap();
|
||||
b.flush().unwrap();
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("--ignore-initial=1K");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(0))
|
||||
.success()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::str::is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_skip() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tmp_dir = tempdir()?;
|
||||
|
||||
let a_path = tmp_dir.path().join("a");
|
||||
let mut a = File::create(&a_path).unwrap();
|
||||
a.write_all(b"abc efg ijkl\n").unwrap();
|
||||
|
||||
let b_path = tmp_dir.path().join("b");
|
||||
let mut b = File::create(&b_path).unwrap();
|
||||
b.write_all(b"abcdefghijkl\n").unwrap();
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-l");
|
||||
cmd.arg("-b");
|
||||
cmd.arg("-i");
|
||||
cmd.arg("8");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(0))
|
||||
.success()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::str::is_empty());
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-b");
|
||||
cmd.arg("-i");
|
||||
cmd.arg("4");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stderr(predicate::str::is_empty())
|
||||
.stdout(predicate::str::ends_with(
|
||||
" differ: byte 4, line 1 is 40 150 h\n",
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_binary() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tmp_dir = tempdir()?;
|
||||
|
||||
let mut bytes = vec![0, 15, 31, 32, 33, 40, 64, 126, 127, 128, 129, 200, 254, 255];
|
||||
|
||||
let a_path = tmp_dir.path().join("a");
|
||||
let mut a = File::create(&a_path).unwrap();
|
||||
a.write_all(&bytes).unwrap();
|
||||
|
||||
bytes.reverse();
|
||||
|
||||
let b_path = tmp_dir.path().join("b");
|
||||
let mut b = File::create(&b_path).unwrap();
|
||||
b.write_all(&bytes).unwrap();
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-l");
|
||||
cmd.arg("-b");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stdout(predicate::eq(concat!(
|
||||
" 1 0 ^@ 377 M-^?\n",
|
||||
" 2 17 ^O 376 M-~\n",
|
||||
" 3 37 ^_ 310 M-H\n",
|
||||
" 4 40 201 M-^A\n",
|
||||
" 5 41 ! 200 M-^@\n",
|
||||
" 6 50 ( 177 ^?\n",
|
||||
" 7 100 @ 176 ~\n",
|
||||
" 8 176 ~ 100 @\n",
|
||||
" 9 177 ^? 50 (\n",
|
||||
"10 200 M-^@ 41 !\n",
|
||||
"11 201 M-^A 40 \n",
|
||||
"12 310 M-H 37 ^_\n",
|
||||
"13 376 M-~ 17 ^O\n",
|
||||
"14 377 M-^? 0 ^@\n"
|
||||
)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn cmp_fast_paths() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tmp_dir = tempdir()?;
|
||||
|
||||
// This test mimics one found in the GNU cmp test suite. It is used for
|
||||
// validating the /dev/null optimization.
|
||||
let a_path = tmp_dir.path().join("a");
|
||||
let a = File::create(&a_path).unwrap();
|
||||
a.set_len(14 * 1024 * 1024 * 1024 * 1024).unwrap();
|
||||
|
||||
let b_path = tmp_dir.path().join("b");
|
||||
let b = File::create(&b_path).unwrap();
|
||||
b.set_len(15 * 1024 * 1024 * 1024 * 1024).unwrap();
|
||||
|
||||
let dev_null = OpenOptions::new().write(true).open("/dev/null").unwrap();
|
||||
|
||||
let mut child = std::process::Command::new(assert_cmd::cargo::cargo_bin!("diffutils"))
|
||||
.arg("cmp")
|
||||
.arg(&a_path)
|
||||
.arg(&b_path)
|
||||
.stdout(dev_null)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
// Bound the runtime to a very short time that still allows for some resource
|
||||
// constraint to slow it down while also allowing very fast systems to exit as
|
||||
// early as possible.
|
||||
const MAX_TRIES: u8 = 50;
|
||||
for tries in 0..=MAX_TRIES {
|
||||
if tries == MAX_TRIES {
|
||||
panic!("cmp took too long to run, /dev/null optimization probably not working")
|
||||
}
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
assert_eq!(status.code(), Some(1));
|
||||
break;
|
||||
}
|
||||
Ok(None) => (),
|
||||
Err(e) => panic!("{e:#?}"),
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
|
||||
// Two stdins should be equal
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg("-");
|
||||
cmd.arg("-");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(0))
|
||||
.success()
|
||||
.stdout(predicate::str::is_empty())
|
||||
.stderr(predicate::str::is_empty());
|
||||
|
||||
// Files with longer than block size equal segments should still report
|
||||
// the correct line number for the difference. Assumes 8KB block size (see
|
||||
// https://github.com/rust-lang/rust/blob/master/library/std/src/sys_common/io.rs),
|
||||
// create a 24KB equality.
|
||||
let mut bytes = " ".repeat(4095);
|
||||
bytes.push('\n');
|
||||
bytes.push_str(&" ".repeat(4096));
|
||||
|
||||
let bytes = bytes.repeat(3);
|
||||
let bytes = bytes.as_bytes();
|
||||
|
||||
let a_path = tmp_dir.path().join("a");
|
||||
let mut a = File::create(&a_path).unwrap();
|
||||
a.write_all(bytes).unwrap();
|
||||
a.write_all(b"A").unwrap();
|
||||
|
||||
let b_path = tmp_dir.path().join("b");
|
||||
let mut b = File::create(&b_path).unwrap();
|
||||
b.write_all(bytes).unwrap();
|
||||
b.write_all(b"B").unwrap();
|
||||
|
||||
let mut cmd = cargo_bin_cmd!("diffutils");
|
||||
cmd.arg("cmp");
|
||||
cmd.arg(&a_path).arg(&b_path);
|
||||
cmd.env("LC_ALL", "en_US");
|
||||
cmd.assert()
|
||||
.code(predicate::eq(1))
|
||||
.failure()
|
||||
.stdout(predicate::str::ends_with(" differ: byte 24577, line 4\n"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Print the test results written to a JSON file by run-upstream-testsuite.sh
|
||||
# in a markdown format. The printout includes the name of the test, the result,
|
||||
# the URL to the test script and the contents of stdout and stderr.
|
||||
# It can be used verbatim as the description when filing an issue for a test
|
||||
# with an unexpected result.
|
||||
|
||||
json="test-results.json"
|
||||
[[ -n $1 ]] && json="$1"
|
||||
|
||||
codeblock () { echo -e "\`\`\`\n$1\n\`\`\`"; }
|
||||
|
||||
jq -c '.tests[]' "$json" | while read -r test
|
||||
do
|
||||
name=$(echo "$test" | jq -r '.test')
|
||||
echo "# test: $name"
|
||||
result=$(echo "$test" | jq -r '.result')
|
||||
echo "result: $result"
|
||||
url=$(echo "$test" | jq -r '.url')
|
||||
echo "url: $url"
|
||||
if [[ "$result" != "SKIP" ]]
|
||||
then
|
||||
stdout=$(echo "$test" | jq -r '.stdout' | base64 -d)
|
||||
if [[ -n "$stdout" ]]
|
||||
then
|
||||
echo "## stdout"
|
||||
codeblock "$stdout"
|
||||
fi
|
||||
stderr=$(echo "$test" | jq -r '.stderr' | base64 -d)
|
||||
if [[ -n "$stderr" ]]
|
||||
then
|
||||
echo "## stderr"
|
||||
codeblock "$stderr"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run the GNU upstream test suite for diffutils against a local build of the
|
||||
# Rust implementation, print out a summary of the test results, and writes a
|
||||
# JSON file ('test-results.json') containing detailed information about the
|
||||
# test run.
|
||||
|
||||
# The JSON file contains metadata about the test run, and for each test the
|
||||
# result as well as the contents of stdout, stderr, and of all the files
|
||||
# written by the test script, if any (excluding subdirectories).
|
||||
|
||||
# The script takes a shortcut to fetch only the test suite from the upstream
|
||||
# repository and carefully avoids running the autotools machinery which is
|
||||
# time-consuming and resource-intensive, and doesn't offer the option to not
|
||||
# build the upstream binaries. As a consequence, the environment in which the
|
||||
# tests are run might not match exactly that used when the upstream tests are
|
||||
# run through the autotools.
|
||||
|
||||
# Exit codes: 0 if all tests passed, 1 if at least one test failed, and 2 if
|
||||
# the test suite could not be run at all (e.g. the upstream repository couldn't
|
||||
# be fetched). Callers must treat 2 as an infrastructure error: no meaningful
|
||||
# result was produced.
|
||||
|
||||
# By default it expects a release build of the diffutils binary, but a
|
||||
# different build profile can be specified as an argument
|
||||
# (e.g. 'dev' or 'test').
|
||||
# Unless overridden by the $TESTS environment variable, all tests in the test
|
||||
# suite will be run. Tests targeting a command that is not yet implemented
|
||||
# (e.g. diff3 or sdiff) are skipped.
|
||||
|
||||
scriptpath=$(dirname "$(readlink -f "$0")")
|
||||
rev=$(git rev-parse HEAD)
|
||||
gnu_version="3.12"
|
||||
gnu_tarball_blake2b=5b4593b39da71578d7f975603abe9359be215b9ac76548a6ab0d6e3838bb103c7ffcddf7fa01abcd5c6289db9a2f16b43aa3d5e846a9aa4b8db866763c2660de
|
||||
|
||||
# Report an infrastructure error: the test suite could not be run at all
|
||||
die() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# Allow passing a specific profile as parameter (default to "release")
|
||||
profile="release"
|
||||
[[ -n $1 ]] && profile="$1"
|
||||
|
||||
# Verify that the diffutils binary was built for the requested profile
|
||||
binary="$scriptpath/../target/$profile/diffutils"
|
||||
if [[ ! -x "$binary" ]]
|
||||
then
|
||||
die "Missing build for profile $profile"
|
||||
fi
|
||||
|
||||
# Work in a temporary directory
|
||||
tempdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tempdir"' EXIT
|
||||
cd "$tempdir" || die "Cannot enter temporary directory $tempdir"
|
||||
|
||||
# Fetch the upstream test suite
|
||||
echo "Fetching upstream test suite"
|
||||
curl -fsSL --retry 5 https://ftpmirror.gnu.org/diffutils/diffutils-${gnu_version}.tar.xz -o gnu-diffutils.tar.xz || die "Failed to fetch test suite"
|
||||
echo ${gnu_tarball_blake2b} gnu-diffutils.tar.xz \
|
||||
| b2sum --check || die "Failed to verify checksum of tarball of GNU diffutils"
|
||||
mkdir -p diffutils
|
||||
tar xJf gnu-diffutils.tar.xz --strip-components=1 -C diffutils || die "Failed to extract test suite"
|
||||
|
||||
cd diffutils || die "Test suite src is broken"
|
||||
|
||||
# Ensure that calling `diff` invokes the built `diffutils` binary instead of
|
||||
# the upstream `diff` binary that is most likely installed on the system
|
||||
cd src || die "Missing the directory holding the diff and cmp symlinks"
|
||||
ln -s "$binary" diff
|
||||
ln -s "$binary" cmp
|
||||
cd ../tests || die "Cannot enter the upstream tests directory"
|
||||
|
||||
if [[ -n "$TESTS" ]]
|
||||
then
|
||||
tests="$TESTS"
|
||||
else
|
||||
# Get a list of all upstream tests (default if $TESTS isn't set)
|
||||
echo -e '\n\nprinttests:\n\t@echo "${TESTS}"' >> Makefile.am
|
||||
tests=$(make -f Makefile.am printtests)
|
||||
fi
|
||||
total=$(echo "$tests" | wc -w)
|
||||
(( total > 0 )) || die "No test to run: the upstream test list is empty"
|
||||
echo "Running $total tests"
|
||||
export LC_ALL=C
|
||||
export KEEP=yes
|
||||
timestamp=$(date -Iseconds)
|
||||
passed=0
|
||||
failed=0
|
||||
skipped=0
|
||||
normal="$(tput sgr0)"
|
||||
for test in $tests
|
||||
do
|
||||
result="FAIL"
|
||||
# Run only the tests that invoke `diff` or `cmp`,
|
||||
# because other binaries aren't implemented yet
|
||||
if ! grep -E -s -q "(diff3|sdiff)" "$test"
|
||||
then
|
||||
sh "$test" 1> stdout.txt 2> stderr.txt && result="PASS"
|
||||
if [[ $? = 77 ]]
|
||||
then
|
||||
result="SKIP"
|
||||
else
|
||||
json+="{\"test\":\"$test\",\"result\":\"$result\","
|
||||
json+="\"stdout\":\"$(base64 -w0 < stdout.txt)\","
|
||||
json+="\"stderr\":\"$(base64 -w0 < stderr.txt)\","
|
||||
json+="\"files\":{"
|
||||
cd gt-$test.*
|
||||
# Note: this doesn't include the contents of subdirectories,
|
||||
# but there isn't much value added in doing so
|
||||
for file in *
|
||||
do
|
||||
# Encode the name with jq: some tests create files whose name contains
|
||||
# quotes or control characters, which would produce invalid JSON
|
||||
[[ -f "$file" ]] && json+="$(jq -Rn --arg name "$file" '$name'):\"$(base64 -w0 < "$file")\","
|
||||
done
|
||||
json="${json%,}}},"
|
||||
cd - > /dev/null
|
||||
[[ "$result" = "PASS" ]] && (( passed++ ))
|
||||
[[ "$result" = "FAIL" ]] && (( failed++ ))
|
||||
fi
|
||||
else
|
||||
result="SKIP"
|
||||
fi
|
||||
color=2 # green
|
||||
[[ "$result" = "FAIL" ]] && color=1 # red
|
||||
if [[ $result = "SKIP" ]]
|
||||
then
|
||||
(( skipped++ ))
|
||||
json+="{\"test\":\"$test\",\"result\":\"$result\"},"
|
||||
color=3 # yellow
|
||||
fi
|
||||
printf " %-40s $(tput setaf $color)$result$(tput sgr0)\n" "$test"
|
||||
done
|
||||
echo ""
|
||||
echo -n "Summary: TOTAL: $total / "
|
||||
echo -n "$(tput setaf 2)PASS$normal: $passed / "
|
||||
echo -n "$(tput setaf 1)FAIL$normal: $failed / "
|
||||
echo "$(tput setaf 3)SKIP$normal: $skipped"
|
||||
echo ""
|
||||
|
||||
json="\"tests\":[${json%,}]"
|
||||
metadata="\"timestamp\":\"$timestamp\","
|
||||
metadata+="\"revision\":\"$rev\","
|
||||
metadata+="\"upstream-version\":\"$gnu_version\","
|
||||
if [[ -n "$GITHUB_ACTIONS" ]]
|
||||
then
|
||||
metadata+="\"branch\":\"$GITHUB_REF\","
|
||||
fi
|
||||
json="{$metadata $json}"
|
||||
|
||||
# Clean up
|
||||
cd "$scriptpath"
|
||||
|
||||
# Write the results out only once they are known to be valid JSON, so that a
|
||||
# malformed (or truncated) file is never left behind for the caller to consume
|
||||
resultsfile="test-results.json"
|
||||
if ! echo "$json" | jq > "$resultsfile.tmp"
|
||||
then
|
||||
rm -f "$resultsfile.tmp"
|
||||
die "Generated invalid JSON results"
|
||||
fi
|
||||
mv "$resultsfile.tmp" "$resultsfile"
|
||||
echo "Results written to $scriptpath/$resultsfile"
|
||||
|
||||
(( failed > 0 )) && exit 1
|
||||
exit 0
|
||||
@@ -0,0 +1,2 @@
|
||||
diff
|
||||
cmp
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Compare the current GNU test results to the last results gathered from the main branch to
|
||||
highlight if a PR is making the results better/worse.
|
||||
Don't exit with error code if all failing tests are in the ignore-intermittent.txt list.
|
||||
|
||||
Exit status: 0 if the comparison shows no new failure, 1 if new non-intermittent
|
||||
failures appeared, and 2 if the comparison couldn't be performed at all (e.g. one
|
||||
of the result files is missing or malformed).
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_ignore_list(ignore_file):
|
||||
"""Load list of intermittent test names to ignore from file."""
|
||||
ignore_set = set()
|
||||
if ignore_file and Path(ignore_file).exists():
|
||||
with open(ignore_file, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#"):
|
||||
ignore_set.add(line)
|
||||
return ignore_set
|
||||
|
||||
|
||||
def extract_test_results(json_data):
|
||||
"""Extract test results from a diffutils test-results.json.
|
||||
|
||||
Note: unlike sed, diffutils JSON has no 'summary' object — results are
|
||||
computed from the 'tests' array using the 'result' and 'test' fields.
|
||||
"""
|
||||
tests = json_data.get("tests", [])
|
||||
passed = sum(1 for t in tests if t.get("result") == "PASS")
|
||||
failed = sum(1 for t in tests if t.get("result") == "FAIL")
|
||||
skipped = sum(1 for t in tests if t.get("result") == "SKIP")
|
||||
summary = {"total": len(tests), "passed": passed, "failed": failed, "skipped": skipped}
|
||||
failed_tests = [t["test"] for t in tests if t.get("result") == "FAIL"]
|
||||
return summary, failed_tests
|
||||
|
||||
|
||||
def compare_results(current_file, reference_file, ignore_file=None, output_file=None):
|
||||
"""Compare current results with reference results."""
|
||||
ignore_set = load_ignore_list(ignore_file)
|
||||
|
||||
try:
|
||||
with open(current_file, "r") as f:
|
||||
current_data = json.load(f)
|
||||
current_summary, current_failed = extract_test_results(current_data)
|
||||
except Exception as e:
|
||||
print(f"Error loading current results: {e}")
|
||||
return 2
|
||||
|
||||
try:
|
||||
with open(reference_file, "r") as f:
|
||||
reference_data = json.load(f)
|
||||
reference_summary, reference_failed = extract_test_results(reference_data)
|
||||
except Exception as e:
|
||||
print(f"Error loading reference results: {e}")
|
||||
return 2
|
||||
|
||||
# Calculate differences
|
||||
pass_diff = int(current_summary.get("passed", 0)) - int(reference_summary.get("passed", 0))
|
||||
fail_diff = int(current_summary.get("failed", 0)) - int(reference_summary.get("failed", 0))
|
||||
total_diff = int(current_summary.get("total", 0)) - int(reference_summary.get("total", 0))
|
||||
|
||||
# Find new failures and improvements
|
||||
current_failed_set = set(current_failed)
|
||||
reference_failed_set = set(reference_failed)
|
||||
|
||||
new_failures = current_failed_set - reference_failed_set
|
||||
improvements = reference_failed_set - current_failed_set
|
||||
|
||||
# Filter out intermittent failures
|
||||
non_intermittent_new_failures = new_failures - ignore_set
|
||||
|
||||
# Check if results are identical (no changes)
|
||||
no_changes = (
|
||||
pass_diff == 0
|
||||
and fail_diff == 0
|
||||
and total_diff == 0
|
||||
and not new_failures
|
||||
and not improvements
|
||||
)
|
||||
|
||||
# If no changes, write empty output to prevent comment posting
|
||||
if no_changes:
|
||||
if output_file:
|
||||
with open(output_file, "w") as f:
|
||||
f.write("")
|
||||
return 0
|
||||
|
||||
# Prepare output message
|
||||
output_lines = []
|
||||
|
||||
output_lines.append("Test results comparison:")
|
||||
output_lines.append(
|
||||
f" Current: TOTAL: {current_summary.get('total', 0)} / PASSED: {current_summary.get('passed', 0)} / FAILED: {current_summary.get('failed', 0)} / SKIPPED: {current_summary.get('skipped', 0)}"
|
||||
)
|
||||
output_lines.append(
|
||||
f" Reference: TOTAL: {reference_summary.get('total', 0)} / PASSED: {reference_summary.get('passed', 0)} / FAILED: {reference_summary.get('failed', 0)} / SKIPPED: {reference_summary.get('skipped', 0)}"
|
||||
)
|
||||
output_lines.append("")
|
||||
|
||||
if pass_diff != 0 or fail_diff != 0 or total_diff != 0:
|
||||
output_lines.append("Changes from main branch:")
|
||||
output_lines.append(f" TOTAL: {total_diff:+d}")
|
||||
output_lines.append(f" PASSED: {pass_diff:+d}")
|
||||
output_lines.append(f" FAILED: {fail_diff:+d}")
|
||||
output_lines.append("")
|
||||
|
||||
if new_failures:
|
||||
output_lines.append(f"New test failures ({len(new_failures)}):")
|
||||
for test in sorted(new_failures):
|
||||
if test in ignore_set:
|
||||
output_lines.append(f" - {test} (intermittent)")
|
||||
else:
|
||||
output_lines.append(f" - {test}")
|
||||
output_lines.append("")
|
||||
|
||||
if improvements:
|
||||
output_lines.append(f"Test improvements ({len(improvements)}):")
|
||||
for test in sorted(improvements):
|
||||
output_lines.append(f" + {test}")
|
||||
output_lines.append("")
|
||||
|
||||
output_text = "\n".join(output_lines)
|
||||
if output_file:
|
||||
with open(output_file, "w") as f:
|
||||
f.write(output_text)
|
||||
else:
|
||||
print(output_text)
|
||||
|
||||
if non_intermittent_new_failures:
|
||||
print(
|
||||
f"ERROR: Found {len(non_intermittent_new_failures)} new non-intermittent test failures"
|
||||
)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Compare GNU diffutils test results")
|
||||
parser.add_argument("current", help="Current test results JSON file")
|
||||
parser.add_argument("reference", help="Reference test results JSON file")
|
||||
parser.add_argument(
|
||||
"--ignore-file", help="File containing intermittent test names to ignore"
|
||||
)
|
||||
parser.add_argument("--output", help="Output file for comparison results")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
return compare_results(args.current, args.reference, args.ignore_file, args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "sausage-exec-tools"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "env"
|
||||
path = "src/env.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "timeout"
|
||||
path = "src/timeout.rs"
|
||||
|
||||
[dependencies]
|
||||
sausage-spawn = { path = "../spawn" }
|
||||
@@ -0,0 +1,20 @@
|
||||
//! Print the environment or start a command with environment overrides.
|
||||
use sausage_spawn::Command;
|
||||
fn run() -> Result<i32, std::io::Error> {
|
||||
let mut args = std::env::args().skip(1).peekable();
|
||||
let mut clear = false;
|
||||
if args.peek().is_some_and(|a| a == "-i" || a == "--ignore-environment") { clear = true; args.next(); }
|
||||
let mut values = if clear { std::collections::BTreeMap::new() } else { std::env::vars().collect() };
|
||||
while args.peek().is_some_and(|a| a.contains('=')) {
|
||||
let arg = args.next().unwrap(); let (key, value) = arg.split_once('=').unwrap();
|
||||
values.insert(key.to_owned(), value.to_owned());
|
||||
}
|
||||
if let Some(program) = args.next() {
|
||||
let mut command = Command::new(program); command.env_clear();
|
||||
for (key, value) in values { command.env(key, value); }
|
||||
command.args(args).status()
|
||||
} else {
|
||||
for (key, value) in values { println!("{key}={value}"); } Ok(0)
|
||||
}
|
||||
}
|
||||
fn main() { std::process::exit(match run() { Ok(code) => code, Err(e) => { eprintln!("env: {e}"); if e.kind() == std::io::ErrorKind::NotFound { 127 } else { 126 } } }); }
|
||||
@@ -0,0 +1,25 @@
|
||||
//! Stop a world child after a deadline. Expiration returns status 124.
|
||||
use sausage_spawn::Command;
|
||||
use std::time::{Duration, Instant};
|
||||
fn run() -> Result<i32, Box<dyn std::error::Error>> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let duration = args.next().ok_or("a duration is required")?;
|
||||
let (number, multiplier) = match duration.as_bytes().last() {
|
||||
Some(b's') => (&duration[..duration.len()-1], 1.0),
|
||||
Some(b'm') => (&duration[..duration.len()-1], 60.0),
|
||||
Some(b'h') => (&duration[..duration.len()-1], 3600.0),
|
||||
Some(b'd') => (&duration[..duration.len()-1], 86400.0),
|
||||
_ => (&duration[..], 1.0),
|
||||
};
|
||||
let duration = Duration::try_from_secs_f64(number.parse::<f64>()? * multiplier)?;
|
||||
let program = args.next().ok_or("a command is required")?;
|
||||
let child = Command::new(program).args(args).new_group().spawn()?;
|
||||
if duration.is_zero() { return Ok(child.wait()); }
|
||||
let deadline = Instant::now() + duration;
|
||||
loop {
|
||||
if let Some(code) = child.try_wait() { return Ok(code); }
|
||||
if Instant::now() >= deadline { child.kill(); child.wait(); return Ok(124); }
|
||||
std::thread::sleep(Duration::from_millis(10).min(deadline.saturating_duration_since(Instant::now())));
|
||||
}
|
||||
}
|
||||
fn main() { std::process::exit(match run() { Ok(code) => code, Err(e) => { eprintln!("timeout: {e}"); 125 } }); }
|
||||
@@ -0,0 +1 @@
|
||||
{"v":1}
|
||||
@@ -0,0 +1,9 @@
|
||||
# On Windows MSVC, statically link the C runtime so that the resulting EXE does
|
||||
# not depend on the vcruntime DLL.
|
||||
#
|
||||
# See: https://github.com/sharkdp/fd/issues/1874
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
[target.i686-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "193d0df1d0ad455e44506953a2cd80428203ff35"
|
||||
},
|
||||
"path_in_vcs": ""
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
github: [sharkdp, tavianator]
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Bug Report
|
||||
description: Report a bug.
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please check out the [troubleshooting section](https://github.com/sharkdp/fd#troubleshooting) first.
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Checks
|
||||
options:
|
||||
- label: I have read the troubleshooting section and still think this is a bug.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: bug
|
||||
attributes:
|
||||
label: "Describe the bug you encountered:"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: "Describe what you expected to happen:"
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: "What version of `fd` are you using?"
|
||||
placeholder: "paste the output of `fd --version` here"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: os
|
||||
attributes:
|
||||
label: Which operating system / distribution are you on?
|
||||
placeholder: |
|
||||
Unix: paste the output of `uname -srm` and `lsb_release -a` here.
|
||||
Windows: please tell us your Windows version
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: true
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for this project.
|
||||
title: ''
|
||||
labels: feature-request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask a question about 'fd'.
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
**What version of `fd` are you using?**
|
||||
[paste the output of `fd --version` here]
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
name: CICD
|
||||
|
||||
env:
|
||||
CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
|
||||
MSRV_FEATURES: "--all-features"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
crate_metadata:
|
||||
name: Extract crate metadata
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Extract crate information
|
||||
id: crate_metadata
|
||||
run: |
|
||||
echo "name=fd" | tee -a $GITHUB_OUTPUT
|
||||
cargo metadata --no-deps --format-version 1 | jq -r '"version=" + .packages[0].version' | tee -a $GITHUB_OUTPUT
|
||||
cargo metadata --no-deps --format-version 1 | jq -r '"maintainer=" + .packages[0].authors[0]' | tee -a $GITHUB_OUTPUT
|
||||
cargo metadata --no-deps --format-version 1 | jq -r '"homepage=" + .packages[0].homepage' | tee -a $GITHUB_OUTPUT
|
||||
cargo metadata --no-deps --format-version 1 | jq -r '"msrv=" + .packages[0].rust_version' | tee -a $GITHUB_OUTPUT
|
||||
outputs:
|
||||
name: ${{ steps.crate_metadata.outputs.name }}
|
||||
version: ${{ steps.crate_metadata.outputs.version }}
|
||||
maintainer: ${{ steps.crate_metadata.outputs.maintainer }}
|
||||
homepage: ${{ steps.crate_metadata.outputs.homepage }}
|
||||
msrv: ${{ steps.crate_metadata.outputs.msrv }}
|
||||
|
||||
ensure_cargo_fmt:
|
||||
name: Ensure 'cargo fmt' has been run
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: cargo fmt -- --check
|
||||
|
||||
lint_check:
|
||||
name: Ensure 'cargo clippy' has no warnings
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: cargo clippy --all-targets --all-features -- -Dwarnings
|
||||
|
||||
min_version:
|
||||
name: Minimum supported rust version
|
||||
runs-on: ubuntu-22.04
|
||||
needs: crate_metadata
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install rust toolchain (v${{ needs.crate_metadata.outputs.msrv }})
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ needs.crate_metadata.outputs.msrv }}
|
||||
components: clippy
|
||||
- name: Run clippy (on minimum supported rust version to prevent warnings we can't fix)
|
||||
run: cargo clippy --locked --all-targets "${MSRV_FEATURES}"
|
||||
- name: Run tests
|
||||
run: cargo test --locked "${MSRV_FEATURES}"
|
||||
|
||||
build:
|
||||
name: '${{ matrix.job.target }} (${{ matrix.job.os }})'
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
needs: crate_metadata
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
attestations: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { target: aarch64-unknown-linux-gnu , os: ubuntu-24.04, use-cross: true }
|
||||
- { target: aarch64-unknown-linux-musl , os: ubuntu-24.04, use-cross: true }
|
||||
- { target: arm-unknown-linux-gnueabihf , os: ubuntu-24.04, use-cross: true }
|
||||
- { target: arm-unknown-linux-musleabihf, os: ubuntu-24.04, use-cross: true }
|
||||
- { target: i686-pc-windows-msvc , os: windows-2025 }
|
||||
- { target: i686-unknown-linux-gnu , os: ubuntu-24.04, use-cross: true }
|
||||
- { target: i686-unknown-linux-musl , os: ubuntu-24.04, use-cross: true }
|
||||
- { target: x86_64-apple-darwin , os: macos-26-intel }
|
||||
- { target: aarch64-apple-darwin , os: macos-26 }
|
||||
- { target: x86_64-pc-windows-gnu , os: windows-2025 }
|
||||
- { target: x86_64-pc-windows-msvc , os: windows-2025 }
|
||||
- { target: aarch64-pc-windows-msvc , os: windows-11-arm }
|
||||
- { target: aarch64-pc-windows-gnullvm , os: windows-11-arm }
|
||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-24.04, use-cross: true }
|
||||
- { target: x86_64-unknown-linux-musl , os: ubuntu-24.04, use-cross: true }
|
||||
env:
|
||||
BUILD_CMD: "${{ matrix.job.use-cross && 'cross' || 'cargo' }}"
|
||||
target: ${{ matrix.job.target }}
|
||||
name: ${{ needs.crate_metadata.outputs.name }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Windows ARM64 GNU-family linker
|
||||
if: matrix.job.target == 'aarch64-pc-windows-gnullvm'
|
||||
id: msys2
|
||||
uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2.33.0
|
||||
with:
|
||||
msystem: CLANGARM64
|
||||
update: true
|
||||
install: mingw-w64-clang-aarch64-clang
|
||||
|
||||
- name: Expose Windows ARM64 GNU-family linker
|
||||
if: matrix.job.target == 'aarch64-pc-windows-gnullvm'
|
||||
shell: bash
|
||||
env:
|
||||
MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }}
|
||||
run: |
|
||||
bin_dir=$(cygpath -w "${MSYS2_LOCATION}/clangarm64/bin")
|
||||
echo "${bin_dir}" >> $GITHUB_PATH
|
||||
echo "CARGO_TARGET_AARCH64_PC_WINDOWS_GNULLVM_LINKER=${bin_dir}\\clang.exe" >> $GITHUB_ENV
|
||||
# Link libunwind statically so the release does not need MSYS2's libunwind.dll.
|
||||
echo "CARGO_TARGET_AARCH64_PC_WINDOWS_GNULLVM_RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
|
||||
|
||||
- name: Install prerequisites
|
||||
shell: bash
|
||||
run: |
|
||||
case ${target} in
|
||||
arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;;
|
||||
aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
|
||||
esac
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.job.target }}
|
||||
toolchain: "stable"
|
||||
|
||||
- name: Install cross
|
||||
if: matrix.job.use-cross
|
||||
env:
|
||||
cross_version: "v0.2.5"
|
||||
package_name: "cross-x86_64-unknown-linux-gnu.tar.gz"
|
||||
GH_TOKEN: "${{ github.token }}"
|
||||
run: |
|
||||
dir="$HOME/.local/bin/"
|
||||
mkdir -p "$dir"
|
||||
gh release download --repo cross-rs/cross \
|
||||
--pattern "${package_name}" -O - "${cross_version}" \
|
||||
| tar -C "$dir" -xz
|
||||
echo "$dir" >> $GITHUB_PATH
|
||||
echo "Installed cross $cross_version" >> $GITHUB_STEP_SUMMARY
|
||||
- name: Show version information (Rust, cargo, GCC)
|
||||
shell: bash
|
||||
run: |
|
||||
gcc --version || true
|
||||
rustup -V
|
||||
rustup toolchain list
|
||||
rustup default
|
||||
cargo -V
|
||||
rustc -V
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: $BUILD_CMD build --locked --release --target="${target}"
|
||||
|
||||
- name: Set binary name & path
|
||||
id: bin
|
||||
shell: bash
|
||||
run: |
|
||||
# Figure out suffix of binary
|
||||
EXE_suffix=""
|
||||
case ${target} in
|
||||
*-pc-windows-*) EXE_suffix=".exe" ;;
|
||||
esac;
|
||||
|
||||
# Setup paths
|
||||
BIN_NAME="${name}${EXE_suffix}"
|
||||
BIN_PATH="target/${target}/release/${BIN_NAME}"
|
||||
|
||||
# Let subsequent steps know where to find the binary
|
||||
echo "BIN_PATH=${BIN_PATH}" >> $GITHUB_OUTPUT
|
||||
echo "BIN_NAME=${BIN_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set testing options
|
||||
id: test-options
|
||||
shell: bash
|
||||
run: |
|
||||
# test only library unit tests and binary for arm-type targets
|
||||
unset CARGO_TEST_OPTIONS
|
||||
case ${target} in
|
||||
arm-* | aarch64-*)
|
||||
CARGO_TEST_OPTIONS="--bin=${name}" ;;
|
||||
esac
|
||||
echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
env:
|
||||
cargo_test_options: ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}}
|
||||
run: $BUILD_CMD test --locked --target="${target}" "${cargo_test_options}"
|
||||
|
||||
- name: Generate completions
|
||||
id: completions
|
||||
shell: bash
|
||||
run: make completions
|
||||
|
||||
- name: Create tarball
|
||||
id: package
|
||||
shell: bash
|
||||
run: bash scripts/create-archive.sh
|
||||
env:
|
||||
BIN_PATH: ${{ steps.bin.outputs.BIN_PATH }}
|
||||
TARGET: ${{ env.target }}
|
||||
ARCHIVE_NAME: ${{ env.name }}-v${{ needs.crate_metadata.outputs.version }}-${{ env.target }}
|
||||
PKG_STAGING: "${{ env.CICD_INTERMEDIATES_DIR }}/package"
|
||||
|
||||
- name: Create Debian package
|
||||
id: debian-package
|
||||
shell: bash
|
||||
if: startsWith(matrix.job.os, 'ubuntu')
|
||||
run: bash scripts/create-deb.sh
|
||||
env:
|
||||
TARGET: ${{ matrix.job.target }}
|
||||
DPKG_VERSION: ${{ needs.crate_metadata.version }}
|
||||
BIN_PATH: ${{ steps.bin.outputs.BIN_PATH }}
|
||||
|
||||
- name: "Artifact upload: tarball"
|
||||
id: upload-tarball
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
with:
|
||||
name: ${{ steps.package.outputs.PKG_NAME }}
|
||||
path: ${{ steps.package.outputs.PKG_PATH }}
|
||||
compression-level: 0 # tarball is already compressed
|
||||
|
||||
- name: "Artifact upload: Debian package"
|
||||
id: upload-deb
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||
if: steps.debian-package.outputs.DPKG_NAME
|
||||
with:
|
||||
name: ${{ steps.debian-package.outputs.DPKG_NAME }}
|
||||
path: ${{ steps.debian-package.outputs.DPKG_PATH }}
|
||||
compression-level: 0 # deb files are already compressed
|
||||
|
||||
- name: Check for release
|
||||
id: is-release
|
||||
shell: bash
|
||||
run: |
|
||||
unset IS_RELEASE ; if [[ $GITHUB_REF =~ ^refs/tags/v[0-9].* ]]; then IS_RELEASE='true' ; fi
|
||||
echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Attest artifacts"
|
||||
uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4
|
||||
if: steps.is-release.outputs.IS_RELEASE
|
||||
with:
|
||||
subject-path: |
|
||||
${{ steps.package.outputs.PKG_PATH }}
|
||||
${{ steps.debian-package.outputs.DPKG_PATH }}
|
||||
|
||||
- name: Publish archives and packages
|
||||
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
if: steps.is-release.outputs.IS_RELEASE
|
||||
with:
|
||||
files: |
|
||||
${{ steps.package.outputs.PKG_PATH }}
|
||||
${{ steps.debian-package.outputs.DPKG_PATH }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
winget:
|
||||
name: Publish to Winget
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: vedantmgoyal9/winget-releaser@b7370d4d152efdce803a26cc6ca9eee58cb2ad54 # v2
|
||||
with:
|
||||
identifier: sharkdp.fd
|
||||
installers-regex: '-pc-windows-msvc\.zip$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
@@ -0,0 +1,4 @@
|
||||
target/
|
||||
/autocomplete/
|
||||
/package/
|
||||
**/*.rs.bk
|
||||
@@ -0,0 +1,883 @@
|
||||
# 10.5.1
|
||||
|
||||
## Bugfixes
|
||||
- Fix `--absolute-path` turning verbatim (`\\?\`) search paths into relative paths on Windows: `\\?\UNC\server\share` is now reported as `\\server\share`, and `\\?\Volume{...}` paths are kept intact, see #2207 (@Duo-ee)
|
||||
- Make `--ignore` and `--no-hidden` after `-u` override only their half of `-u`, see #2192 (@NavpreetSSidhu)
|
||||
- Fix inline case-insensitive regex flags incorrectly making other `--and` patterns case-sensitive, see #2184 (@mabunemeh)
|
||||
- Accept `--format` templates that start with `-` when passed as a separate argument, see #2126 (@vulragrag-star)
|
||||
- Don't incorrectly escape newlines in error messages, see #2104
|
||||
- Restore jemalloc as default allocator on supported systems
|
||||
- Fix the `cargo binstall` release URL, which named the crate (`fd-find`) instead of the binary (`fd`) and fell back to a third-party mirror
|
||||
- Fix integer overflow when parsing size constraints, rejecting values that exceed `u64::MAX` instead of panicking, see #2196 (@parneetsingh022).
|
||||
- Various fixes and updates to zsh completion
|
||||
- Fix --glob patterns when using --full-path on windows. See #2067.
|
||||
|
||||
# 10.5.0
|
||||
|
||||
## Features
|
||||
- Add `--ignore-parent` option to override `--no-ignore-parent`, see #1958 (@tmchow)
|
||||
- Add `--exact` option to match the entire filename exactly (literal, non-substring).
|
||||
|
||||
## Bugfixes
|
||||
- Sanitize control characters and bidirectional override characters in filenames
|
||||
when output goes to a terminal, to prevent terminal escape-sequence injection.
|
||||
Also reject a placeholder as the executable for `--exec-batch`, while still
|
||||
allowing it for `--exec`.
|
||||
- Fix broken symlinks being incorrectly filtered out by `--min-depth` when following links (`--follow`), because their depth was not computed; see #1017 (@hexbinoct).
|
||||
- Handle invalid working directories gracefully when using `--full-path`, see #1900 (@Xavrir).
|
||||
- Fire the "search pattern contains a path separator" diagnostic for any pattern containing `/`, not just patterns that happen to name an existing directory. Preserves the legacy Windows behaviour that also flags native `\` separators when the pattern resolves to a real directory. See #1873.
|
||||
- Also fire the "search pattern contains a path separator" diagnostic for `--and` patterns, not only the primary positional pattern. `--and` patterns are matched against the file name just like the primary pattern, so a path separator in them silently returned zero results. See #1873.
|
||||
- Fix bug where passing "-" as a directory argument didn't actually search that directory, see #849 (@Sean-Kenneth-Doherty).
|
||||
- Fix panic when `--changed-before`/`--changed-within` is given an out-of-range `@` Unix timestamp; the value is now rejected gracefully, see #2081 (@nikolauspschuetz).
|
||||
|
||||
# 10.4.2
|
||||
|
||||
## Bugfixes
|
||||
- Fixed performance regression due to `--ignore-contain`; see #1913 and #1914
|
||||
|
||||
# 10.4.1
|
||||
|
||||
This is just a re-release of 10.4.0 due to an issue with the 10.4.0 release.
|
||||
|
||||
# 10.4.0
|
||||
|
||||
## Features
|
||||
- Add `--ignore-contain` option to ignore directories containing a named entry (e.g. to ignore [`CACHEDIR.TAG`](https://bford.info/cachedir/)); see #1727 (@fischman).
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fix Windows hyperlink generation for paths with spaces. (#1872)
|
||||
|
||||
- `--print0` combined with `--exec` will now print a `\0` between the output of each entry. Note that if there are multiple instances
|
||||
of `--exec`, the `\0` will be between each _set_ of commands, _not_ between each individual command run. Fixes #1797.
|
||||
|
||||
- Several bugs were fixed by an update to the `ignore` library used for handling ignore rules
|
||||
- #1506
|
||||
- #1667
|
||||
- #1813
|
||||
|
||||
- Fix completions for alias `fdfind` in deb release, see #1888 (@skane-lukas)
|
||||
|
||||
## Changes
|
||||
|
||||
- Minimum required rust version has been increased to 1.90.0. Notably, this means dropping fully support for intel Mac and Windows 7.
|
||||
- Statically link the CRT for MSVC builds via Cargo config to avoid runtime DLL dependencies, see #1874 (@FidelSch)
|
||||
|
||||
# 10.3.0
|
||||
|
||||
## Features
|
||||
|
||||
- Add a hidden `--mindepth` alias for `--min-depth`. (#1617)
|
||||
|
||||
|
||||
## Bugfixes
|
||||
|
||||
|
||||
## Changes
|
||||
|
||||
- Replace `humantime` crate and `chrono` crate with `jiff` crate, see #1690 (@sorairolake). This has some small changes to the
|
||||
way dates given to options such `--changed-within` and `--changed-before` including:
|
||||
- 'M' no longer means "month", as that could be confusing with minutes. Use "mo", "mos", "month" or "months" instead.
|
||||
- month and year now account for variability in the calander rather than being a hard-coded number of seconds. That is probably
|
||||
what you would expect, but it is a slight change in behavior.
|
||||
- aarch64 Windows was added to CI and release artifacts
|
||||
- Many dependencies were updated
|
||||
- Better support building on Illumos (there is no automated testing, but some known issues were fixed)
|
||||
|
||||
## Other
|
||||
|
||||
This will be the last release that has been tested on x86_64 Mac OS, since GitHub is
|
||||
dropping support for runners with that hardware.
|
||||
|
||||
It may also be the last release to use a version of Rust with tier-1 support for
|
||||
x86_64/intel Macs and Windows 7.
|
||||
|
||||
|
||||
# 10.2.0
|
||||
|
||||
## Features
|
||||
|
||||
- Add --hyperlink option to add OSC 8 hyperlinks to output
|
||||
|
||||
|
||||
## Bugfixes
|
||||
|
||||
|
||||
## Changes
|
||||
|
||||
- Build windows releases with rust 1.77 so windows 7 is still supported
|
||||
- Deb packages now include symlink for fdfind to be more consistent with official packages
|
||||
|
||||
|
||||
## Other
|
||||
|
||||
# 10.1.0
|
||||
|
||||
## Features
|
||||
|
||||
- Allow passing an optional argument to `--strip-cwd-prefix` of "always", "never", or "auto". to force whether the cwd prefix is stripped or not.
|
||||
- Add a `--format` option which allows using a format template for direct ouput similar to the template used for `--exec`. (#1043)
|
||||
|
||||
## Bugfixes
|
||||
- Fix aarch64 page size again. This time it should actually work. (#1085, #1549) (@tavianator)
|
||||
|
||||
|
||||
## Other
|
||||
|
||||
- aarch64-apple-darwin target added to builds on the release page. Note that this is a tier 2 rust target.
|
||||
|
||||
# v10.0.0
|
||||
|
||||
## Features
|
||||
|
||||
- Add `dir` as an alias to `directory` when using `-t` \ `--type`, see #1460 and #1464 (@Ato2207).
|
||||
- Add support for @%s date format in time filters similar to GNU date (seconds since Unix epoch for --older/--newer), see #1493 (@nabellows)
|
||||
- Breaking: No longer automatically ignore `.git` when using `--hidden` with vcs ignore enabled. This reverts the change in v9.0.0. While this feature
|
||||
was often useful, it also broke some existing workflows, and there wasn't a good way to opt out of it. And there isn't really a good way for us to add
|
||||
a way to opt out of it. And you can easily get similar behavior by adding `.git/` to your global fdignore file.
|
||||
See #1457.
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Respect NO_COLOR environment variable with `--list-details` option. (#1455)
|
||||
- Fix bug that would cause hidden files to be included despite gitignore rules
|
||||
if search path is "." (#1461, BurntSushi/ripgrep#2711).
|
||||
- aarch64 builds now use 64k page sizes with jemalloc. This fixes issues on some systems, such as ARM Macs that
|
||||
have a larger system page size than the system that the binary was built on. (#1547)
|
||||
- Address [CVE-2024-24576](https://blog.rust-lang.org/2024/04/09/cve-2024-24576.html), by increasing minimum rust version.
|
||||
|
||||
|
||||
## Changes
|
||||
- Minimum supported rust version is now 1.77.2
|
||||
|
||||
|
||||
# v9.0.0
|
||||
|
||||
## Performance
|
||||
|
||||
- Performance has been *significantly improved*, both due to optimizations in the underlying `ignore`
|
||||
crate (#1429), and in `fd` itself (#1422, #1408, #1362) - @tavianator.
|
||||
[Benchmarks results](https://gist.github.com/tavianator/32edbe052f33ef60570cf5456b59de81) show gains
|
||||
of 6-8x for full traversals of smaller directories (100k files) and up to 13x for larger directories (1M files).
|
||||
|
||||
- The default number of threads is now constrained to be at most 64. This should improve startup time on
|
||||
systems with many CPU cores. (#1203, #1410, #1412, #1431) - @tmccombs and @tavianator
|
||||
|
||||
- New flushing behavior when writing output to stdout, providing better performance for TTY and non-TTY
|
||||
use cases, see #1452 and #1313 (@tavianator).
|
||||
|
||||
## Features
|
||||
|
||||
- Support character and block device file types, see #1213 and #1336 (@cgzones)
|
||||
- Breaking: `.git/` is now ignored by default when using `--hidden` / `-H`, use `--no-ignore` / `-I` or
|
||||
`--no-ignore-vcs` to override, see #1387 and #1396 (@skoriop)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fix `NO_COLOR` support, see #1421 (@acuteenvy)
|
||||
|
||||
## Other
|
||||
|
||||
- Fixed documentation typos, see #1409 (@marcospb19)
|
||||
|
||||
## Thanks
|
||||
|
||||
Special thanks to @tavianator for his incredible work on performance in the `ignore` crate and `fd` itself.
|
||||
|
||||
|
||||
|
||||
# v8.7.1
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- `-1` properly conflicts with the exec family of options.
|
||||
- `--max-results` overrides `-1`
|
||||
- `--quiet` properly conflicts with the exec family of options. This used to be the case, but broke during the switch to clap-derive
|
||||
- `--changed-within` now accepts a space as well as a "T" as the separator between date and time (due to update of chrono dependency)
|
||||
|
||||
## Other
|
||||
- Many dependencies were updated
|
||||
- Some documentation was updated and fixed
|
||||
|
||||
# v8.7.0
|
||||
|
||||
## Features
|
||||
|
||||
- Add flag --no-require-git to always respect gitignore files, see #1216 (@vegerot)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fix logic for when to use global ignore file. There was a bug where the only case where the
|
||||
global ignore file wasn't processed was if `--no-ignore` was passed, but neither `--unrestricted`
|
||||
nor `--no-global-ignore-file` is passed. See #1209
|
||||
|
||||
# v8.6.0
|
||||
|
||||
## Features
|
||||
|
||||
- New `--and <pattern>` option to add additional patterns that must also be matched. See #315
|
||||
and #1139 (@Uthar)
|
||||
- Added `--changed-after` as alias for `--changed-within`, to have a name consistent with `--changed-before`.
|
||||
|
||||
|
||||
## Changes
|
||||
|
||||
- Breaking: On Unix-like systems, `--type executable` now additionally checks if
|
||||
the file is executable by the current user, see #1106 and #1169 (@ptipiak)
|
||||
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Use fd instead of fd.exe for Powershell completions (when completions are generated on windows)
|
||||
|
||||
|
||||
## Other
|
||||
|
||||
|
||||
# v8.5.3
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fix completion generation to not include full path of fd command
|
||||
- Fix build error if completions feature is disabled
|
||||
|
||||
# v8.5.2
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fix --owner option value parsing, see #1163 and #1164 (@tmccombs)
|
||||
|
||||
|
||||
# v8.5.1
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fix --threads/-j option value parsing, see #1160 and #1162 (@sharkdp)
|
||||
|
||||
|
||||
# v8.5.0
|
||||
|
||||
## Features
|
||||
|
||||
- `--type executable`/`-t` now works on Windows, see #1051 and #1061 (@tavianator)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fixed differences between piped / non-piped output. This changes `fd`s behavior back to what we
|
||||
had before 8.3.0, i.e. there will be no leading `./` prefixes, unless `--exec`/`-x`,
|
||||
`--exec-batch`/`-X`, or `--print0`/`-0` are used. `--strip-cwd-prefix` can be used to strip that
|
||||
prefix in those cases. See #1046, #1115, and #1121 (@tavianator)
|
||||
- `fd` could previously crash with a panic due to a race condition in Rusts standard library
|
||||
(see https://github.com/rust-lang/rust/issues/39364). This has been fixed by switching to a different
|
||||
message passing implementation, see #1060 and #1146 (@tavianator)
|
||||
- `fd`s memory usage will not grow unboundedly on huge directory trees, see #1146 (@tavianator)
|
||||
- fd returns an error when current working directory does not exist while a search path is
|
||||
specified, see #1072 (@vijfhoek)
|
||||
- Improved "command not found" error message, see #1083 and #1109 (@themkat)
|
||||
- Preserve command exit codes when using `--exec-batch`, see #1136 and #1137 (@amesgen)
|
||||
|
||||
## Changes
|
||||
|
||||
- No leading `./` prefix for non-interactive results, see above.
|
||||
- fd now colorizes paths in parallel, significantly improving performance, see #1148 (@tavianator)
|
||||
- fd can now avoid `stat` syscalls even when colorizing paths, as long as the color scheme doesn't
|
||||
require metadata, see #1148 (@tavianator)
|
||||
- The statically linked `musl` versions of `fd` now use `jmalloc`, leading to a significant performance
|
||||
improvement, see #1062 (@tavianator)
|
||||
|
||||
## Other
|
||||
|
||||
- Added link back to GitHub in man page and `--help` text, see #1086 (@scottchiefbaker)
|
||||
- Major update in how `fd` handles command line options internally, see #1067 (@tmccombs)
|
||||
|
||||
# v8.4.0
|
||||
|
||||
## Features
|
||||
|
||||
- Support multiple `--exec <cmd>` instances, see #406 and #960 (@tmccombs)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- "Argument list too long" errors can not appear anymore when using `--exec-batch`/`-X`, as the command invocations are automatically batched at the maximum possible size, even if `--batch-size` is not given. See #410 and #1020 (@tavianator)
|
||||
|
||||
## Changes
|
||||
|
||||
- Directories are now printed with an additional path separator at the end: `foo/bar/`, see #436 and #812 (@yyogo)
|
||||
- The `-u` flag was changed to be equivalent to `-HI` (previously, a single `-u` was only equivalent to `-I`). Additional `-u` flags are still allowed, but ignored. See #840 and #986 (@jacksontheel)
|
||||
|
||||
## Other
|
||||
|
||||
- Added installation instructions for RHEL8, see #989 (@ethsol)
|
||||
|
||||
|
||||
# v8.3.2
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Invalid absolute path on windows when searching from the drive root, see #931 and #936 (@gbarta)
|
||||
|
||||
|
||||
# v8.3.1
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Stop implying `--no-ignore-parent` when `--no-vcs-ignore` is supplied, see #907, #901, #908 (@tmccombs)
|
||||
- fd no longer waits for the whole traversal if the only matches arrive within max_buffer_time, see #868 and #895 (@tavianator)
|
||||
- `--max-results=1` now immediately quits after the first result, see #867
|
||||
- `fd -h` does not panic anymore when stdout is closed, see #897
|
||||
|
||||
## Changes
|
||||
|
||||
- Disable jemalloc on FreeBSD, see #896 (@xanderio)
|
||||
- Updated man page, see #912 (@rlue)
|
||||
- Updated zsh completions, see #932 (@tmccombs)
|
||||
|
||||
|
||||
# v8.3.0
|
||||
|
||||
## Performance improvements
|
||||
|
||||
- Colorized output is now significantly faster, see #720 and #853 (@tavianator)
|
||||
- Writing to stdout is now buffered if the output does not go to a TTY. This increases performance
|
||||
when the output of `fd` is piped to another program or to a file, see #885 (@tmccombs, original
|
||||
implementation by @sourlemon207)
|
||||
- File metadata is now cached between the different filters that require it (e.g. `--owner`,
|
||||
`--size`), reducing the number of `stat` syscalls when multiple filters are used; see #863
|
||||
(@tavianator, original implementation by @alexmaco)
|
||||
|
||||
## Features
|
||||
|
||||
- Don't buffer command output from `--exec` when using a single thread. See #522
|
||||
- Add new `-q, --quiet` flag, see #303 (@Asha20)
|
||||
- Add new `--no-ignore-parent` flag, see #787 (@will459)
|
||||
- Add new `--batch-size` flag, see #410 (@devonhollowood)
|
||||
- Add opposing command-line options, see #595 (@Asha20)
|
||||
- Add support for more filesystem indicators in `LS_COLORS`, see
|
||||
https://github.com/sharkdp/lscolors/pull/35 (@tavianator)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Always show the `./` prefix for search results unless the output is a TTY or `--strip-cwd-prefix` is set, see #760 and #861 (@jcaplan)
|
||||
- Set default path separator to `/` in MSYS, see #537 and #730 (@aswild)
|
||||
- fd cannot search files under a RAM disk, see #752
|
||||
- fd doesn't show substituted drive on Windows, see #365
|
||||
- Properly handle write errors to devices that are full, see #737
|
||||
- Use local time zone for time functions (`--change-newer-than`, `--change-older-than`), see #631 (@jacobmischka)
|
||||
- Support `--list-details` on more platforms (like BusyBox), see #783
|
||||
- The filters `--owner`, `--size`, and `--changed-{within,before}` now apply to symbolic links
|
||||
themselves, rather than the link target, except when `--follow` is specified; see #863
|
||||
- Change time comparisons to be exclusive, see #794 (@jacobmischka)
|
||||
|
||||
## Changes
|
||||
|
||||
- Apply custom `--path-separator` to commands run with `--exec(-batch)` and `--list-details`, see #697 (@aswild)
|
||||
|
||||
## Other
|
||||
|
||||
- Many documentation updates
|
||||
|
||||
|
||||
# v8.2.1
|
||||
|
||||
No functional changes with respect to v8.2.0. Bugfix in the release process.
|
||||
|
||||
# v8.2.0
|
||||
|
||||
## Features
|
||||
|
||||
- Add new `--prune` flag, see #535 (@reima)
|
||||
- Improved the usability of the time-based options, see #624 and #645 (@gorogoroumaru)
|
||||
- Add support for exact file sizes in the `--size` filter, see #669 and #696 (@Rogach)
|
||||
- `fd` now prints an error message if the search pattern requires a leading dot but
|
||||
`--hidden` is not enabled (Unix only), see #615
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Avoid panic when performing limited searches in directories with restricted permissions, see #678
|
||||
- Invalid numeric command-line arguments are silently ignored, see #675
|
||||
- Disable jemalloc on Android, see #662
|
||||
- The `--help` text will be colorless if `NO_COLOR` has been set, see #600 (@xanonid)
|
||||
|
||||
## Changes
|
||||
|
||||
- If `LS_COLORS` is not set (e.g. on Windows), we now provide a more comprehensive default which
|
||||
includes much more filetypes, see #604 and #682 (mjsir911).
|
||||
|
||||
## Other
|
||||
|
||||
- Added `zsh` completion files, see #654 and #189 (@smancill)
|
||||
|
||||
# v8.1.1
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Support colored output on older Windows versions if either (1) `--color=always` is set or (2) the `TERM` environment variable is set. See #469
|
||||
|
||||
# v8.1.0
|
||||
|
||||
## Features
|
||||
|
||||
- Add new `--owner [user][:group]` filter. See #307 (pull #581) (@alexmaco)
|
||||
- Add support for a global ignore file (`~/.config/fd/ignore` on Unix), see #575 (@soedirgo)
|
||||
- Do not exit immediately if one of the search paths is missing, see #587 (@DJRHails)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Reverted a change from fd 8.0 that enabled colors on all Windows terminals (see below) in order to support older Windows versions again, see #577. Unfortunately, this re-opens #469
|
||||
- Fix segfault caused by jemalloc on macOS Catalina, see #498
|
||||
- Fix `--glob` behavior with empty pattern, see #579 (@SeamusConnor)
|
||||
- Fix `--list-details` on FreeBSD, DragonFly BSD, OpenBSD and NetBSD. See #573 (@t6)
|
||||
|
||||
## Changes
|
||||
|
||||
- Updated documentation for `--size`, see #584
|
||||
|
||||
# v8.0.0
|
||||
|
||||
## Features
|
||||
|
||||
- Add a new `-l`/`--list-details` option to show more details about the search results. This is
|
||||
basically an alias for `--exec-batch ls -l` with some additional `ls` options.
|
||||
This can be used in order to:
|
||||
* see metadata like permissions, owner, file size, modification times (#491)
|
||||
* see symlink targets (#482)
|
||||
* achieve a deterministic output order (#324, #196, #159)
|
||||
- Add a new `--max-results=<count>` option to limit the number of search results, see #472, #476 and #555
|
||||
This can be useful to speed up searches in cases where you know that there are only N results.
|
||||
Using this option is also (slightly) faster than piping to `head -n <count>` where `fd` can only
|
||||
exit when it finds the search results `<count> + 1`.
|
||||
- Add the alias `-1` for `--max-results=1`, see #561. (@SimplyDanny).
|
||||
- Add new `--type socket` and `--type pipe` filters, see #511.
|
||||
- Add new `--min-depth <depth>` and `--exact-depth <depth>` options in addition to the existing option
|
||||
to limit the maximum depth. See #404.
|
||||
- Support additional ANSI font styles in `LS_COLORS`: faint, slow blink, rapid blink, dimmed, hidden and strikethrough.
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Preserve non-UTF8 filenames: invalid UTF-8 filenames are now properly passed to child-processes
|
||||
when using `--exec`, `--exec-batch` or `--list-details`. In `fd`'s output, we replace non-UTF-8
|
||||
sequences with the "�" character. However, if the output of `fd` goes to another process, we
|
||||
print the actual bytes of the filename. For more details, see #558 and #295.
|
||||
- `LS_COLORS` entries with unsupported font styles are not completely ignored, see #552
|
||||
|
||||
## Changes
|
||||
|
||||
- Colored output will now be enabled by default on older Windows versions.
|
||||
This allows the use of colored output if the terminal supports it (e.g.
|
||||
MinTTY, Git Bash). On the other hand, this will be a regression for users
|
||||
on older Windows versions with terminals that do not support ANSI escape
|
||||
sequences. Affected users can use an alias `fd="fd --color=never"` to
|
||||
continue using `fd` without colors. There is no change of behavior for
|
||||
Windows 10. See #469.
|
||||
- When using `--glob` in combination with `--full-path`, a `*` character does not match a path
|
||||
separation character (`/` or `\\`) anymore. You can use `**` for that. This allows things like
|
||||
`fd -p -g '/some/base/path/*/*/*.txt'` which would previously match to arbitrary depths (instead
|
||||
of exactly two folders below `/some/base/path`. See #404.
|
||||
- "Legacy" support to use `fd -exec` (with a single dash) has been removed. Use `fd -x` or
|
||||
`fd --exec` instead.
|
||||
- Overall improved error handling and error messages.
|
||||
|
||||
|
||||
## Other
|
||||
|
||||
- Korean translation of the README, see: [한국어](https://github.com/spearkkk/fd-kor) (@spearkkk)
|
||||
|
||||
|
||||
# v7.5.0
|
||||
|
||||
## Features
|
||||
|
||||
- Added `--one-file-system` (aliases: `--mount`, `--xdev`) to not cross file system boundaries on Unix and Windows, see #507 (@FallenWarrior2k).
|
||||
- Added `--base-directory` to change the working directory in which `fd` is run, see #509 and #475 (@hajdamak).
|
||||
- `fd` will not use colored output if the `NO_COLOR` environment variable is set, see #550 and #551 (@metadave).
|
||||
- `fd --exec` will return exit code 1 if one of the executed commands fails, see #526 and #531 (@fusillicode and @Giuffre)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- Fixed 'command not found' error when using zsh completion, see #487 (@barskern).
|
||||
- `fd -L` should include broken symlinks, see #357 and #497 (@tommilligan, @neersighted and @sharkdp)
|
||||
- Display directories even if we don't have permission to enter, see #437 (@sharkdp)
|
||||
|
||||
## Changes
|
||||
|
||||
- A flag can now be passed multiple times without producing an error, see #488 and #496 (@rootbid).
|
||||
- Search results are sorted when using the `-X` option to match the behaviour of piping to `xargs`, see #441 and #524 (@Marcoleni @crash-g).
|
||||
|
||||
|
||||
# v7.4.0
|
||||
|
||||
## Performance improvements
|
||||
|
||||
- Reduce number of `stat` syscalls, improving the performance for searches where file metadata is
|
||||
required (`--type`, `--size`, `--changed-within`, …), see #434 (@tavianator)
|
||||
- Use jemalloc by default, improving the performance for almost all searches, see #481. Note that
|
||||
Windows and `*musl*` builds do not profit from this.
|
||||
|
||||
## Features
|
||||
|
||||
- Added a new `-g`/`--glob` option to switch to glob-based searches (instead of regular expression
|
||||
based searches). This is accompanied by a new `--regex` option that can be used to switch back,
|
||||
if users want to `alias fd="fd --glob"`. See #284
|
||||
- Added a new `--path-separator <sep>` option which can be useful for Windows users who
|
||||
want/need `fd` to use `/` instead of `\`, see #428 and #153 (@mookid)
|
||||
- Added support for hidden files on Windows, see #379
|
||||
- When `fd` is run with the `--exec-batch`/`-X` option, it now exposes the exit status of the
|
||||
command that was run, see #333.
|
||||
- Exit immediately when Ctrl-C has been pressed twice, see #423
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Make `--changed-within`/`--changed-before` work for directories, see #470
|
||||
|
||||
## Other
|
||||
|
||||
- Pre-built `fd` binaries should now be available for `armhf` targets, see #457 (@detly)
|
||||
- `fd` is now available on Alpine Linux, see #451 (@5paceToast)
|
||||
- `fd` is now in the officla FreeBSD repositories, see #412 (@t6)
|
||||
- Added OpenBSD install instructions, see #421 (@evitalis)
|
||||
- Added metadata to the Debian package, see #416 (@cathalgarvey)
|
||||
- `fd` can be installed via npm, see #438 (@pablopunk)
|
||||
|
||||
|
||||
# v7.3.0
|
||||
|
||||
## Features
|
||||
|
||||
- New `--exec-batch <cmd>`/`-X <cmd>` option for batch execution of commands, see #360 (@kimsnj).
|
||||
This allows you to do things like:
|
||||
``` bash
|
||||
fd … -X vim # open all search results in vim (or any other editor)
|
||||
fd … -X ls -l # view detailed stats about the search results with 'ls'
|
||||
fd -e svg -X inkscape # open all SVG files in Inkscape
|
||||
```
|
||||
- Support for 24-bit color codes (when specified via `LS_COLORS`) as well as
|
||||
different font styles (bold, italic, underline).
|
||||
|
||||
## Changes
|
||||
|
||||
- A few performance improvements, in particular when printing lots of colorized
|
||||
results to the console, see #370
|
||||
- The `LS_COLORS` handling has been "outsourced" to a separate crate (https://github.com/sharkdp/lscolors) that is now being used by other tools as well: [fselect](https://github.com/jhspetersson/fselect), [lsd](https://github.com/Peltoche/lsd/pull/84). For details, see #363.
|
||||
|
||||
## Other
|
||||
|
||||
- `fd` will be available in Ubuntu Disco DIngo (19.04), see #373 (@sylvestre)
|
||||
- This release should come with a static ARM binary (`arm-unknown-linux-musleabihf`), see #320 (@duncanfinney)
|
||||
- Various documentation improvements, see #389
|
||||
|
||||
## Thanks
|
||||
|
||||
Special thanks to @alexmaco for his awesome work on refactoring and code improvements! (see #401, #398, and #383)
|
||||
|
||||
# v7.2.0
|
||||
|
||||
## Features
|
||||
|
||||
* Added support for filtering by file modification time by adding two new options `--changed-before <date|duration>` and `--changed-within <..>`. For more details, see the `--help` text, the man page, the relevant issue #165 and the PR #339 (@kimsnj)
|
||||
* Added `--show-errors` option to enable the display of filesystem error messages such as "permission denied", see #311 (@psinghal20 and @majecty)
|
||||
* Added `--maxdepth` as a (hidden) alias for `--max-depth`, see #323 (@mqudsi)
|
||||
* Added `--search-path` option which can be supplied to replace the positional `path` argument at any position.
|
||||
|
||||
## Changes
|
||||
|
||||
* Loosen strict handling of missing `--ignore-file`, see #280 (@psinghal20)
|
||||
* Re-enabled `.ignore` files, see #156.
|
||||
|
||||
## Bugfixes
|
||||
|
||||
* `fd` could previously get stuck when run from the root directory in the
|
||||
presence of zombie processes. This curious bug has been fixed in Rust 1.29 and higher. For more details, see #288, [rust-lang/rust#50619](https://github.com/rust-lang/rust/issues/50619) and [the fix](https://github.com/rust-lang/rust/pull/50630)
|
||||
|
||||
## Other
|
||||
|
||||
* `fd` has officially landed in Debian! See #345 for details. Thanks goes to @sylvestre, @paride and possibly others I don't know about.
|
||||
* Added Chinese translation of README (@chinanf-boy)
|
||||
|
||||
## Thanks
|
||||
|
||||
A special thanks goes to @joshleeb for his amazing improvements throughout
|
||||
the code base (new tests, refactoring work and various other things)!
|
||||
|
||||
|
||||
# v7.1.0
|
||||
|
||||
## Features
|
||||
|
||||
* Added `--size` filter option, see #276 (@stevepentland, @JonathanxD and @alexmaco)
|
||||
* Added `--type empty` (or `-t e`) to search for empty files and/or directories, see #273
|
||||
|
||||
## Changes
|
||||
|
||||
* With the new version, `.gitignore` files will only be respected in Git repositories, not outside.
|
||||
* A few performance improvements for `--type` searches, see 641976cf7ad311ba741571ca8b7f02b2654b6955 and 50a2bab5cd52d26d4a3bc786885a2c270ed3b227
|
||||
|
||||
## Other
|
||||
|
||||
* Starting with this release, we will offer pre-built ARM binaries, see #244
|
||||
* Added instructions on how to use `fd` with `emacs`, see #282 (@redguardtoo)
|
||||
* `fd` is now in the official openSUSE repositories, see #275 (@avindra)
|
||||
* `fd` is now available via MacPorts, see #291 (@raimue)
|
||||
|
||||
|
||||
# v7.0.0
|
||||
|
||||
## Features
|
||||
|
||||
* Added `--type executable` (or `-t x`) to search for executable files only, see #246 (@PramodBisht)
|
||||
* Added support for `.fdignore` files, see #156 and #241.
|
||||
* Added `--ignore-file` option to add custom ignore files, see #156.
|
||||
* Suggest `--fixed-strings` on invalid regular expressions, see #234 (@PramodBisht)
|
||||
* Detect when user supplied path instead of pattern, see #235.
|
||||
|
||||
## Changes
|
||||
|
||||
* `.ignore` and `.rgignore` files are not parsed anymore. Use `.fdignore` files
|
||||
or add custom files via `--ignore-file` instead.
|
||||
* Updated to `regex-syntax` 0.5 (@cuviper)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
* Properly normalize absolute paths, see #268
|
||||
* Invalid utf8 filenames displayed when `-e` is used, see #250
|
||||
* If `--type` is used, fifos/sockets/etc. are always shown, see #260
|
||||
|
||||
## Other
|
||||
|
||||
* Packaging:
|
||||
* The Arch Linux package is now simply called `fd`.
|
||||
* There is now a `fd` ebuild for Gentoo Linux.
|
||||
* There is a `scoop` package for `fd` (Windows).
|
||||
* There is a `Chocolatey` package for `fd` (Windows).
|
||||
* There is a Fedora `copr` package for `fd`.
|
||||
|
||||
|
||||
# v6.3.0
|
||||
|
||||
## Features
|
||||
|
||||
* Files with multiple extensions can now be found via `--extension`/`-e`, see #214 (@althonos)
|
||||
``` bash
|
||||
> fd -e tar.gz
|
||||
```
|
||||
|
||||
* Added new `-F`/`--fixed-strings`/`--literal` option that treats the pattern as a literal string instead of a regular expression, see #157
|
||||
|
||||
``` bash
|
||||
> fd -F 'file(1).txt'
|
||||
```
|
||||
|
||||
* Allow `-exec` to work as `--exec`, see #226 (@stevepentland)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
* Fixed `Ctrl-C` handling when using `--exec`, see #224 (@Doxterpepper)
|
||||
|
||||
* Fixed wrong file owner for files in deb package, see #213
|
||||
|
||||
## Other
|
||||
|
||||
* Replaced old gif by a fancy new SVG screencast (@marionebl)
|
||||
* Updated [benchmark results](https://github.com/sharkdp/fd#benchmark) (fd has become faster in the meantime!). There is a new repository that hosts several benchmarking scripts for fd: https://github.com/sharkdp/fd-benchmarks
|
||||
|
||||
|
||||
# v6.2.0
|
||||
|
||||
## Features
|
||||
|
||||
* Support for filtering by multiple file extensions and multiple file types, see #199 and #177
|
||||
(@tkadur).
|
||||
|
||||
For example, it's possible to search for C++ source or header files:
|
||||
``` bash
|
||||
> fd -e cpp -e c -e cxx -e h pattern
|
||||
```
|
||||
|
||||
## Changes
|
||||
|
||||
* The size of the output buffer (for sorting search results) is now limited to 1000 entries. This
|
||||
improves the search speed significantly if there are a lot of results, see #191 (@sharkdp).
|
||||
|
||||
## Bugfixes
|
||||
|
||||
* Fix a bug where long-running searches could not be killed via Ctrl-C, see #210 (@Doxterpepper)
|
||||
* fd's exit codes are now in accordance with Unix standards, see #201 (@Doxterpepper)
|
||||
|
||||
## Other
|
||||
|
||||
* Bash, zsh and fish completion should now work with the Ubuntu `.deb` packages, see #195 and #209
|
||||
(@tmccombs and @sharkdp)
|
||||
* There is a new section on how to set up `fzf` to use `fd` in the
|
||||
[README](https://github.com/sharkdp/fd#using-fd-with-fzf), see #168.
|
||||
|
||||
|
||||
# v6.1.0
|
||||
|
||||
## Features
|
||||
|
||||
* Support for multiple search paths, see #166 (@Doxterpepper)
|
||||
* Added `--no-ignore-vcs` option to disable `.gitignore` and other VCS ignore files,
|
||||
without disabling `.ignore` files - see #156 (@ptzz).
|
||||
|
||||
## Bugfixes
|
||||
|
||||
* Handle terminal signals, see #128 (@Doxterpepper)
|
||||
* Fixed hang on `--exec` when user input was required, see #178 and #193 (@reima)
|
||||
|
||||
## Other
|
||||
|
||||
* Debian packages are now created via Travis CI and should be available for this and all
|
||||
future releases (@tmccombs).
|
||||
* fd is now available on Void Linux (@maxice8)
|
||||
* The minimum required Rust version is now 1.20
|
||||
|
||||
## Thanks
|
||||
|
||||
@Doxterpepper deserves a special mention for his great work that is included in this release and
|
||||
for the support in ticket discussions and concerning Travis CI fixes. Thank you very much!
|
||||
|
||||
Thanks also go out to @tmccombs for the work on Debian packages and for reviewing a lot of pull requests!
|
||||
|
||||
# v6.0.0
|
||||
|
||||
## Changes
|
||||
|
||||
- The `--exec`/`-x` option does not spawn an intermediate shell anymore. This improves the
|
||||
performance of parallel command execution and fixes a whole class of (present and potentially
|
||||
future) problems with shell escaping. The drawback is that shell commands cannot directly be
|
||||
called with `--exec`. See #155 for the full discussion. These changes have been implemented by
|
||||
@reima (Thanks!).
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- `--exec` does not escape cmd.exe metacharacters on Windows (see #155, as above).
|
||||
|
||||
## Other
|
||||
|
||||
* *fd* is now available in the FreeBSD ports (@andoriyu)
|
||||
* The minimal `rustc` version is now checked when building with `cargo`, see #164 (@matematikaadit)
|
||||
* The output directory for the shell completion files is created if it does not exist (@andoriyu)
|
||||
|
||||
|
||||
# v5.0.0
|
||||
|
||||
## Features
|
||||
|
||||
* Added new `--exec`, `-x` option for parallel command execution (@mmstick, see #84 and #116). See the corresponding [README section](https://github.com/sharkdp/fd#parallel-command-execution) for an introduction.
|
||||
* Auto-disable color output on unsupported Windows shells like `cmd.exe` (@iology, see #129)
|
||||
* Added the `--exclude`, `-X` option to suppress certain files/directories in the search results
|
||||
(see #89).
|
||||
* Added ripgrep aliases `-u` and `-uu` for `--no-ignore` and `--no-ignore --hidden`, respectively
|
||||
(@unsignedint, see #92)
|
||||
* Added `-i`, `--ignore-case` (@iology, see #95)
|
||||
* Made smart case really smart (@reima, see #103)
|
||||
* Added RedoxOS support (@goyox86, see #131)
|
||||
|
||||
## Changes
|
||||
|
||||
* The dot `.` can now match newlines in file names (@iology, see #111)
|
||||
* The short `--type` argument for symlinks has been changed from `s` to `l` (@jcpetkovich, see #83)
|
||||
|
||||
## Bugfixes
|
||||
|
||||
* Various improvements in root-path and symlink handling (@iology, see #82, #107, and #113)
|
||||
* Fixed absolute path handling on Windows (@reima, #93)
|
||||
* Fixed: current directory not included when using relative path (see #81)
|
||||
* Fixed `--type` behavior for unknown file types (@iology, see #150)
|
||||
* Some fixes around `--exec` (@iology, see #142)
|
||||
|
||||
## Other
|
||||
|
||||
* Major updates and bugfixes to our continuous integration and deployment tooling on Travis
|
||||
(@matematikaadit, see #149, #145, #133)
|
||||
* Code style improvements & automatic style checking via `rustfmt` on Travis (@Detegr, see #99)
|
||||
* Added a man page (@pickfire, see #77)
|
||||
* *fd* has been relicensed under the dual license MIT/Apache-2.0 (@Detegr, see #105)
|
||||
* Major refactorings and code improvements (Big thanks to @gsquire, @reima, @iology)
|
||||
* First version of [`CONTRIBUTING`](https://github.com/sharkdp/fd/blob/master/CONTRIBUTING.md) guidelines
|
||||
* There is now a Nix package (@mehandes)
|
||||
* *fd* is now in the official Arch Linux repos (@cassava)
|
||||
* Improved tooling around shell completion files (@ImbaKnugel, see #124)
|
||||
* Updated tutorial in the [`README`](https://github.com/sharkdp/fd/blob/master/README.md)
|
||||
* The minimum required version of Rust has been bumped to 1.19.
|
||||
|
||||
## Thanks
|
||||
|
||||
A *lot* of things have happened since the last release and I'd like to thank all contributors for their great support. I'd also like to thank those that have contributed by reporting bugs and by posting feature requests.
|
||||
|
||||
I'd also like to take this chance to say a special Thank You to a few people that have stood out in one way or another: To @iology, for contributing a multitude of bugfixes, improvements and new features. To @reima and @Detegr for their continuing great support. To @mmstick, for implementing the most advanced new feature of *fd*. And to @matematikaadit for the CI/tooling upgrades.
|
||||
|
||||
|
||||
# v4.0.0
|
||||
|
||||
## Features
|
||||
|
||||
* Added filtering by file extension, for example `fd -e txt`, see #56 (@reima)
|
||||
* Add option to force colored output: `--color always`, see #49 (@Detegr)
|
||||
* Generate Shell completions for Bash, ZSH, Fish and Powershell, see #64 (@ImbaKnugel)
|
||||
* Better & extended `--help` text (@abaez and @Detegr)
|
||||
* Proper Windows support, see #70
|
||||
|
||||
## Changes
|
||||
|
||||
* The integration tests have been re-written in Rust :sparkles:, making them platform-independent and easily callable via `cargo test` - see #65 (many thanks to @reima!)
|
||||
* New tutorial in the README (@deg4uss3r)
|
||||
* Reduced number of `stat` syscalls for each result from 3 to 1, see #36.
|
||||
* Enabled Appveyor CI
|
||||
|
||||
# v3.1.0
|
||||
|
||||
## Features
|
||||
- Added file type filtering, e.g. `find --type directory` or `find -t f` (@exitium)
|
||||
|
||||
# v3.0.0
|
||||
|
||||
## Features
|
||||
- Directories are now traversed in parallel, leading to significant performance improvements (see [benchmarks](https://github.com/sharkdp/fd#benchmark))
|
||||
- Added `--print0` option (@michaelmior)
|
||||
- Added AUR packages (@wezm)
|
||||
|
||||
## Changes
|
||||
- Changed short flag for `--follow` from `-f` to `-L` (consistency with `ripgrep`)
|
||||
|
||||
# v2.0.0
|
||||
|
||||
* Changed `--sensitive` to `--case-sensitive`
|
||||
* Changed `--absolute` to `--absolute-path`
|
||||
* Throw an error if root directory is not existent, see #39
|
||||
* Use absolute paths if the root dir is an absolute path, see #40
|
||||
* Handle invalid UTF-8, see #34 #38
|
||||
* Support `-V`, `--version` by switching from `getopts` to `clap`.
|
||||
|
||||
Misc:
|
||||
* It's now possible to install `fd` via homebrew on macOS: `brew install fd`.
|
||||
|
||||
# v1.1.0
|
||||
|
||||
- Windows compatibility (@sebasv), see #29 #35
|
||||
- Safely exit on broken output pipes (e.g.: usage with `head`, `tail`, ..), see #24
|
||||
- Backport for rust 1.16, see #23
|
||||
|
||||
# v1.0.0
|
||||
|
||||
* Respect `.(git)ignore` files
|
||||
* Use `LS_COLORS` environment variable directly, instead of `~/.dir_colors` file.
|
||||
* Added unit and integration tests
|
||||
* Added optional second argument (search path)
|
||||
|
||||
# v0.3.0
|
||||
|
||||
- Parse dircolors files, closes #20
|
||||
- Colorize each path component, closes #19
|
||||
- Add short command line option for --hidden, see #18
|
||||
|
||||
# v0.2.0
|
||||
|
||||
- Option to follow symlinks, disable colors, closes #16, closes #17
|
||||
- `--filename` instead of `--full-path`
|
||||
- Option to search hidden directories, closes #12
|
||||
- Configurable search depth, closes #13
|
||||
- Detect interactive terminal, closes #11
|
||||
|
||||
# v0.1.0
|
||||
|
||||
Initial release
|
||||
@@ -0,0 +1,54 @@
|
||||
## Contributing to *fd*
|
||||
|
||||
**Thank you very much for considering contributing to this project!**
|
||||
|
||||
We welcome any form of contribution:
|
||||
|
||||
* New issues (feature requests, bug reports, questions, ideas, ...)
|
||||
* Pull requests (documentation improvements, code improvements, new features, ...)
|
||||
|
||||
**Note**: Before you take the time to open a pull request, please open a ticket first. This will
|
||||
give us the chance to discuss any potential changes first.
|
||||
|
||||
## Pull Request Expectations
|
||||
|
||||
1. Your contribution should be high quality, and best effort. In particular, please don't make a Pull Request
|
||||
that was created using generative AI unless you have reviewed it yourself and understand what it does, and be
|
||||
able to meaningfully answer questions and make changes to it as needed.
|
||||
2. Your code should successfully compile and pass tests before publishing a Pull Request. It's understandable
|
||||
if tests fail on one or two specific targets, but if the build doesn't pass for any targets, that is a signal
|
||||
that the PR is low effort.
|
||||
3. It is your responsibility to ensure that the generated code does not violate any copyrights or patents. This is
|
||||
true of all contributions, but is especially relevant when using an LLM that may be generating output that is
|
||||
very similar or even identical to existing code with an incompatible license.
|
||||
4. If an AI coding assistant or other LLM tool was used, you must indicate that an AI tool was used, and to what extent it was used.
|
||||
5. AI should not be used to generate comments when communicating with maintainers. We expect communication with the maintainers,
|
||||
including PR descriptions and comments, to be from humans, not from an AI. Using an AI to assist with grammar, spelling, and
|
||||
proofreading or translating is acceptable, as long as the content and voice of the
|
||||
message is your own.
|
||||
|
||||
## Add an entry to the changelog
|
||||
|
||||
If your contribution changes the behavior of `fd` (as opposed to a typo-fix
|
||||
in the documentation), please update the [`CHANGELOG.md`](CHANGELOG.md#upcoming-release) file
|
||||
and describe your changes. This makes the release process much easier and
|
||||
therefore helps to get your changes into a new `fd` release faster.
|
||||
|
||||
The top of the `CHANGELOG` contains an *"Upcoming release"* section with a few
|
||||
subsections (Features, Bugfixes, …). Please add your entry to the subsection
|
||||
that best describes your change.
|
||||
|
||||
Entries follow this format:
|
||||
```
|
||||
- Short description of what has been changed, see #123 (@user)
|
||||
```
|
||||
Here, `#123` is the number of the original issue and/or your pull request.
|
||||
Please replace `@user` by your GitHub username.
|
||||
|
||||
## Important links
|
||||
|
||||
* [Open issues](https://github.com/sharkdp/fd/issues)
|
||||
* [Open pull requests](https://github.com/sharkdp/fd/pulls)
|
||||
* [Development section in the README](https://github.com/sharkdp/fd#development)
|
||||
* [fd on crates.io](https://crates.io/crates/fd-find)
|
||||
* [LICENSE-APACHE](https://github.com/sharkdp/fd/blob/master/LICENSE-APACHE) and [LICENSE-MIT](https://github.com/sharkdp/fd/blob/master/LICENSE-MIT)
|
||||
Generated
+883
@@ -0,0 +1,883 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "argmax"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0144c58b55af0133ec3963ce5e4d07aad866e3bbcfdcddbf4590dbd7ad6ff557"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"nix 0.30.1",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
"terminal_size",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037e2a1a92236d0aff7e845093f64661d6df4c02c9fcc61a60e9e1d736fa392f"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"nix 0.31.3",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"defmt-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-macros"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366"
|
||||
dependencies = [
|
||||
"defmt-parser",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-parser"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "faccess"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||
|
||||
[[package]]
|
||||
name = "fd-find"
|
||||
version = "10.5.1"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"argmax",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"crossbeam-channel",
|
||||
"ctrlc",
|
||||
"diff",
|
||||
"etcetera",
|
||||
"faccess",
|
||||
"filetime",
|
||||
"globset",
|
||||
"ignore",
|
||||
"jiff",
|
||||
"libc",
|
||||
"lscolors",
|
||||
"memchr",
|
||||
"nix 0.31.3",
|
||||
"normpath",
|
||||
"nu-ansi-term",
|
||||
"regex",
|
||||
"regex-syntax",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"tikv-jemallocator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98b0d85522dc0a824cfebbc9594ff4a9b289c568bea0370319dfbdf20ccdd2d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "484be5657551d7868d67587df967fb4fcbe9d266a761cc855aaa148fda0b3cc7"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383"
|
||||
dependencies = [
|
||||
"defmt",
|
||||
"jiff-core",
|
||||
"jiff-static",
|
||||
"jiff-tzdb-platform",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-core"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c"
|
||||
dependencies = [
|
||||
"defmt",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c"
|
||||
dependencies = [
|
||||
"jiff-core",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-tzdb"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7"
|
||||
|
||||
[[package]]
|
||||
name = "jiff-tzdb-platform"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
|
||||
dependencies = [
|
||||
"jiff-tzdb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.191"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "lscolors"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d60e266dfb1426eb2d24792602e041131fdc0236bb7007abc0e589acafd60929"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"nu-ansi-term",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "normpath"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b11ce00d2594068e8a27c9146fdc5cf9f3ac38eb42c7cd34d05fea618873ac9f"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "test-case"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "124953e7f67cb0b2fcfb87e899e4ae5a64fb68e2e160767933cc67b646fb0042"
|
||||
dependencies = [
|
||||
"test-case-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "test-case-core"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd097615b407247e102046ba34426bb594923ab3554cf0f3cdfed050f1a5a3e8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "test-case-macros"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4ce35ece947bccac166e1ded639133827a953f69a48ba134d17f91108333082"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
"test-case-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemalloc-sys"
|
||||
version = "0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a2825c78386b4ae0314074867860ba9577875de945f05992c38815cbec327f0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemallocator"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "249f09e49ab1609436f34c776e84231bead18d6a955f119f939bdc1d847561bd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"tikv-jemalloc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
@@ -0,0 +1,183 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
rust-version = "1.90.0"
|
||||
name = "fd-find"
|
||||
version = "10.5.1"
|
||||
authors = ["David Peter <mail@david-peter.de>"]
|
||||
build = false
|
||||
exclude = ["/benchmarks/*"]
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "fd is a simple, fast and user-friendly alternative to find."
|
||||
homepage = "https://github.com/sharkdp/fd"
|
||||
documentation = "https://docs.rs/fd-find"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"search",
|
||||
"find",
|
||||
"file",
|
||||
"filesystem",
|
||||
"tool",
|
||||
]
|
||||
categories = ["command-line-utilities"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/sharkdp/fd"
|
||||
|
||||
[package.metadata.binstall]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/fd-v{ version }-{ target }.{ archive-format }"
|
||||
bin-dir = "{ bin }-v{ version }-{ target }/{ bin }{ binary-ext }"
|
||||
pkg-fmt = "tgz"
|
||||
|
||||
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
|
||||
pkg-fmt = "zip"
|
||||
|
||||
[package.metadata.binstall.overrides.x86_64-pc-windows-gnu]
|
||||
pkg-fmt = "zip"
|
||||
|
||||
[package.metadata.binstall.overrides.i686-pc-windows-msvc]
|
||||
pkg-fmt = "zip"
|
||||
|
||||
[package.metadata.binstall.overrides.aarch64-pc-windows-msvc]
|
||||
pkg-fmt = "zip"
|
||||
|
||||
[badges.appveyor]
|
||||
repository = "sharkdp/fd"
|
||||
|
||||
[badges.travis-ci]
|
||||
repository = "sharkdp/fd"
|
||||
|
||||
[features]
|
||||
base = ["use-jemalloc"]
|
||||
completions = ["clap_complete"]
|
||||
# WASI uses the standard allocator.
|
||||
default = []
|
||||
use-jemalloc = ["tikv-jemallocator"]
|
||||
|
||||
[[bin]]
|
||||
name = "fd"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[test]]
|
||||
name = "tests"
|
||||
path = "tests/tests.rs"
|
||||
|
||||
[dependencies.aho-corasick]
|
||||
version = "1.1"
|
||||
|
||||
[dependencies.anyhow]
|
||||
version = "1.0"
|
||||
|
||||
[dependencies.argmax]
|
||||
version = "0.4.0"
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.6.1"
|
||||
features = [
|
||||
"suggestions",
|
||||
"color",
|
||||
"wrap_help",
|
||||
"cargo",
|
||||
"derive",
|
||||
]
|
||||
|
||||
[dependencies.clap_complete]
|
||||
version = "4.6.5"
|
||||
optional = true
|
||||
|
||||
[dependencies.crossbeam-channel]
|
||||
version = "0.5.15"
|
||||
|
||||
[target.'cfg(not(target_os = "wasi"))'.dependencies.ctrlc]
|
||||
version = "3.5"
|
||||
|
||||
[dependencies.etcetera]
|
||||
version = "0.11"
|
||||
|
||||
[dependencies.faccess]
|
||||
version = "0.2.4"
|
||||
|
||||
[dependencies.globset]
|
||||
version = "0.4.19"
|
||||
|
||||
[dependencies.ignore]
|
||||
version = "0.4.28"
|
||||
|
||||
[dependencies.jiff]
|
||||
version = "0.2.27"
|
||||
|
||||
[dependencies.lscolors]
|
||||
version = "0.21"
|
||||
features = ["nu-ansi-term"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.memchr]
|
||||
version = "2.8.0"
|
||||
|
||||
[dependencies.normpath]
|
||||
version = "1.5.1"
|
||||
|
||||
[dependencies.nu-ansi-term]
|
||||
version = "0.50"
|
||||
|
||||
[dependencies.regex]
|
||||
version = "1.12.2"
|
||||
|
||||
[dependencies.regex-syntax]
|
||||
version = "0.8"
|
||||
|
||||
[dev-dependencies.diff]
|
||||
version = "0.1"
|
||||
|
||||
[dev-dependencies.filetime]
|
||||
version = "0.2"
|
||||
|
||||
[dev-dependencies.tempfile]
|
||||
version = "3.27"
|
||||
|
||||
[dev-dependencies.test-case]
|
||||
version = "3.3"
|
||||
|
||||
[target.'cfg(all(not(windows), not(target_os = "android"), not(target_os = "macos"), not(target_os = "freebsd"), not(target_os = "openbsd"), not(target_os = "illumos"), not(all(target_env = "musl", target_pointer_width = "32")), not(target_arch = "riscv64")))'.dependencies.tikv-jemallocator]
|
||||
version = "0.7.0"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies.libc]
|
||||
version = "0.2"
|
||||
|
||||
[target."cfg(unix)".dependencies.nix]
|
||||
version = "0.31.1"
|
||||
features = [
|
||||
"signal",
|
||||
"user",
|
||||
"hostname",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[profile.debugging]
|
||||
debug = 2
|
||||
inherits = "dev"
|
||||
|
||||
[profile.dev]
|
||||
debug = "line-tables-only"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
debug = 0
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
Generated
+116
@@ -0,0 +1,116 @@
|
||||
[package]
|
||||
authors = ["David Peter <mail@david-peter.de>"]
|
||||
categories = ["command-line-utilities"]
|
||||
description = "fd is a simple, fast and user-friendly alternative to find."
|
||||
exclude = ["/benchmarks/*"]
|
||||
homepage = "https://github.com/sharkdp/fd"
|
||||
documentation = "https://docs.rs/fd-find"
|
||||
keywords = [
|
||||
"search",
|
||||
"find",
|
||||
"file",
|
||||
"filesystem",
|
||||
"tool",
|
||||
]
|
||||
license = "MIT OR Apache-2.0"
|
||||
name = "fd-find"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/sharkdp/fd"
|
||||
version = "10.5.1"
|
||||
edition= "2024"
|
||||
rust-version = "1.90.0"
|
||||
|
||||
[badges.appveyor]
|
||||
repository = "sharkdp/fd"
|
||||
|
||||
[badges.travis-ci]
|
||||
repository = "sharkdp/fd"
|
||||
|
||||
[[bin]]
|
||||
name = "fd"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
aho-corasick = "1.1"
|
||||
nu-ansi-term = "0.50"
|
||||
argmax = "0.4.0"
|
||||
ignore = "0.4.28"
|
||||
regex = "1.12.2"
|
||||
regex-syntax = "0.8"
|
||||
ctrlc = "3.5"
|
||||
globset = "0.4.19"
|
||||
anyhow = "1.0"
|
||||
etcetera = "0.11"
|
||||
normpath = "1.5.1"
|
||||
crossbeam-channel = "0.5.15"
|
||||
clap_complete = {version = "4.6.5", optional = true}
|
||||
faccess = "0.2.4"
|
||||
jiff = "0.2.27"
|
||||
memchr = "2.8.0"
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.6.1"
|
||||
features = ["suggestions", "color", "wrap_help", "cargo", "derive"]
|
||||
|
||||
[dependencies.lscolors]
|
||||
version = "0.21"
|
||||
default-features = false
|
||||
features = ["nu-ansi-term"]
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = { version = "0.31.1", default-features = false, features = ["signal", "user", "hostname"] }
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
# FIXME: Re-enable jemalloc on macOS
|
||||
# jemalloc is currently disabled on macOS due to a bug in jemalloc in combination with macOS
|
||||
# Catalina. See https://github.com/sharkdp/fd/issues/498 for details.
|
||||
# This has to be kept in sync with src/main.rs where the allocator for
|
||||
# the program is set.
|
||||
[target.'cfg(all(not(windows), not(target_os = "android"), not(target_os = "macos"), not(target_os = "freebsd"), not(target_os = "openbsd"), not(target_os = "illumos"), not(all(target_env = "musl", target_pointer_width = "32")), not(target_arch = "riscv64")))'.dependencies]
|
||||
tikv-jemallocator = {version = "0.7.0", optional = true}
|
||||
|
||||
[dev-dependencies]
|
||||
diff = "0.1"
|
||||
tempfile = "3.27"
|
||||
filetime = "0.2"
|
||||
test-case = "3.3"
|
||||
|
||||
[profile.dev]
|
||||
debug = "line-tables-only"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
debug = false
|
||||
|
||||
[profile.debugging]
|
||||
inherits = "dev"
|
||||
debug = true
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
|
||||
[features]
|
||||
use-jemalloc = ["tikv-jemallocator"]
|
||||
completions = ["clap_complete"]
|
||||
base = ["use-jemalloc"]
|
||||
default = ["use-jemalloc", "completions"]
|
||||
|
||||
[package.metadata.binstall]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/fd-v{ version }-{ target }.{ archive-format }"
|
||||
bin-dir = "{ bin }-v{ version }-{ target }/{ bin }{ binary-ext }"
|
||||
pkg-fmt = "tgz"
|
||||
|
||||
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
|
||||
pkg-fmt = "zip"
|
||||
|
||||
[package.metadata.binstall.overrides.x86_64-pc-windows-gnu]
|
||||
pkg-fmt = "zip"
|
||||
|
||||
[package.metadata.binstall.overrides.i686-pc-windows-msvc]
|
||||
pkg-fmt = "zip"
|
||||
|
||||
[package.metadata.binstall.overrides.aarch64-pc-windows-msvc]
|
||||
pkg-fmt = "zip"
|
||||
@@ -0,0 +1,6 @@
|
||||
# https://github.com/sharkdp/fd/issues/1085
|
||||
[target.aarch64-unknown-linux-gnu.env]
|
||||
passthrough = ["JEMALLOC_SYS_WITH_LG_PAGE=16"]
|
||||
|
||||
[target.aarch64-unknown-linux-musl.env]
|
||||
passthrough = ["JEMALLOC_SYS_WITH_LG_PAGE=16"]
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017-2020 fd developers
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-present The fd developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,64 @@
|
||||
PROFILE?=release
|
||||
EXE=target/$(PROFILE)/fd
|
||||
prefix=/usr/local
|
||||
bindir=$(prefix)/bin
|
||||
datadir=$(prefix)/share
|
||||
exe_name=fd
|
||||
ifdef VERSION
|
||||
ARCHIVE_NAME = fd-v$(VERSION)
|
||||
else
|
||||
ARCHIVE_NAME = fd
|
||||
endif
|
||||
export ARCHIVE_NAME
|
||||
archive_path=package/$(ARCHIVE_NAME).tar.gz
|
||||
|
||||
$(EXE): Cargo.toml src/**/*.rs
|
||||
cargo build --profile $(PROFILE) --locked
|
||||
|
||||
.PHONY: completions
|
||||
completions: autocomplete/fd.bash autocomplete/fd.fish autocomplete/_fd.ps1 autocomplete/_fd autocomplete/_fdfind autocomplete/fdfind.bash autocomplete/fdfind.fish
|
||||
|
||||
comp_dir=@mkdir -p autocomplete
|
||||
|
||||
autocomplete/fd.bash: $(EXE)
|
||||
$(comp_dir)
|
||||
$(EXE) --gen-completions bash > $@
|
||||
|
||||
autocomplete/fd.fish: $(EXE)
|
||||
$(comp_dir)
|
||||
$(EXE) --gen-completions fish > $@
|
||||
|
||||
autocomplete/_fd.ps1: $(EXE)
|
||||
$(comp_dir)
|
||||
$(EXE) --gen-completions powershell > $@
|
||||
|
||||
autocomplete/_fd: contrib/completion/_fd
|
||||
$(comp_dir)
|
||||
cp $< $@
|
||||
|
||||
autocomplete/_fdfind: contrib/completion/_fdfind
|
||||
$(comp_dir)
|
||||
cp $< $@
|
||||
|
||||
autocomplete/fdfind.bash: contrib/completion/fdfind.bash
|
||||
$(comp_dir)
|
||||
cp $< $@
|
||||
|
||||
autocomplete/fdfind.fish: contrib/completion/fdfind.fish
|
||||
$(comp_dir)
|
||||
cp $< $@
|
||||
|
||||
archive: $(archive_path)
|
||||
|
||||
$(archive_path): completions $(EXE)
|
||||
bash scripts/create-archive.sh
|
||||
|
||||
install: $(EXE) completions
|
||||
install -Dm755 $(EXE) $(DESTDIR)$(bindir)/fd
|
||||
install -Dm644 autocomplete/fd.bash $(DESTDIR)/$(datadir)/bash-completion/completions/$(exe_name)
|
||||
install -Dm644 autocomplete/fd.fish $(DESTDIR)/$(datadir)/fish/vendor_completions.d/$(exe_name).fish
|
||||
install -Dm644 autocomplete/_fd $(DESTDIR)/$(datadir)/zsh/site-functions/_fd
|
||||
install -Dm644 autocomplete/fdfind.bash $(DESTDIR)/$(datadir)/zsh/site-functions/fdfind.bash
|
||||
install -Dm644 autocomplete/fdfind.fish $(DESTDIR)/$(datadir)/zsh/site-functions/fdfind.fish
|
||||
install -Dm644 autocomplete/_fdfind $(DESTDIR)/$(datadir)/zsh/site-functions/_fdfind
|
||||
install -Dm644 doc/fd.1 $(DESTDIR)/$(datadir)/man/man1/$(exe_name).1
|
||||
@@ -0,0 +1,15 @@
|
||||
# WASI patches
|
||||
|
||||
Source: crates.io `fd-find` 10.5.1, https://github.com/sharkdp/fd.
|
||||
The vendor copy retains the upstream licenses.
|
||||
|
||||
- `Cargo.toml` disables the default allocator and completion features.
|
||||
- `Cargo.toml` excludes ctrlc on WASI.
|
||||
- `src/main.rs` selects one thread on WASI and disables the unavailable filesystem-boundary option.
|
||||
- `src/filesystem.rs` uses the existing false-returning special-file predicates on WASI.
|
||||
- `src/walk.rs` uses a sequential walker and an unbounded result queue on WASI.
|
||||
- `src/wasi_walk.rs` adapts the existing visitor to the sequential ignore walker.
|
||||
- `src/main.rs` and `src/walk.rs` conditionally import native-only and completion-only items.
|
||||
|
||||
The WASI search path consumes results after the sequential walk.
|
||||
The optional upstream child-execution modes remain unsupported on WASI.
|
||||
@@ -0,0 +1,807 @@
|
||||
# fd
|
||||
|
||||
[](https://github.com/sharkdp/fd/actions/workflows/CICD.yml)
|
||||
[](https://crates.io/crates/fd-find)
|
||||
[[中文](https://github.com/cha0ran/fd-zh)]
|
||||
[[한국어](https://github.com/spearkkk/fd-kor)]
|
||||
|
||||
`fd` is a program to find entries in your filesystem.
|
||||
It is a simple, fast and user-friendly alternative to [`find`](https://www.gnu.org/software/findutils/).
|
||||
While it does not aim to support all of `find`'s powerful functionality, it provides sensible
|
||||
(opinionated) defaults for a majority of use cases.
|
||||
|
||||
[Installation](#installation) • [How to use](#how-to-use) • [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Features
|
||||
|
||||
* Intuitive syntax: `fd PATTERN` instead of `find -iname '*PATTERN*'`.
|
||||
* Regular expression (default) and glob-based patterns.
|
||||
* [Very fast](#benchmark) due to parallelized directory traversal.
|
||||
* Uses colors to highlight different file types (same as `ls`).
|
||||
* Supports [parallel command execution](#command-execution)
|
||||
* Smart case: the search is case-insensitive by default. It switches to
|
||||
case-sensitive if the pattern contains an uppercase
|
||||
character[\*](http://vimdoc.sourceforge.net/htmldoc/options.html#'smartcase').
|
||||
* Ignores hidden directories and files, by default.
|
||||
* Ignores patterns from your `.gitignore`, by default.
|
||||
* The command name is *50%* shorter[\*](https://github.com/ggreer/the_silver_searcher) than
|
||||
`find` :-).
|
||||
|
||||
## Demo
|
||||
|
||||

|
||||
|
||||
## How to use
|
||||
|
||||
First, to get an overview of all available command line options, you can either run
|
||||
[`fd -h`](#command-line-options) for a concise help message or `fd --help` for a more detailed
|
||||
version.
|
||||
|
||||
### Simple search
|
||||
|
||||
*fd* is designed to find entries in your filesystem. The most basic search you can perform is to
|
||||
run *fd* with a single argument: the search pattern. For example, assume that you want to find an
|
||||
old script of yours (the name included `netflix`):
|
||||
``` bash
|
||||
> fd netfl
|
||||
Software/python/imdb-ratings/netflix-details.py
|
||||
```
|
||||
If called with just a single argument like this, *fd* searches the current directory recursively
|
||||
for any entries that *contain* the pattern `netfl`.
|
||||
|
||||
### Regular expression search
|
||||
|
||||
The search pattern is treated as a regular expression. Here, we search for entries that start
|
||||
with `x` and end with `rc`:
|
||||
``` bash
|
||||
> cd /etc
|
||||
> fd '^x.*rc$'
|
||||
X11/xinit/xinitrc
|
||||
X11/xinit/xserverrc
|
||||
```
|
||||
|
||||
The regular expression syntax used by `fd` is [documented here](https://docs.rs/regex/latest/regex/#syntax).
|
||||
|
||||
### Specifying the root directory
|
||||
|
||||
If we want to search a specific directory, it can be given as a second argument to *fd*:
|
||||
``` bash
|
||||
> fd passwd /etc
|
||||
/etc/default/passwd
|
||||
/etc/pam.d/passwd
|
||||
/etc/passwd
|
||||
```
|
||||
|
||||
### List all files, recursively
|
||||
|
||||
*fd* can be called with no arguments. This is very useful to get a quick overview of all entries
|
||||
in the current directory, recursively (similar to `ls -R`):
|
||||
``` bash
|
||||
> cd fd/tests
|
||||
> fd
|
||||
testenv
|
||||
testenv/mod.rs
|
||||
tests.rs
|
||||
```
|
||||
|
||||
If you want to use this functionality to list all files in a given directory, you have to use
|
||||
a catch-all pattern such as `.` or `^`:
|
||||
``` bash
|
||||
> fd . fd/tests/
|
||||
testenv
|
||||
testenv/mod.rs
|
||||
tests.rs
|
||||
```
|
||||
|
||||
### Searching for a particular file extension
|
||||
|
||||
Often, we are interested in all files of a particular type. This can be done with the `-e` (or
|
||||
`--extension`) option. Here, we search for all Markdown files in the fd repository:
|
||||
``` bash
|
||||
> cd fd
|
||||
> fd -e md
|
||||
CONTRIBUTING.md
|
||||
README.md
|
||||
```
|
||||
|
||||
The `-e` option can be used in combination with a search pattern:
|
||||
``` bash
|
||||
> fd -e rs mod
|
||||
src/fshelper/mod.rs
|
||||
src/lscolors/mod.rs
|
||||
tests/testenv/mod.rs
|
||||
```
|
||||
|
||||
### Searching for a particular file name
|
||||
|
||||
To find files with exactly the provided search pattern, use the `-g` (or `--glob`) option:
|
||||
``` bash
|
||||
> fd -g libc.so /usr
|
||||
/usr/lib32/libc.so
|
||||
/usr/lib/libc.so
|
||||
```
|
||||
|
||||
### Hidden and ignored files
|
||||
By default, *fd* does not search hidden directories and does not show hidden files in the
|
||||
search results. To disable this behavior, we can use the `-H` (or `--hidden`) option:
|
||||
``` bash
|
||||
> fd pre-commit
|
||||
> fd -H pre-commit
|
||||
.git/hooks/pre-commit.sample
|
||||
```
|
||||
|
||||
If we work in a directory that is a Git repository (or includes Git repositories), *fd* does not
|
||||
search folders (and does not show files) that match one of the `.gitignore` patterns. To disable
|
||||
this behavior, we can use the `-I` (or `--no-ignore`) option:
|
||||
``` bash
|
||||
> fd num_cpu
|
||||
> fd -I num_cpu
|
||||
target/debug/deps/libnum_cpus-f5ce7ef99006aa05.rlib
|
||||
```
|
||||
|
||||
To really search *all* files and directories, simply combine the hidden and ignore features to show
|
||||
everything (`-HI`) or use `-u`/`--unrestricted`.
|
||||
|
||||
### Matching the full path
|
||||
By default, *fd* only matches the filename of each file. However, using the `--full-path` or `-p` option,
|
||||
you can match against the full path.
|
||||
|
||||
```bash
|
||||
> fd -u -p -g '**/.git/config'
|
||||
> fd -p '.*/lesson-\d+/[a-z]+.(jpg|png)'
|
||||
```
|
||||
|
||||
### Command execution
|
||||
|
||||
Instead of just showing the search results, you often want to *do something* with them. `fd`
|
||||
provides two ways to execute external commands for each of your search results:
|
||||
|
||||
* The `-x`/`--exec` option runs an external command *for each of the search results* (in parallel).
|
||||
* The `-X`/`--exec-batch` option launches the external command once, with *all search results as arguments*.
|
||||
|
||||
#### Examples
|
||||
|
||||
Recursively find all zip archives and unpack them:
|
||||
``` bash
|
||||
fd -e zip -x unzip
|
||||
```
|
||||
If there are two such files, `file1.zip` and `backup/file2.zip`, this would execute
|
||||
`unzip file1.zip` and `unzip backup/file2.zip`. The two `unzip` processes run in parallel
|
||||
(if the files are found fast enough).
|
||||
|
||||
Find all `*.h` and `*.cpp` files and auto-format them inplace with `clang-format -i`:
|
||||
``` bash
|
||||
fd -e h -e cpp -x clang-format -i
|
||||
```
|
||||
Note how the `-i` option to `clang-format` can be passed as a separate argument. This is why
|
||||
we put the `-x` option last.
|
||||
|
||||
Any positional arguments after `-x` belong to the command template, not to `fd` itself. If you
|
||||
also want to pass a pattern or search path, put `-x` last:
|
||||
``` bash
|
||||
fd pattern path -x echo
|
||||
```
|
||||
|
||||
Find all `test_*.py` files and open them in your favorite editor:
|
||||
``` bash
|
||||
fd -g 'test_*.py' -X vim
|
||||
```
|
||||
Note that we use capital `-X` here to open a single `vim` instance. If there are two such files,
|
||||
`test_basic.py` and `lib/test_advanced.py`, this will run `vim test_basic.py lib/test_advanced.py`.
|
||||
|
||||
To see details like file permissions, owners, file sizes etc., you can tell `fd` to show them
|
||||
by running `ls` for each result:
|
||||
``` bash
|
||||
fd … -X ls -lhd --color=always
|
||||
```
|
||||
This pattern is so useful that `fd` provides a shortcut. You can use the `-l`/`--list-details`
|
||||
option to execute `ls` in this way: `fd … -l`.
|
||||
|
||||
The `-X` option is also useful when combining `fd` with [ripgrep](https://github.com/BurntSushi/ripgrep/) (`rg`) in order to search within a certain class of files, like all C++ source files:
|
||||
```bash
|
||||
fd -e cpp -e cxx -e h -e hpp -X rg 'std::cout'
|
||||
```
|
||||
|
||||
Convert all `*.jpg` files to `*.png` files:
|
||||
``` bash
|
||||
fd -e jpg -x convert {} {.}.png
|
||||
```
|
||||
Here, `{}` is a placeholder for the search result. `{.}` is the same, without the file extension.
|
||||
See below for more details on the placeholder syntax.
|
||||
|
||||
The terminal output of commands run from parallel threads using `-x` will not be interlaced or garbled,
|
||||
so `fd -x` can be used to rudimentarily parallelize a task run over many files.
|
||||
An example of this is calculating the checksum of each individual file within a directory.
|
||||
``` bash
|
||||
fd -tf -x md5sum > file_checksums.txt
|
||||
```
|
||||
|
||||
#### Placeholder syntax
|
||||
|
||||
The `-x` and `-X` options take a *command template* as a series of arguments (instead of a single string).
|
||||
If you want to add additional options to `fd` after the command template, you can terminate it with a `\;`.
|
||||
|
||||
For example, `fd -x echo \; pattern path` treats `pattern path` as `fd` arguments instead of
|
||||
passing them to `echo`. In practice, it is often clearer to write `fd pattern path -x echo`.
|
||||
|
||||
The syntax for generating commands is similar to that of [GNU Parallel](https://www.gnu.org/software/parallel/):
|
||||
|
||||
- `{}`: A placeholder token that will be replaced with the path of the search result
|
||||
(`documents/images/party.jpg`).
|
||||
- `{.}`: Like `{}`, but without the file extension (`documents/images/party`).
|
||||
- `{/}`: A placeholder that will be replaced by the basename of the search result (`party.jpg`).
|
||||
- `{//}`: The parent of the discovered path (`documents/images`).
|
||||
- `{/.}`: The basename, with the extension removed (`party`).
|
||||
|
||||
If you do not include a placeholder, *fd* automatically adds a `{}` at the end.
|
||||
|
||||
#### Parallel vs. serial execution
|
||||
|
||||
For `-x`/`--exec`, you can control the number of parallel jobs by using the `-j`/`--threads` option.
|
||||
Use `--threads=1` for serial execution.
|
||||
|
||||
### Excluding specific files or directories
|
||||
|
||||
Sometimes we want to ignore search results from a specific subdirectory. For example, we might
|
||||
want to search all hidden files and directories (`-H`) but exclude all matches from `.git`
|
||||
directories. We can use the `-E` (or `--exclude`) option for this. It takes an arbitrary glob
|
||||
pattern as an argument:
|
||||
``` bash
|
||||
> fd -H -E .git …
|
||||
```
|
||||
|
||||
We can also use this to skip mounted directories:
|
||||
``` bash
|
||||
> fd -E /mnt/external-drive …
|
||||
```
|
||||
|
||||
.. or to skip certain file types:
|
||||
``` bash
|
||||
> fd -E '*.bak' …
|
||||
```
|
||||
|
||||
To make exclude-patterns like these permanent, you can create a `.fdignore` file. They work like
|
||||
`.gitignore` files, but are specific to `fd`. For example:
|
||||
``` bash
|
||||
> cat ~/.fdignore
|
||||
/mnt/external-drive
|
||||
*.bak
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> `fd` also supports `.ignore` files that are used by other programs such as `rg` or `ag`.
|
||||
|
||||
If you want `fd` to ignore these patterns globally, you can put them in `fd`'s global ignore file.
|
||||
This is usually located in `~/.config/fd/ignore` in macOS or Linux, and `%APPDATA%\fd\ignore` in
|
||||
Windows.
|
||||
|
||||
You may wish to include `.git/` in your `fd/ignore` file so that `.git` directories, and their contents
|
||||
are not included in output if you use the `--hidden` option.
|
||||
|
||||
### Deleting files
|
||||
|
||||
You can use `fd` to remove all files and directories that are matched by your search pattern.
|
||||
If you only want to remove files, you can use the `--exec-batch`/`-X` option to call `rm`. For
|
||||
example, to recursively remove all `.DS_Store` files, run:
|
||||
``` bash
|
||||
> fd -H '^\.DS_Store$' -tf -X rm
|
||||
```
|
||||
If you are unsure, always call `fd` without `-X rm` first. Alternatively, use `rm`s "interactive"
|
||||
option:
|
||||
``` bash
|
||||
> fd -H '^\.DS_Store$' -tf -X rm -i
|
||||
```
|
||||
|
||||
If you also want to remove a certain class of directories, you can use the same technique. You will
|
||||
have to use `rm`s `--recursive`/`-r` flag to remove directories.
|
||||
|
||||
> [!NOTE]
|
||||
> There are scenarios where using `fd … -X rm -r` can cause race conditions: if you have a
|
||||
path like `…/foo/bar/foo/…` and want to remove all directories named `foo`, you can end up in a
|
||||
situation where the outer `foo` directory is removed first, leading to (harmless) *"'foo/bar/foo':
|
||||
No such file or directory"* errors in the `rm` call.
|
||||
|
||||
### Command-line options
|
||||
|
||||
This is the output of `fd -h`. To see the full set of command-line options, use `fd --help` which
|
||||
also includes a much more detailed help text.
|
||||
|
||||
```
|
||||
Usage: fd [OPTIONS] [pattern] [path]...
|
||||
|
||||
Arguments:
|
||||
[pattern] the search pattern (a regular expression, unless '--glob' is used; optional)
|
||||
[path]... the root directories for the filesystem search (optional, requires pattern first)
|
||||
|
||||
Options:
|
||||
-H, --hidden Search hidden files and directories
|
||||
-I, --no-ignore Do not respect .(git|fd)ignore files
|
||||
-s, --case-sensitive Case-sensitive search (default: smart case)
|
||||
-i, --ignore-case Case-insensitive search (default: smart case)
|
||||
-g, --glob Glob-based search (default: regular expression)
|
||||
-a, --absolute-path Show absolute instead of relative paths
|
||||
-l, --list-details Use a long listing format with file metadata
|
||||
-L, --follow Follow symbolic links
|
||||
-p, --full-path Search full abs. path (default: filename only)
|
||||
-d, --max-depth <depth> Set maximum search depth (default: none)
|
||||
-E, --exclude <glob> Exclude entries that match the given glob pattern
|
||||
-t, --type <filetype> Filter by type: file (f), directory (d/dir), symlink (l),
|
||||
executable (x), empty (e), socket (s), pipe (p), char-device
|
||||
(c), block-device (b)
|
||||
-e, --extension <ext> Filter by extension
|
||||
-S, --size <size> Limit results based on the size of files
|
||||
--changed-within <date|dur> Filter by file modification time (newer than)
|
||||
--changed-before <date|dur> Filter by file modification time (older than)
|
||||
-o, --owner <user:group> Filter by owning user and/or group
|
||||
--format <fmt> Print results according to template
|
||||
-x, --exec <cmd>... Execute a command for each search result
|
||||
-X, --exec-batch <cmd>... Execute a command with all search results at once
|
||||
-c, --color <when> When to use colors [default: auto] [possible values: auto,
|
||||
always, never]
|
||||
--hyperlink[=<when>] Add hyperlinks to output paths [default: never] [possible
|
||||
values: auto, always, never]
|
||||
--ignore-contain <name> Ignore directories containing the named entry
|
||||
-h, --help Print help (see more with '--help')
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
Note that options can be given after the pattern and/or path as well.
|
||||
|
||||
## Benchmark
|
||||
|
||||
Let's search my home folder for files that end in `[0-9].jpg`. It contains ~750,000
|
||||
subdirectories and about a 4 million files. For averaging and statistical analysis, I'm using
|
||||
[hyperfine](https://github.com/sharkdp/hyperfine). The following benchmarks are performed
|
||||
with a "warm"/pre-filled disk-cache (results for a "cold" disk-cache show the same trends).
|
||||
|
||||
Let's start with `find`:
|
||||
```
|
||||
Benchmark 1: find ~ -iregex '.*[0-9]\.jpg$'
|
||||
Time (mean ± σ): 19.922 s ± 0.109 s
|
||||
Range (min … max): 19.765 s … 20.065 s
|
||||
```
|
||||
|
||||
`find` is much faster if it does not need to perform a regular-expression search:
|
||||
```
|
||||
Benchmark 2: find ~ -iname '*[0-9].jpg'
|
||||
Time (mean ± σ): 11.226 s ± 0.104 s
|
||||
Range (min … max): 11.119 s … 11.466 s
|
||||
```
|
||||
|
||||
Now let's try the same for `fd`. Note that `fd` performs a regular expression
|
||||
search by default. The options `-u`/`--unrestricted` option is needed here for
|
||||
a fair comparison. Otherwise `fd` does not have to traverse hidden folders and
|
||||
ignored paths (see below):
|
||||
```
|
||||
Benchmark 3: fd -u '[0-9]\.jpg$' ~
|
||||
Time (mean ± σ): 854.8 ms ± 10.0 ms
|
||||
Range (min … max): 839.2 ms … 868.9 ms
|
||||
```
|
||||
For this particular example, `fd` is approximately **23 times faster** than `find -iregex`
|
||||
and about **13 times faster** than `find -iname`. By the way, both tools found the exact
|
||||
same 546 files :smile:.
|
||||
|
||||
**Note**: This is *one particular* benchmark on *one particular* machine. While we have
|
||||
performed a lot of different tests (and found consistent results), things might
|
||||
be different for you! We encourage everyone to try it out on their own. See
|
||||
[this repository](https://github.com/sharkdp/fd-benchmarks) for all necessary scripts.
|
||||
|
||||
Concerning *fd*'s speed, a lot of credit goes to the `regex` and `ignore` crates that are
|
||||
also used in [ripgrep](https://github.com/BurntSushi/ripgrep) (check it out!).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `fd` does not find my file!
|
||||
|
||||
Remember that `fd` ignores hidden directories and files by default. It also ignores patterns
|
||||
from `.gitignore` files. If you want to make sure to find absolutely every possible file, always
|
||||
use the options `-u`/`--unrestricted` option (or `-HI` to enable hidden and ignored files):
|
||||
``` bash
|
||||
> fd -u …
|
||||
```
|
||||
|
||||
Also remember that by default, `fd` only searches based on the filename and
|
||||
doesn't compare the pattern to the full path. If you want to search based on the
|
||||
full path (similar to the `-path` option of `find`) you need to use the `--full-path`
|
||||
(or `-p`) option.
|
||||
|
||||
### Colorized output
|
||||
|
||||
`fd` can colorize files by extension, just like `ls`. In order for this to work, the environment
|
||||
variable [`LS_COLORS`](https://linux.die.net/man/5/dir_colors) has to be set. Typically, the value
|
||||
of this variable is set by the `dircolors` command which provides a convenient configuration format
|
||||
to define colors for different file formats.
|
||||
On most distributions, `LS_COLORS` should be set already. If you are on Windows or if you are looking
|
||||
for alternative, more complete (or more colorful) variants, see [here](https://github.com/sharkdp/vivid),
|
||||
[here](https://github.com/seebi/dircolors-solarized) or
|
||||
[here](https://github.com/trapd00r/LS_COLORS).
|
||||
|
||||
`fd` also honors the [`NO_COLOR`](https://no-color.org/) environment variable.
|
||||
|
||||
### `fd` doesn't seem to interpret my regex pattern correctly
|
||||
|
||||
A lot of special regex characters (like `[]`, `^`, `$`, ..) are also special characters in your
|
||||
shell. If in doubt, always make sure to put single quotes around the regex pattern:
|
||||
|
||||
``` bash
|
||||
> fd '^[A-Z][0-9]+$'
|
||||
```
|
||||
|
||||
If your pattern starts with a dash, you have to add `--` to signal the end of command line
|
||||
options. Otherwise, the pattern will be interpreted as a command-line option. Alternatively,
|
||||
use a character class with a single hyphen character:
|
||||
|
||||
``` bash
|
||||
> fd -- '-pattern'
|
||||
> fd '[-]pattern'
|
||||
```
|
||||
|
||||
### "Command not found" for `alias`es or shell functions
|
||||
|
||||
Shell `alias`es and shell functions can not be used for command execution via `fd -x` or
|
||||
`fd -X`. In `zsh`, you can make the alias global via `alias -g myalias="…"`. In `bash`,
|
||||
you can use `export -f my_function` to make available to child processes. You would still
|
||||
need to call `fd -x bash -c 'my_function "$1"' bash`. For other use cases or shells, use
|
||||
a (temporary) shell script.
|
||||
|
||||
### Placeholders in `-x`/`-X`
|
||||
|
||||
Depending on your shell, you may need to quote the placeholders (`{}`, `{/}`, `{//}`,
|
||||
`{.}`, `{/.}`) to prevent the shell from interpreting them before `fd` sees them.
|
||||
|
||||
### `fd` crashes when another memory allocator is preloaded
|
||||
|
||||
The released Linux binaries use jemalloc, through the `use-jemalloc` feature, which is enabled by default for
|
||||
some targets. Preloading a different allocator while jemalloc is compiled in is not supported, for example
|
||||
|
||||
``` bash
|
||||
> LD_PRELOAD=/usr/bin/mimalloc fd
|
||||
```
|
||||
|
||||
makes the two disagree about which of them owns an allocation, and `fd` crashes. `fd` cannot detect
|
||||
this and report it, because the preloaded library is already answering its first allocation. To run
|
||||
with a preloaded allocator, build without jemalloc:
|
||||
|
||||
``` bash
|
||||
> cargo build --release --no-default-features
|
||||
```
|
||||
|
||||
## Integration with other programs
|
||||
|
||||
### Using fd with `fzf`
|
||||
|
||||
You can use *fd* to generate input for the command-line fuzzy finder [fzf](https://github.com/junegunn/fzf):
|
||||
``` bash
|
||||
export FZF_DEFAULT_COMMAND='fd --type file'
|
||||
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
|
||||
```
|
||||
|
||||
Then, you can type `vim <Ctrl-T>` on your terminal to open fzf and search through the fd-results.
|
||||
|
||||
Alternatively, you might like to follow symbolic links and include hidden files (but exclude `.git` folders):
|
||||
``` bash
|
||||
export FZF_DEFAULT_COMMAND='fd --type file --follow --hidden --exclude .git'
|
||||
```
|
||||
|
||||
You can even use fd's colored output inside fzf by setting:
|
||||
``` bash
|
||||
export FZF_DEFAULT_COMMAND="fd --type file --color=always"
|
||||
export FZF_DEFAULT_OPTS="--ansi"
|
||||
```
|
||||
|
||||
For more details, see the [Tips section](https://github.com/junegunn/fzf#tips) of the fzf README.
|
||||
|
||||
### Using fd with `rofi`
|
||||
|
||||
[*rofi*](https://github.com/davatorium/rofi) is a graphical launch menu application that is able to create menus by reading from *stdin*. Piping `fd` output into `rofi`s `-dmenu` mode creates fuzzy-searchable lists of files and directories.
|
||||
|
||||
#### Example
|
||||
|
||||
Create a case-insensitive searchable multi-select list of *PDF* files under your `$HOME` directory and open the selection with your configured PDF viewer. To list all file types, drop the `-e pdf` argument.
|
||||
|
||||
``` bash
|
||||
fd --type f -e pdf . $HOME | rofi -keep-right -dmenu -i -p FILES -multi-select | xargs -I {} xdg-open {}
|
||||
```
|
||||
|
||||
To modify the list that is presented by rofi, add arguments to the `fd` command. To modify the search behaviour of rofi, add arguments to the `rofi` command.
|
||||
|
||||
### Using fd with `emacs`
|
||||
|
||||
The emacs package [find-file-in-project](https://github.com/technomancy/find-file-in-project) can
|
||||
use *fd* to find files.
|
||||
|
||||
After installing `find-file-in-project`, add the line `(setq ffip-use-rust-fd t)` to your
|
||||
`~/.emacs` or `~/.emacs.d/init.el` file.
|
||||
|
||||
In emacs, run `M-x find-file-in-project-by-selected` to find matching files. Alternatively, run
|
||||
`M-x find-file-in-project` to list all available files in the project.
|
||||
|
||||
### Printing the output as a tree
|
||||
|
||||
To format the output of `fd` as a file-tree you can use the `tree` command with
|
||||
`--fromfile`:
|
||||
```bash
|
||||
❯ fd | tree --fromfile
|
||||
```
|
||||
|
||||
This can be more useful than running `tree` by itself because `tree` does not
|
||||
ignore any files by default, nor does it support as rich a set of options as
|
||||
`fd` does to control what to print:
|
||||
```bash
|
||||
❯ fd --extension rs | tree --fromfile
|
||||
.
|
||||
├── build.rs
|
||||
└── src
|
||||
├── app.rs
|
||||
└── error.rs
|
||||
```
|
||||
|
||||
On bash and similar you can simply create an alias:
|
||||
```bash
|
||||
❯ alias as-tree='tree --fromfile'
|
||||
```
|
||||
|
||||
### Using fd with `xargs` or `parallel`
|
||||
|
||||
Note that `fd` has a builtin feature for [command execution](#command-execution) with
|
||||
its `-x`/`--exec` and `-X`/`--exec-batch` options. If you prefer, you can still use
|
||||
it in combination with `xargs`:
|
||||
``` bash
|
||||
> fd -0 -e rs | xargs -0 wc -l
|
||||
```
|
||||
Here, the `-0` option tells *fd* to separate search results by the NULL character (instead of
|
||||
newlines). In the same way, the `-0` option of `xargs` tells it to read the input in this way.
|
||||
|
||||
## Installation
|
||||
|
||||
[](https://repology.org/project/fd-find/versions)
|
||||
|
||||
### On Ubuntu
|
||||
*... and other Debian-based Linux distributions.*
|
||||
|
||||
If you run Ubuntu 19.04 (Disco Dingo) or newer, you can install the
|
||||
[officially maintained package](https://packages.ubuntu.com/fd-find):
|
||||
```
|
||||
apt install fd-find
|
||||
```
|
||||
Note that the binary is called `fdfind` as the binary name `fd` is already used by another package.
|
||||
It is recommended that after installation, you add a link to `fd` by executing command
|
||||
`ln -s $(which fdfind) ~/.local/bin/fd`, in order to use `fd` in the same way as in this documentation.
|
||||
Make sure that `$HOME/.local/bin` is in your `$PATH`.
|
||||
|
||||
If you use an older version of Ubuntu, you can download the latest `.deb` package from the
|
||||
[release page](https://github.com/sharkdp/fd/releases) and install it via:
|
||||
``` bash
|
||||
dpkg -i fd_9.0.0_amd64.deb # adapt version number and architecture
|
||||
```
|
||||
|
||||
Note that the .deb packages on the release page for this project still name the executable `fd`.
|
||||
|
||||
### On Debian
|
||||
|
||||
If you run Debian Buster or newer, you can install the
|
||||
[officially maintained Debian package](https://tracker.debian.org/pkg/rust-fd-find):
|
||||
```
|
||||
apt-get install fd-find
|
||||
```
|
||||
Note that the binary is called `fdfind` as the binary name `fd` is already used by another package.
|
||||
It is recommended that after installation, you add a link to `fd` by executing command
|
||||
`ln -s $(which fdfind) ~/.local/bin/fd`, in order to use `fd` in the same way as in this documentation.
|
||||
Make sure that `$HOME/.local/bin` is in your `$PATH`.
|
||||
|
||||
Note that the .deb packages on the release page for this project still name the executable `fd`.
|
||||
|
||||
### On Fedora
|
||||
|
||||
Starting with Fedora 28, you can install `fd` from the official package sources:
|
||||
``` bash
|
||||
dnf install fd-find
|
||||
```
|
||||
|
||||
### On Alpine Linux
|
||||
|
||||
You can install [the fd package](https://pkgs.alpinelinux.org/packages?name=fd)
|
||||
from the official sources, provided you have the appropriate repository enabled:
|
||||
```
|
||||
apk add fd
|
||||
```
|
||||
|
||||
### On Arch Linux
|
||||
|
||||
You can install [the fd package](https://www.archlinux.org/packages/extra/x86_64/fd/) from the official repos:
|
||||
```
|
||||
pacman -S fd
|
||||
```
|
||||
You can also install fd [from the AUR](https://aur.archlinux.org/packages/fd-git).
|
||||
|
||||
### On Gentoo Linux
|
||||
|
||||
You can use [the fd ebuild](https://packages.gentoo.org/packages/sys-apps/fd) from the official repo:
|
||||
```
|
||||
emerge -av fd
|
||||
```
|
||||
|
||||
### On openSUSE Linux
|
||||
|
||||
You can install [the fd package](https://software.opensuse.org/package/fd) from the official repo:
|
||||
```
|
||||
zypper in fd
|
||||
```
|
||||
|
||||
### On Void Linux
|
||||
|
||||
You can install `fd` via xbps-install:
|
||||
```
|
||||
xbps-install -S fd
|
||||
```
|
||||
|
||||
### On ALT Linux
|
||||
|
||||
You can install [the fd package](https://packages.altlinux.org/en/sisyphus/srpms/fd/) from the official repo:
|
||||
```
|
||||
apt-get install fd
|
||||
```
|
||||
|
||||
### On Solus
|
||||
|
||||
You can install [the fd package](https://github.com/getsolus/packages/tree/main/packages/f/fd) from the official repo:
|
||||
```
|
||||
eopkg install fd
|
||||
```
|
||||
|
||||
### On RedHat Enterprise Linux (RHEL) 8/9/10, Almalinux 8/9/10, EuroLinux 8/9 or Rocky Linux 8/9/10
|
||||
|
||||
You can install [the `fd` package](https://copr.fedorainfracloud.org/coprs/tkbcopr/fd/) from Fedora Copr.
|
||||
|
||||
```bash
|
||||
dnf copr enable tkbcopr/fd
|
||||
dnf install fd
|
||||
```
|
||||
|
||||
A different version using the [slower](https://github.com/sharkdp/fd/pull/481#issuecomment-534494592) malloc [instead of jemalloc](https://bugzilla.redhat.com/show_bug.cgi?id=2216193#c1) is also available from the EPEL8/9 repo as the package `fd-find`.
|
||||
|
||||
### On macOS
|
||||
|
||||
You can install `fd` with [Homebrew](https://formulae.brew.sh/formula/fd):
|
||||
```
|
||||
brew install fd
|
||||
```
|
||||
|
||||
… or with MacPorts:
|
||||
```
|
||||
port install fd
|
||||
```
|
||||
|
||||
### On Windows
|
||||
|
||||
You can download pre-built binaries from the [release page](https://github.com/sharkdp/fd/releases).
|
||||
|
||||
Alternatively, you can install `fd` via [Scoop](http://scoop.sh):
|
||||
```
|
||||
scoop install fd
|
||||
```
|
||||
|
||||
Or via [Chocolatey](https://chocolatey.org):
|
||||
```
|
||||
choco install fd
|
||||
```
|
||||
|
||||
Or via [Winget](https://learn.microsoft.com/en-us/windows/package-manager/):
|
||||
```
|
||||
winget install sharkdp.fd
|
||||
```
|
||||
|
||||
### On GuixOS
|
||||
|
||||
You can install [the fd package](https://guix.gnu.org/en/packages/fd-8.1.1/) from the official repo:
|
||||
```
|
||||
guix install fd
|
||||
```
|
||||
|
||||
### On Mise
|
||||
|
||||
You can use [mise](https://github.com/jdx/mise) to install `fd` with a command like this:
|
||||
```
|
||||
mise use -g fd@latest
|
||||
```
|
||||
|
||||
### On NixOS / via Nix
|
||||
|
||||
You can use the [Nix package manager](https://nixos.org/nix/) to install `fd`:
|
||||
```
|
||||
nix-env -i fd
|
||||
```
|
||||
|
||||
### Via Flox
|
||||
|
||||
You can use [Flox](https://flox.dev) to install `fd` into a Flox environment:
|
||||
```
|
||||
flox install fd
|
||||
```
|
||||
|
||||
### On FreeBSD
|
||||
|
||||
You can install [the fd-find package](https://www.freshports.org/sysutils/fd) from the official repo:
|
||||
```
|
||||
pkg install fd-find
|
||||
```
|
||||
|
||||
### From npm
|
||||
|
||||
On Linux and macOS, you can install the [fd-find](https://npm.im/fd-find) package:
|
||||
|
||||
```
|
||||
npm install -g fd-find
|
||||
```
|
||||
|
||||
### From source
|
||||
|
||||
With Rust's package manager [cargo](https://github.com/rust-lang/cargo), you can install *fd* via:
|
||||
```
|
||||
cargo install fd-find
|
||||
```
|
||||
Note that rust version *1.90.0* or later is required.
|
||||
|
||||
`make` is also needed for the build.
|
||||
|
||||
### From binaries
|
||||
|
||||
The [release page](https://github.com/sharkdp/fd/releases) includes precompiled binaries for Linux, macOS and Windows. Statically-linked binaries are also available: look for archives with `musl` in the file name.
|
||||
|
||||
## Development
|
||||
```bash
|
||||
git clone https://github.com/sharkdp/fd
|
||||
|
||||
# Build
|
||||
cd fd
|
||||
cargo build
|
||||
|
||||
# Run unit tests and integration tests
|
||||
cargo test
|
||||
|
||||
# Install
|
||||
cargo install --path .
|
||||
```
|
||||
|
||||
### Completions
|
||||
|
||||
#### From Release Archives
|
||||
|
||||
Pre-built completion files are included in the release archives (`.tar.gz`/`.zip`) on the
|
||||
[Releases page](https://github.com/sharkdp/fd/releases), in the `autocomplete` directory.
|
||||
To use these completions:
|
||||
|
||||
- **bash**: Source the `fd.bash` file in your `~/.bashrc`, or place it in a directory that gets sourced automatically.
|
||||
- **zsh**: Move `_fd` to a directory in your `fpath` (e.g., `~/.zfunc`).
|
||||
- **fish**: Copy `fd.fish` to `~/.config/fish/completions/`.
|
||||
- **powershell**: Source `_fd.ps1` from one of your [profile scripts](https://learn.microsoft.com/en-us/powershell/scripting/learn/shell/creating-profiles?view=powershell-7.5).
|
||||
|
||||
#### Generate from fd
|
||||
|
||||
You can also generate completions directly using `fd --gen-completions <shell>`:
|
||||
|
||||
```bash
|
||||
# Bash
|
||||
fd --gen-completions bash > ~/.local/share/bash-completion/completions/fd
|
||||
|
||||
# Zsh (ensure ~/.zfunc is in your fpath)
|
||||
fd --gen-completions zsh > ~/.zfunc/_fd
|
||||
|
||||
# Fish
|
||||
fd --gen-completions fish > ~/.config/fish/completions/fd.fish
|
||||
|
||||
# PowerShell
|
||||
fd --gen-completions powershell >> $PROFILE
|
||||
```
|
||||
|
||||
## Maintainers
|
||||
|
||||
- [sharkdp](https://github.com/sharkdp)
|
||||
- [tmccombs](https://github.com/tmccombs)
|
||||
- [tavianator](https://github.com/tavianator)
|
||||
|
||||
## License
|
||||
|
||||
`fd` is distributed under the terms of both the MIT License and the Apache License 2.0.
|
||||
|
||||
See the [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) files for license details.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Security Reporting
|
||||
|
||||
If you wish to report a security vulnerability privately, we appreciate your diligence. Please follow the guidelines below to submit your report.
|
||||
|
||||
## Reporting
|
||||
|
||||
To report a security vulnerability, please provide the following information:
|
||||
|
||||
1. **PROJECT**
|
||||
- Include the URL of the project repository - Example: <https://github.com/sharkdp/fd>
|
||||
|
||||
2. **PUBLIC**
|
||||
- Indicate whether this vulnerability has already been publicly discussed or disclosed.
|
||||
- If so, provide relevant links.
|
||||
|
||||
3. **DESCRIPTION**
|
||||
- Provide a detailed description of the security vulnerability.
|
||||
- Include as much information as possible to help us understand and address the issue.
|
||||
|
||||
Send this information, along with any additional relevant details, to our [vulnerability reporting form](https://github.com/sharkdp/fd/security/advisories/new).
|
||||
|
||||
## Confidentiality
|
||||
|
||||
We kindly ask you to keep the report confidential until a public announcement is made.
|
||||
|
||||
## Notes
|
||||
|
||||
- Vulnerabilities will be handled on a best-effort basis.
|
||||
- You may request an advance copy of the patched release, but we cannot guarantee early access before the public release.
|
||||
- You will be notified via email simultaneously with the public announcement.
|
||||
- We will respond within a few weeks to confirm whether your report has been accepted or rejected.
|
||||
|
||||
Thank you for helping to improve the security of our project!
|
||||
@@ -0,0 +1,299 @@
|
||||
#compdef fd
|
||||
|
||||
##
|
||||
# zsh completion function for fd
|
||||
#
|
||||
# Based on ripgrep completion function.
|
||||
# Originally based on code from the zsh-users project — see copyright notice
|
||||
# below.
|
||||
|
||||
autoload -U is-at-least
|
||||
|
||||
_fd() {
|
||||
local curcontext="$curcontext" no='!' ret=1
|
||||
local -a context line state state_descr _arguments_options fd_types fd_args
|
||||
local -A opt_args
|
||||
|
||||
if is-at-least 5.2; then
|
||||
_arguments_options=( -s -S )
|
||||
else
|
||||
_arguments_options=( -s )
|
||||
fi
|
||||
|
||||
fd_types=(
|
||||
{f,file}'\:"regular files"'
|
||||
{d,dir,directory}'\:"directories"'
|
||||
{l,symlink}'\:"symbolic links"'
|
||||
{e,empty}'\:"empty files or directories"'
|
||||
{x,executable}'\:"executable (files)"'
|
||||
{b,block-device}'\:"block devices"'
|
||||
{c,char-device}'\:"character devices"'
|
||||
{s,socket}'\:"sockets"'
|
||||
{p,pipe}'\:"named pipes (FIFOs)"'
|
||||
)
|
||||
|
||||
# Do not complete rare options unless either the current prefix
|
||||
# matches one of those options or the user has the `complete-all`
|
||||
# style set. Note that this prefix check has to be updated manually to account
|
||||
# for all of the potential negation options listed below!
|
||||
if
|
||||
# (--[bpsu]* => match all options marked with '$no')
|
||||
[[ $PREFIX$SUFFIX == --[bopsun]* ]] ||
|
||||
zstyle -t ":complete:$curcontext:*" complete-all
|
||||
then
|
||||
no=
|
||||
fi
|
||||
|
||||
# We make heavy use of argument groups here to prevent the option specs from
|
||||
# growing unwieldy. These aren't supported in zsh <5.4, though, so we'll strip
|
||||
# them out below if necessary. This makes the exclusions inaccurate on those
|
||||
# older versions, but oh well — it's not that big a deal
|
||||
fd_args=(
|
||||
+ '(hidden)' # hidden files
|
||||
{-H,--hidden}'[search hidden files/directories]'
|
||||
|
||||
+ '(no-ignore-full)' # all ignore files
|
||||
'(no-ignore-partial)'{-I,--no-ignore}"[don't respect .(git|fd)ignore and global ignore files]"
|
||||
$no'(no-ignore-partial)*'{-u,--unrestricted}'[alias for --no-ignore --hidden]'
|
||||
|
||||
+ no-ignore-partial # some ignore files
|
||||
"(no-ignore-full --no-ignore-vcs)--no-ignore-vcs[don't respect .gitignore files]"
|
||||
"!(no-ignore-full --no-global-ignore-file)--no-global-ignore-file[don't respect the global ignore file]"
|
||||
"$no(no-ignore-full --no-ignore-parent)--no-ignore-parent[don't respect .(git|fd)ignore files in parent directories]"
|
||||
|
||||
+ '(case)' # case-sensitivity
|
||||
{-s,--case-sensitive}'[perform a case-sensitive search]'
|
||||
{-i,--ignore-case}'[perform a case-insensitive search]'
|
||||
|
||||
+ '(regex-pattern)' # regex-based search pattern
|
||||
'(no-regex-pattern)--regex[perform a regex-based search (default)]'
|
||||
|
||||
+ '(no-regex-pattern)' # non-regex-based search pattern
|
||||
{-g,--glob}'[perform a glob-based search]'
|
||||
{-F,--fixed-strings}'[treat pattern as literal string instead of a regex]'
|
||||
'!--literal'
|
||||
'--exact[pattern must exactly match the full filename]'
|
||||
|
||||
+ '(no-require-git)'
|
||||
"$no(no-ignore-full --no-ignore-vcs --no-require-git)--no-require-git[don't require git repo to respect gitignores]"
|
||||
|
||||
+ '(match-full)' # match against full path
|
||||
{-p,--full-path}'[match the pattern against the full path instead of the basename]'
|
||||
|
||||
+ '(follow)' # follow symlinks
|
||||
{-L,--follow}'[follow symbolic links to directories]'
|
||||
'!--dereference'
|
||||
|
||||
+ '(abs-path)' # show absolute paths
|
||||
'(long-listing)'{-a,--absolute-path}'[show absolute paths instead of relative paths]'
|
||||
|
||||
+ '(null-sep)' # use null separator for output
|
||||
'(long-listing)'{-0,--print0}'[separate search results by the null character]'
|
||||
|
||||
+ '(long-listing)' # long-listing output
|
||||
'(abs-path null-sep max-results exec-cmds)'{-l,--list-details}'[use a long listing format with file metadata]'
|
||||
|
||||
+ '(output-format)' # template-based output
|
||||
'(long-listing)'--format=[print results according to template]:format
|
||||
|
||||
+ '(max-results)' # max number of results
|
||||
'(long-listing exec-cmds quiet)--max-results=[limit number of search results to given count and quit]:count'
|
||||
'(long-listing exec-cmds)-1[limit to a single search result and quit]'
|
||||
|
||||
+ '(quiet)'
|
||||
'(max-results)'{-q,--quiet}'[print nothing. exit code 1 if no matches]'
|
||||
'!(max-results)--has-results'
|
||||
|
||||
+ '(fs-errors)' # file-system errors
|
||||
$no'--show-errors[enable the display of filesystem errors]'
|
||||
|
||||
+ '(fs-traversal)' # file-system traversal
|
||||
$no"--one-file-system[don't descend into directories on other file systems]"
|
||||
'!--mount'
|
||||
'!--xdev'
|
||||
|
||||
+ dir-depth # directory depth
|
||||
'(--exact-depth -d --max-depth)'{-d+,--max-depth=}'[set max directory depth to descend when searching]:depth'
|
||||
'!(--exact-depth -d --max-depth)--maxdepth=:depth'
|
||||
'(--exact-depth --min-depth)--min-depth=[set directory depth to descend before start searching]:depth'
|
||||
'!(--exact-depth --min-depth)--mindepth=:depth'
|
||||
'(--exact-depth -d --max-depth --maxdepth --min-depth)--exact-depth=[only search at the exact given directory depth]:depth'
|
||||
|
||||
+ prune # pruning
|
||||
"--prune[don't traverse into matching directories]"
|
||||
|
||||
+ filter-misc # filter search
|
||||
'*'{-t+,--type=}"[filter search by type]:type:(($fd_types))"
|
||||
'*'{-e+,--extension=}'[filter search by file extension]:extension'
|
||||
'*'{-E+,--exclude=}'[exclude files/directories that match the given glob pattern]:glob pattern'
|
||||
'*'{-S+,--size=}'[limit search by file size]:size limit:->size'
|
||||
'(-o --owner)'{-o+,--owner=}'[filter by owning user and/or group]:owner and/or group:->owner'
|
||||
|
||||
+ ignore-file # extra ignore files
|
||||
'*--ignore-file=[add a custom, low-precedence ignore-file with .gitignore format]: :_files'
|
||||
|
||||
+ '(filter-mtime-newer)' # filter by files modified after than
|
||||
'--changed-within=[limit search to files/directories modified within the given date/duration]:date or duration'
|
||||
'--changed-after=[alias for --changed-within]:date/duration'
|
||||
'!--change-newer-than=:date/duration'
|
||||
'!--newer=:date/duration'
|
||||
|
||||
+ '(filter-mtime-older)' # filter by files modified before than
|
||||
'--changed-before=[limit search to files/directories modified before the given date/duration]:date or duration'
|
||||
'!--change-older-than=:date/duration'
|
||||
'!--older=:date/duration'
|
||||
|
||||
+ '(color)' # colorize output
|
||||
{-c+,--color=}'[declare when to colorize search results]:when to colorize:((
|
||||
auto\:"show colors if the output goes to an interactive console (default)"
|
||||
never\:"do not use colorized output"
|
||||
always\:"always use colorized output"
|
||||
))'
|
||||
|
||||
'--hyperlink=-[add hyperlinks to output paths]::when:(auto never always)'
|
||||
'!--hyper=-'
|
||||
|
||||
+ '(threads)'
|
||||
{-j+,--threads=}'[set the number of threads for searching and executing]:number of threads'
|
||||
|
||||
+ '(exec-cmds)' # execute command
|
||||
'(long-listing max-results)'{-x+,--exec=}'[execute command for each search result]:command: _command_names -e:*\;::program arguments: _normal'
|
||||
'(long-listing max-results)'{-X+,--exec-batch=}'[execute command for all search results at once]:command: _command_names -e:*\;::program arguments: _normal'
|
||||
'(long-listing max-results)--batch-size=[max number of args for each -X call]:size'
|
||||
|
||||
+ other
|
||||
'!(--max-buffer-time)--max-buffer-time=[set amount of time to buffer before showing output]:time (ms)'
|
||||
|
||||
'*--ignore-contain=[Ignore dirs containing named file]:name'
|
||||
|
||||
+ '(about)' # about flags
|
||||
'(: * -)'{-h,--help}'[display help message]'
|
||||
'(: * -)'{-V,--version}'[display version information]'
|
||||
|
||||
+ path-sep # set path separator for output
|
||||
$no'(--path-separator)--path-separator=[set the path separator to use when printing file paths]:path separator'
|
||||
|
||||
+ search-path
|
||||
$no'(--base-directory)--base-directory=[change the current working directory to the given path]:directory:_files -/'
|
||||
$no'(*)*--search-path=[set search path (instead of positional <path> arguments)]:directory:_files -/'
|
||||
|
||||
+ strip-cwd-prefix
|
||||
$no'(strip-cwd-prefix exec-cmds)--strip-cwd-prefix=-[When to strip ./]::when:(always never auto)'
|
||||
|
||||
+ and
|
||||
'*--and=[additional required search pattern]:pattern'
|
||||
|
||||
|
||||
+ args # positional arguments
|
||||
'1: :_guard "^-*" pattern'
|
||||
'(--search-path)*:directory:_files -/'
|
||||
)
|
||||
|
||||
# Strip out argument groups where unsupported (see above)
|
||||
is-at-least 5.4 ||
|
||||
fd_args=( ${(@)args:#(#i)(+|[a-z0-9][a-z0-9_-]#|\([a-z0-9][a-z0-9_-]#\))} )
|
||||
|
||||
_arguments $_arguments_options : $fd_args && ret=0
|
||||
|
||||
case ${state} in
|
||||
owner)
|
||||
compset -P '(\\|)\!'
|
||||
if compset -P '*:'; then
|
||||
_groups && ret=0
|
||||
else
|
||||
if
|
||||
compset -S ':*' ||
|
||||
# Do not add the colon suffix when completing "!user<TAB>
|
||||
# (with a starting double-quote) otherwise pressing tab again
|
||||
# after the inserted colon "!user:<TAB> will complete history modifiers
|
||||
[[ $IPREFIX == (\\|\!)* && ($QIPREFIX == \"* && -z $QISUFFIX) ]]
|
||||
then
|
||||
_users && ret=0
|
||||
else
|
||||
local q
|
||||
# Since quotes are needed when using the negation prefix !,
|
||||
# automatically remove the colon suffix also when closing the quote
|
||||
if [[ $QIPREFIX == [\'\"]* ]]; then
|
||||
q=${QIPREFIX:0:1}
|
||||
fi
|
||||
_users -r ": \t\n\-$q" -S : && ret=0
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
|
||||
size)
|
||||
if compset -P '[-+][0-9]##'; then
|
||||
local -a suff=(
|
||||
'B:bytes'
|
||||
'K:kilobytes (10^3 = 1000 bytes)'
|
||||
'M:megabytes (10^6 = 1000^2 bytes)'
|
||||
'G:gigabytes (10^9 = 1000^3 bytes)'
|
||||
'T:terabytes (10^12 = 1000^4 bytes)'
|
||||
'Ki:kibibytes ( 2^10 = 1024 bytes)'
|
||||
'Mi:mebibytes ( 2^20 = 1024^2 bytes)'
|
||||
'Gi:gibibytes ( 2^30 = 1024^3 bytes)'
|
||||
'Ti:tebibytes ( 2^40 = 1024^4 bytes)'
|
||||
)
|
||||
_describe -t units 'size limit units' suff -V 'units'
|
||||
elif compset -P '[-+]'; then
|
||||
_message -e 'size limit number (full format: <+-><number><unit>)'
|
||||
else
|
||||
_values 'size limit prefix (full format: <prefix><number><unit>)' \
|
||||
'\+[file size must be greater or equal to]'\
|
||||
'-[file size must be less than or equal to]' && ret=0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
_fd "$@"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2011 GitHub zsh-users - http://github.com/zsh-users
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the zsh-users nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL ZSH-USERS BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
# Description
|
||||
# -----------
|
||||
#
|
||||
# Completion script for fd
|
||||
#
|
||||
# ------------------------------------------------------------------------------
|
||||
# Authors
|
||||
# -------
|
||||
#
|
||||
# * smancill (https://github.com/smancill)
|
||||
#
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Local Variables:
|
||||
# mode: shell-script
|
||||
# coding: utf-8-unix
|
||||
# indent-tabs-mode: nil
|
||||
# sh-indentation: 2
|
||||
# sh-basic-offset: 2
|
||||
# End:
|
||||
# vim: ft=zsh sw=2 ts=2 et
|
||||
@@ -0,0 +1,2 @@
|
||||
#compdef fdfind
|
||||
_fd "$@"
|
||||
@@ -0,0 +1,8 @@
|
||||
# Source fd completions
|
||||
source /usr/share/bash-completion/completions/fd
|
||||
|
||||
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
|
||||
complete -F _fd -o nosort -o bashdefault -o default fdfind
|
||||
else
|
||||
complete -F _fd -o bashdefault -o default fdfind
|
||||
fi
|
||||
@@ -0,0 +1 @@
|
||||
complete -c fdfind --wraps fd
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
* linguist-vendored
|
||||
Vendored
+588
@@ -0,0 +1,588 @@
|
||||
.TH FD 1
|
||||
.SH NAME
|
||||
fd \- find entries in the filesystem
|
||||
.SH SYNOPSIS
|
||||
.B fd
|
||||
.RB [ \-HIEsiaLp0hV ]
|
||||
.RB [ \-d
|
||||
.IR depth ]
|
||||
.RB [ \-t
|
||||
.IR filetype ]
|
||||
.RB [ \-e
|
||||
.IR ext ]
|
||||
.RB [ \-E
|
||||
.IR exclude ]
|
||||
.RB [ \-c
|
||||
.IR when ]
|
||||
.RB [ \-j
|
||||
.IR num ]
|
||||
.RB [ \-x
|
||||
.IR cmd ]
|
||||
.RI [ pattern
|
||||
.RI [ path... ]]
|
||||
.SH DESCRIPTION
|
||||
.B fd
|
||||
is a simple, fast and user-friendly alternative to
|
||||
.BR find (1).
|
||||
.P
|
||||
By default
|
||||
.B fd
|
||||
uses regular expressions for the pattern. However, this can be changed to use simple glob patterns
|
||||
with the '\-\-glob' option.
|
||||
.P
|
||||
By default
|
||||
.B fd
|
||||
will exclude hidden files and directories, as well as any files that match gitignore rules
|
||||
or ignore rules in .ignore or .fdignore files.
|
||||
.P
|
||||
If you wish to search all files in a specific directory, you can use '' or . as the search pattern,
|
||||
to match all files. Or you can use the '\-\-search\-path' option to specify the path(s) instead of
|
||||
the positional parameter.
|
||||
.P
|
||||
Options may be given anywhere on the command line.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-H, \-\-hidden
|
||||
Include hidden files and directories in the search results
|
||||
(default: hidden files and directories are skipped). The flag can be overridden with '--no-hidden'.
|
||||
.IP
|
||||
Ignored files are still excluded unless \-\-no\-ignore or \-\-no\-ignore\-vcs
|
||||
is also used.
|
||||
.TP
|
||||
.B \-I, \-\-no\-ignore
|
||||
Show search results from files and directories that would otherwise be ignored by
|
||||
.RS
|
||||
.IP \[bu] 2
|
||||
.I .gitignore
|
||||
.IP \[bu]
|
||||
.I .git/info/exclude
|
||||
.IP \[bu]
|
||||
The global gitignore configuration (by default
|
||||
.IR $HOME/.config/git/ignore )
|
||||
.IP \[bu]
|
||||
.I .ignore
|
||||
.IP \[bu]
|
||||
.I .fdignore
|
||||
.IP \[bu]
|
||||
The global fd ignore file (usually
|
||||
.I $HOME/.config/fd/ignore
|
||||
)
|
||||
.RE
|
||||
.IP
|
||||
The flag can be overridden with '--ignore'.
|
||||
.TP
|
||||
.B \-u, \-\-unrestricted
|
||||
Perform an unrestricted search, including ignored and hidden files. This is an alias for '--hidden --no-ignore'.
|
||||
.TP
|
||||
.B \-\-no\-ignore\-vcs
|
||||
Show search results from files and directories that would otherwise be ignored by gitignore files
|
||||
including
|
||||
.IR .gitignore ,
|
||||
.IR .git/info/exclude ,
|
||||
and the global gitignore configuration
|
||||
.RI ( core.excludesFile
|
||||
git setting, which defaults to
|
||||
.IR $HOME/.config/git/ignore ).
|
||||
The flag can be overridden with '--ignore-vcs'.
|
||||
.TP
|
||||
.B \-\-no\-require\-git
|
||||
Do not require a git repository to respect gitignores. By default, fd will only
|
||||
respect global gitignore rules, .gitignore rules and local exclude rules if fd
|
||||
detects that you are searching inside a git repository. This flag allows you to
|
||||
relax this restriction such that fd will respect all git related ignore rules
|
||||
regardless of whether you’re searching in a git repository or not. The flag can
|
||||
be overridden with '--require-git'.
|
||||
.TP
|
||||
.B \-\-no\-ignore\-parent
|
||||
Show search results from files and directories that would otherwise be ignored by
|
||||
'.gitignore', '.ignore', or '.fdignore' files in parent directories.
|
||||
.TP
|
||||
.B \-s, \-\-case\-sensitive
|
||||
Perform a case-sensitive search. By default, fd uses case-insensitive searches, unless the
|
||||
pattern contains an uppercase character (smart case).
|
||||
.TP
|
||||
.B \-i, \-\-ignore\-case
|
||||
Perform a case-insensitive search. By default, fd uses case-insensitive searches, unless the
|
||||
pattern contains an uppercase character (smart case).
|
||||
.TP
|
||||
.B \-g, \-\-glob
|
||||
Perform a glob-based search instead of a regular expression search.
|
||||
If combined with the '\-\-full-path' option, '**' can be used to match multiple path components.
|
||||
.TP
|
||||
.B \-\-regex
|
||||
Perform a regular-expression based search (default). This can be used to override --glob.
|
||||
.TP
|
||||
.B \-F, \-\-fixed\-strings
|
||||
Treat the pattern as a literal string instead of a regular expression. Note that this also
|
||||
performs substring comparison. If you want to match on an exact filename, consider using '\-\-glob'
|
||||
or '\-\-exact'.
|
||||
.TP
|
||||
.B \-\-exact
|
||||
Perform an exact match. This is equivalent to '\-\-fixed\-strings' but requires the pattern to
|
||||
match the entire filename (or the full path if '\-\-full\-path' is used), rather than a substring.
|
||||
Special regex characters in the pattern are treated as literal characters.
|
||||
Cannot be used together with '\-\-glob' or '\-\-fixed\-strings'.
|
||||
.TP
|
||||
.BI "\-\-and " pattern
|
||||
Add additional required search patterns, all of which must be matched. Multiple additional
|
||||
patterns can be specified. The patterns are regular expressions, unless '\-\-glob'
|
||||
or '\-\-fixed\-strings' is used.
|
||||
.TP
|
||||
.B \-a, \-\-absolute\-path
|
||||
Shows the full path starting from the root as opposed to relative paths.
|
||||
The flag can be overridden with '--relative-path'.
|
||||
.TP
|
||||
.B \-l, \-\-list\-details
|
||||
Use a detailed listing format like 'ls -l'. This is basically an alias
|
||||
for '--exec-batch ls -l' with some additional 'ls' options. This can be used
|
||||
to see more metadata, to show symlink targets and to achieve a deterministic
|
||||
sort order.
|
||||
.TP
|
||||
.B \-L, \-\-follow
|
||||
By default, fd does not descend into symlinked directories. Using this flag, symbolic links are
|
||||
also traversed. The flag can be overridden with '--no-follow'.
|
||||
.TP
|
||||
.B \-p, \-\-full\-path
|
||||
By default, the search pattern is only matched against the filename (or directory name). Using
|
||||
this flag, the
|
||||
.I pattern
|
||||
is matched against the full path.
|
||||
.TP
|
||||
.B \-0, \-\-print0
|
||||
Separate search results by the null character (instead of newlines). Useful for piping results to
|
||||
.IR xargs .
|
||||
.TP
|
||||
.B \-\-max\-results count
|
||||
Limit the number of search results to 'count' and quit immediately.
|
||||
.TP
|
||||
.B \-1
|
||||
Limit the search to a single result and quit immediately. This is an alias for '--max-results=1'.
|
||||
.TP
|
||||
.B \-q, \-\-quiet
|
||||
When the flag is present, the program does not print anything and will instead exit with a code of 0 if there is at least one search result.
|
||||
Otherwise, the exit code will be 1.
|
||||
This is mainly for usage in scripts and can be faster than checking for output because the search can be stopped early after the first match.
|
||||
.B \-\-has\-results
|
||||
can be used as an alias.
|
||||
.TP
|
||||
.B \-\-show-errors
|
||||
Enable the display of filesystem errors for situations such as insufficient
|
||||
permissions or dead symlinks.
|
||||
.TP
|
||||
.B \-\-strip-cwd-prefix [when]
|
||||
By default, relative paths are prefixed with './' when -x/--exec,
|
||||
-X/--exec-batch, or -0/--print0 are given, to reduce the risk of a
|
||||
path starting with '-' being treated as a command line option. Use
|
||||
this flag to change this behavior. If this flag is used without a value,
|
||||
it is equivalent to passing "always". Possible values are:
|
||||
.RS
|
||||
.IP never
|
||||
Never strip the ./ at the beginning of paths
|
||||
.IP always
|
||||
Always strip the ./ at the beginning of paths
|
||||
.IP auto
|
||||
Only strip if used with --exec, --exec-batch, or --print0. That is, it resets to the default behavior.
|
||||
.RE
|
||||
.TP
|
||||
.B \-\-one\-file\-system, \-\-mount, \-\-xdev
|
||||
By default, fd will traverse the file system tree as far as other options dictate. With this flag, fd ensures that it does not descend into a different file system than the one it started in. Comparable to the -mount or -xdev filters of find(1).
|
||||
.TP
|
||||
.B \-h, \-\-help
|
||||
Print help information.
|
||||
.TP
|
||||
.B \-V, \-\-version
|
||||
Print version information.
|
||||
.TP
|
||||
.BI "\-d, \-\-max\-depth " d
|
||||
Limit directory traversal to at most
|
||||
.I d
|
||||
levels of depth. By default, there is no limit on the search depth.
|
||||
.TP
|
||||
.BI "\-\-min\-depth " d
|
||||
Only show search results starting at the given depth. See also: '--max-depth' and '--exact-depth'.
|
||||
.TP
|
||||
.BI "\-\-exact\-depth " d
|
||||
Only show search results at the exact given depth. This is an alias for '--min-depth <depth> --max-depth <depth>'.
|
||||
.TP
|
||||
.B \-\-prune
|
||||
Do not traverse into matching directories.
|
||||
.TP
|
||||
.BI "\-t, \-\-type " filetype
|
||||
Filter search by type:
|
||||
.RS
|
||||
.IP "f, file" 20
|
||||
regular files
|
||||
.IP "d, dir, directory"
|
||||
directories
|
||||
.IP "l, symlink"
|
||||
symbolic links
|
||||
.IP "b, block-device"
|
||||
block devices
|
||||
.IP "c, char-device"
|
||||
character devices
|
||||
.IP "s, socket"
|
||||
sockets
|
||||
.IP "p, pipe"
|
||||
named pipes (FIFOs)
|
||||
.IP "x, executable"
|
||||
executable (files)
|
||||
.IP "e, empty"
|
||||
empty files or directories
|
||||
.RE
|
||||
|
||||
.RS
|
||||
This option can be specified more than once to include multiple file types.
|
||||
Searching for '--type file --type symlink' will show both regular files as well as
|
||||
symlinks. Note that the 'executable' and 'empty' filters work differently: '--type
|
||||
executable' implies '--type file' by default. And '--type empty' searches for
|
||||
empty files and directories, unless either '--type file' or '--type directory' is
|
||||
specified in addition.
|
||||
|
||||
Examples:
|
||||
- Only search for files:
|
||||
fd --type file …
|
||||
fd -tf …
|
||||
- Find both files and symlinks
|
||||
fd --type file --type symlink …
|
||||
fd -tf -tl …
|
||||
- Find executable files:
|
||||
fd --type executable
|
||||
fd -tx
|
||||
- Find empty files:
|
||||
fd --type empty --type file
|
||||
fd -te -tf
|
||||
- Find empty directories:
|
||||
fd --type empty --type directory
|
||||
fd -te -td
|
||||
.RE
|
||||
.TP
|
||||
.BI "\-e, \-\-extension " ext
|
||||
Filter search results by extension
|
||||
.IR ext .
|
||||
This option can be used repeatedly to allow for multiple possible extensions.
|
||||
By default, extension filters apply to both files and directories. Use
|
||||
\fB\-\-type\fR to restrict the search to specific types.
|
||||
|
||||
If you want to search for files without extension, you can use the regex '^[^.]+$'
|
||||
as a normal search pattern.
|
||||
.TP
|
||||
.BI "\-E, \-\-exclude " glob
|
||||
Exclude files/directories that match the given glob pattern.
|
||||
This overrides any other ignore logic.
|
||||
Multiple exclude patterns can be specified.
|
||||
Examples:
|
||||
\-\-exclude '*.pyc'
|
||||
\-\-exclude node_modules
|
||||
.TP
|
||||
.BI "\-\-ignore-contain " name
|
||||
Exclude directories that (directly) contain the given name.
|
||||
This option can be specified multiple times.
|
||||
.TP
|
||||
.BI "\-\-ignore-file " path
|
||||
Add a custom ignore-file in '.gitignore' format.
|
||||
These files have a low precedence.
|
||||
.TP
|
||||
.BI "\-c, \-\-color " when
|
||||
Declare
|
||||
.I when
|
||||
to colorize search results:
|
||||
.RS
|
||||
.IP auto
|
||||
Colorize output when standard output is connected to terminal (default).
|
||||
.IP never
|
||||
Do not colorize output.
|
||||
.IP always
|
||||
Always colorize output.
|
||||
.RE
|
||||
.TP
|
||||
.B "\-\-hyperlink
|
||||
Specify whether the output should use terminal escape codes to indicate a hyperlink to a
|
||||
file url pointing to the path.
|
||||
|
||||
The value can be auto, always, or never.
|
||||
|
||||
Currently, the default is "never", and if the option is used without an argument "auto" is
|
||||
used. In the future this may be changed to "auto" and "always".
|
||||
.RS
|
||||
.IP auto
|
||||
Only output hyperlinks if color is also enabled, as a proxy for whether terminal escape
|
||||
codes are acceptable.
|
||||
.IP never
|
||||
Never output hyperlink escapes.
|
||||
.IP always
|
||||
Always output hyperlink escapes, regardless of color settings.
|
||||
.RE
|
||||
.TP
|
||||
.BI "\-j, \-\-threads " num
|
||||
Set number of threads to use for searching & executing (default: number of available CPU cores).
|
||||
.TP
|
||||
.BI "\-S, \-\-size " size
|
||||
Limit results based on the size of files using the format
|
||||
.I <+-><NUM><UNIT>
|
||||
.RS
|
||||
.IP '+'
|
||||
file size must be greater than or equal to this
|
||||
.IP '-'
|
||||
file size must be less than or equal to this
|
||||
.P
|
||||
If neither '+' nor '-' is specified, file size must be exactly equal to this.
|
||||
.IP 'NUM'
|
||||
The numeric size (e.g. 500)
|
||||
.IP 'UNIT'
|
||||
The units for NUM. They are not case-sensitive.
|
||||
Allowed unit values:
|
||||
.RS
|
||||
.IP 'b'
|
||||
bytes
|
||||
.IP 'k'
|
||||
kilobytes (base ten, 10^3 = 1000 bytes)
|
||||
.IP 'm'
|
||||
megabytes
|
||||
.IP 'g'
|
||||
gigabytes
|
||||
.IP 't'
|
||||
terabytes
|
||||
.IP 'ki'
|
||||
kibibytes (base two, 2^10 = 1024 bytes)
|
||||
.IP 'mi'
|
||||
mebibytes
|
||||
.IP 'gi'
|
||||
gibibytes
|
||||
.IP 'ti'
|
||||
tebibytes
|
||||
.RE
|
||||
.RE
|
||||
.TP
|
||||
.BI "\-\-changed-within " date|duration
|
||||
Filter results based on the file modification time.
|
||||
Files with modification timestamps greater than the argument will be returned.
|
||||
The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point
|
||||
in time as full RFC3339 format with time zone, as a date or datetime in the
|
||||
local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR), or as the prefix '@'
|
||||
followed by the number of seconds since the Unix epoch (@[0-9]+).
|
||||
\fB\-\-change-newer-than\fR,
|
||||
.B --newer
|
||||
or
|
||||
.B --changed-after
|
||||
can be used as aliases.
|
||||
|
||||
Examples:
|
||||
\-\-changed-within 2weeks
|
||||
\-\-change-newer-than "2018-10-27 10:00:00"
|
||||
\-\-newer 2018-10-27
|
||||
\-\-changed-after @1704067200
|
||||
.TP
|
||||
.BI "\-\-changed-before " date|duration
|
||||
Filter results based on the file modification time.
|
||||
Files with modification timestamps less than the argument will be returned.
|
||||
The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point
|
||||
in time as full RFC3339 format with time zone, as a date or datetime in the
|
||||
local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR), or as the prefix '@'
|
||||
followed by the number of seconds since the Unix epoch (@[0-9]+).
|
||||
.B --change-older-than
|
||||
or
|
||||
.B --older
|
||||
can be used as aliases.
|
||||
|
||||
Examples:
|
||||
\-\-changed-before "2018-10-27 10:00:00"
|
||||
\-\-change-older-than 2weeks
|
||||
\-\-older @1704067200
|
||||
.TP
|
||||
.BI "-o, \-\-owner " [user][:group]
|
||||
Filter files by their user and/or group. Format: [(user|uid)][:(group|gid)]. Either side
|
||||
is optional. Precede either side with a '!' to exclude files instead.
|
||||
|
||||
Examples:
|
||||
\-\-owner john
|
||||
\-\-owner :students
|
||||
\-\-owner "!john:students"
|
||||
.TP
|
||||
.BI "-C, \-\-base\-directory " path
|
||||
Change the current working directory of fd to the provided path. This means that search results will
|
||||
be shown with respect to the given base path. Note that relative paths which are passed to fd via the
|
||||
positional \fIpath\fR argument or the \fB\-\-search\-path\fR option will also be resolved relative to
|
||||
this directory.
|
||||
.TP
|
||||
.BI "\-\-path\-separator " separator
|
||||
Set the path separator to use when printing file paths. The default is the OS-specific separator
|
||||
('/' on Unix, '\\' on Windows).
|
||||
.TP
|
||||
.BI "\-\-search\-path " search\-path
|
||||
Provide paths to search as an alternative to the positional \fIpath\fR argument. Changes the usage to
|
||||
\'fd [FLAGS/OPTIONS] \-\-search\-path PATH \-\-search\-path PATH2 [PATTERN]\'
|
||||
.TP
|
||||
.BI "\-\-format " fmt
|
||||
Specify a template string that is used for printing a line for each file found.
|
||||
|
||||
The following placeholders are substituted into the string for each file before printing:
|
||||
.RS
|
||||
.IP {}
|
||||
path (of the current search result)
|
||||
.IP {/}
|
||||
basename
|
||||
.IP {//}
|
||||
parent directory
|
||||
.IP {.}
|
||||
path without file extension
|
||||
.IP {/.}
|
||||
basename without file extension
|
||||
.IP {{
|
||||
literal '{' (an escape sequence)
|
||||
.IP }}
|
||||
literal '}' (an escape sequence)
|
||||
.P
|
||||
Notice that you can use "{{" and "}}" to escape "{" and "}" respectively, which is especially
|
||||
useful if you need to include the literal text of one of the above placeholders.
|
||||
.RE
|
||||
.TP
|
||||
.BI "\-x, \-\-exec " command
|
||||
.RS
|
||||
Execute
|
||||
.I command
|
||||
for each search result in parallel (use --threads=1 for sequential command execution).
|
||||
|
||||
Note that all subsequent positional arguments are considered to be arguments to the
|
||||
.I command
|
||||
- not to fd.
|
||||
It is therefore recommended to place the \-x/\-\-exec option last. Alternatively, you can supply
|
||||
a ';' argument to end the argument list and continue with more fd options.
|
||||
Most shells require ';' to be escaped: '\\;'.
|
||||
Specify this option multiple times to run several commands for each file or directory found.
|
||||
For each file or directory, fd runs these commands one after another in the order they appear
|
||||
on the command line. In that case, you must supply a ';' argument for all but the last command.
|
||||
When running in parallel, fd buffers command output and prints it together for each search result,
|
||||
without interleaving it with command output for other results.
|
||||
|
||||
The order in which different search results are processed and their output is printed is not
|
||||
guaranteed, even with --threads=1.
|
||||
|
||||
Before executing the command, any placeholder patterns in the command are replaced with the
|
||||
corresponding values for the current file. The same placeholders are used as in the "\-\-format"
|
||||
option.
|
||||
|
||||
If no placeholder is present, an implicit "{}" at the end is assumed.
|
||||
|
||||
If --print0 is also given, then a null character (\\0) will be printed between the output for each found entry.
|
||||
This allows another program to easily distinguish the output for each file if the command(s) produce multiple lines.
|
||||
|
||||
Examples:
|
||||
|
||||
- find all *.zip files and unzip them:
|
||||
|
||||
fd -e zip -x unzip
|
||||
|
||||
- find *.h and *.cpp files and run "clang-format -i .." for each of them:
|
||||
|
||||
fd -e h -e cpp -x clang-format -i
|
||||
|
||||
- Convert all *.jpg files to *.png files:
|
||||
|
||||
fd -e jpg -x convert {} {.}.png
|
||||
|
||||
- Run stat for each *.txt file, separated by null characters
|
||||
|
||||
fd -0 -e txt -x stat
|
||||
.RE
|
||||
.TP
|
||||
.BI "\-X, \-\-exec-batch " command
|
||||
.RS
|
||||
Execute
|
||||
.I command
|
||||
once, with all search results as arguments.
|
||||
|
||||
The order of the arguments is non-deterministic and should not be relied upon.
|
||||
|
||||
This uses the same placeholders as "\-\-format" and "\-\-exec", but instead of expanding
|
||||
once per command invocation each argument containing a placeholder is expanding for every
|
||||
file in a batch and passed as separate arguments.
|
||||
|
||||
If no placeholder is present, an implicit "{}" at the end is assumed.
|
||||
|
||||
Like \-\-exec, subsequent arguments are assumed to be part of
|
||||
.I command
|
||||
until either the end of command arguments or a ';' argument. See above.
|
||||
|
||||
Like \-\-exec, this can be used multiple times, in which case each command will be run in
|
||||
the order given.
|
||||
|
||||
Examples:
|
||||
|
||||
- Find all test_*.py files and open them in your favorite editor:
|
||||
|
||||
fd -g 'test_*.py' -X vim
|
||||
|
||||
Note that this executes a single "vim" process with all search results as arguments.
|
||||
|
||||
- Find all *.rs files and count the lines with "wc -l ...":
|
||||
|
||||
fd -e rs -X wc -l
|
||||
.RE
|
||||
.TP
|
||||
.BI "\-\-batch-size " size
|
||||
Maximum number of arguments to pass to the command given with -X. If the number of results is
|
||||
greater than the given size, the command given with -X is run again with remaining arguments. A
|
||||
batch size of zero means there is no limit (default), but note that batching might still happen
|
||||
due to OS restrictions on the maximum length of command lines.
|
||||
.SH PATTERN SYNTAX
|
||||
The regular expression syntax used by fd is documented here:
|
||||
|
||||
https://docs.rs/regex/1.0.0/regex/#syntax
|
||||
|
||||
The glob syntax is documented here:
|
||||
|
||||
https://docs.rs/globset/#syntax
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
.B LS_COLORS
|
||||
Determines how to colorize search results, see
|
||||
.BR dircolors (1) .
|
||||
.TP
|
||||
.B NO_COLOR
|
||||
Disables colorized output.
|
||||
.TP
|
||||
.B XDG_CONFIG_HOME, HOME
|
||||
Used to locate the global ignore file. If
|
||||
.B XDG_CONFIG_HOME
|
||||
is set, use
|
||||
.IR $XDG_CONFIG_HOME/fd/ignore .
|
||||
Otherwise, use
|
||||
.IR $HOME/.config/fd/ignore .
|
||||
.SH FILES
|
||||
.TP
|
||||
.B .fdignore
|
||||
This file works similarly to a .gitignore file anywhere in the searched tree and specifies patterns
|
||||
that should be excluded from the search. However, this file is specific to fd, and will be used even
|
||||
if the --no-ignore-vcs option is used.
|
||||
.TP
|
||||
.B $XDG_CONFIG_HOME/fd/ignore
|
||||
Global ignore file. Unless ignore mode is turned off (such as with --no-ignore)
|
||||
ignore entries in this file will be ignored, as if it was an .fdignore file in the
|
||||
current directory.
|
||||
.SH EXAMPLES
|
||||
.TP
|
||||
.RI "Find files and directories that match the pattern '" needle "':"
|
||||
$ fd needle
|
||||
.TP
|
||||
.RI "Start a search in a given directory (" /var/log "):"
|
||||
$ fd nginx /var/log
|
||||
.TP
|
||||
.RI "Find all Python files (all files with the extension " .py ") in the current directory:"
|
||||
$ fd -e py
|
||||
.TP
|
||||
.RI "Open all search results with vim:"
|
||||
$ fd pattern -X vim
|
||||
.SH Tips and Tricks
|
||||
.IP \[bu]
|
||||
If you add ".git/" to your global ignore file ($XDG_CONFIG_HOME/fd/ignore), then
|
||||
".git" folders will be ignored by default, even when the --hidden option is used.
|
||||
.IP \[bu]
|
||||
You can use a shell alias or a wrapper script in order to pass desired flags to fd
|
||||
by default. For example if you do not like the default behavior of respecting gitignore,
|
||||
you can use
|
||||
`alias fd="/usr/bin/fd --no-ignore-vcs"`
|
||||
in your .bashrc to create an alias for fd that doesn't ignore git files by default.
|
||||
.SH BUGS
|
||||
Bugs can be reported on GitHub: https://github.com/sharkdp/fd/issues
|
||||
.SH SEE ALSO
|
||||
.BR find (1)
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
Vendored
+161
@@ -0,0 +1,161 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="66mm"
|
||||
height="66mm"
|
||||
viewBox="0 0 66 66.000001"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1 (c4e8f9ed74, 2021-05-24)"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:export-filename="/home/shark/Informatik/rust/fd/doc/logo.png"
|
||||
inkscape:export-xdpi="192.42"
|
||||
inkscape:export-ydpi="192.42"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#999999"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="1"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.1795515"
|
||||
inkscape:cx="114.47309"
|
||||
inkscape:cy="176.18304"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1175"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1"
|
||||
showguides="false"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:snap-global="false"
|
||||
fit-margin-top="2"
|
||||
fit-margin-left="2"
|
||||
fit-margin-right="2"
|
||||
fit-margin-bottom="2"
|
||||
lock-margins="true">
|
||||
<sodipodi:guide
|
||||
position="26.228232,26.126763"
|
||||
orientation="0,-1"
|
||||
id="guide47826" />
|
||||
<sodipodi:guide
|
||||
position="25.799494,2.3628924"
|
||||
orientation="0,-1"
|
||||
id="guide47828" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-21.358009,-148.28012)">
|
||||
<g
|
||||
id="g66267"
|
||||
transform="matrix(0.84959471,0,0,0.84959471,7.9920783,43.351816)">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:29.7126px;line-height:0;font-family:'Fira Sans Condensed';-inkscape-font-specification:'Fira Sans Condensed, ';white-space:pre;inline-size:37.3715;fill:#e5e5e5;fill-opacity:1;stroke-width:0.742816"
|
||||
x="50.647034"
|
||||
y="173.19841"
|
||||
id="text50653"
|
||||
transform="matrix(1.0604862,0,0,1.0604862,-3.3101428,-10.150043)"><tspan
|
||||
x="50.647034"
|
||||
y="173.19841"
|
||||
id="tspan66635"><tspan
|
||||
style="font-family:'Source Code Pro';-inkscape-font-specification:'Source Code Pro'"
|
||||
id="tspan66633">fd</tspan></tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:29.7126px;line-height:0;font-family:'Fira Sans Condensed';-inkscape-font-specification:'Fira Sans Condensed, ';white-space:pre;inline-size:37.3715;fill:#00ccff;fill-opacity:0.996078;stroke-width:0.742816"
|
||||
x="50.647034"
|
||||
y="173.19841"
|
||||
id="text1244"
|
||||
transform="matrix(1.0604862,0,0,1.0604862,-2.8008599,-9.6407599)"><tspan
|
||||
x="50.647034"
|
||||
y="173.19841"
|
||||
id="tspan66639"><tspan
|
||||
style="font-family:'Source Code Pro';-inkscape-font-specification:'Source Code Pro'"
|
||||
id="tspan66637">fd</tspan></tspan></text>
|
||||
<g
|
||||
id="g47824"
|
||||
transform="translate(0.1724878,-0.35338542)">
|
||||
<g
|
||||
id="g42041">
|
||||
<path
|
||||
style="fill:none;stroke:#939dac;stroke-width:1.065;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 28.209616,155.51329 v 15.68758 H 40.83001"
|
||||
id="path39763"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<path
|
||||
style="fill:#b7bec8;stroke:#939dac;stroke-width:1.065;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 27.869464,161.83767 H 40.261291"
|
||||
id="path39765" />
|
||||
</g>
|
||||
<g
|
||||
id="g41945"
|
||||
transform="translate(-1.0583333)">
|
||||
<path
|
||||
style="fill:#0088aa;fill-opacity:0.993797;stroke:none;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 42.164961,159.42939 -1.554274,-1.89462 -1.975227,-0.005 c -0.941841,0.014 -1.165466,0.27232 -1.14085,2.88812 z"
|
||||
id="path40006"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<rect
|
||||
style="fill:#01ccff;fill-opacity:1;stroke:none;stroke-width:2.3;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.80126;paint-order:stroke fill markers"
|
||||
id="rect39949"
|
||||
width="9.4925022"
|
||||
height="6.2080379"
|
||||
x="37.492516"
|
||||
y="158.82776"
|
||||
ry="0.90871465" />
|
||||
</g>
|
||||
<g
|
||||
id="g41951"
|
||||
transform="translate(-1.0583334,9.3665773)">
|
||||
<path
|
||||
style="fill:#373e48;fill-opacity:0.993797;stroke:none;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 42.164961,159.42939 -1.554274,-1.89462 -1.975227,-0.005 c -0.941841,0.014 -1.165466,0.27232 -1.14085,2.88812 z"
|
||||
id="path41947"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<rect
|
||||
style="fill:#535d6c;fill-opacity:0.993797;stroke:none;stroke-width:2.3;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.80126;paint-order:stroke fill markers"
|
||||
id="rect41949"
|
||||
width="9.4925022"
|
||||
height="6.2080379"
|
||||
x="37.492516"
|
||||
y="158.82776"
|
||||
ry="0.90871465" />
|
||||
</g>
|
||||
<g
|
||||
id="g41957"
|
||||
transform="translate(-14.306994,-6.8962642)">
|
||||
<path
|
||||
style="fill:#373e48;fill-opacity:0.993797;stroke:none;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 42.164961,159.42939 -1.554274,-1.89462 -1.975227,-0.005 c -0.941841,0.014 -1.165466,0.27232 -1.14085,2.88812 z"
|
||||
id="path41953"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<rect
|
||||
style="fill:#535d6c;fill-opacity:0.993797;stroke:none;stroke-width:2.3;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.80126;paint-order:stroke fill markers"
|
||||
id="rect41955"
|
||||
width="9.4925022"
|
||||
height="6.2080379"
|
||||
x="37.492516"
|
||||
y="158.82776"
|
||||
ry="0.90871465" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g65006"
|
||||
transform="matrix(0.55302761,0,0,0.55302761,66.463548,117.45819)" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
# Release checklist
|
||||
|
||||
This file can be used as-is, or copied into the GitHub PR description which includes
|
||||
necessary changes for the upcoming release.
|
||||
|
||||
## Version bump
|
||||
|
||||
This section can be done by running `scripts/version-bump.sh` with the new version
|
||||
as an argument.
|
||||
|
||||
- [ ] Create a new branch for the required changes for this release.
|
||||
- [ ] Update version in `Cargo.toml`. Run `cargo build` to update `Cargo.lock`.
|
||||
Make sure to `git add` the `Cargo.lock` changes as well.
|
||||
- [ ] Find the current min. supported Rust version by running
|
||||
`grep rust-version Cargo.toml`.
|
||||
- [ ] Update the `fd` version and the min. supported Rust version in `README.md`.
|
||||
- [ ] Update `CHANGELOG.md`. Change the heading of the *"Upcoming release"* section
|
||||
to the version of this release.
|
||||
|
||||
## Pre-release checks and updates
|
||||
|
||||
- [ ] Install the latest version (`cargo install --locked -f --path .`) and make
|
||||
sure that it is available on the `PATH` (`fd --version` should show the
|
||||
new version).
|
||||
- [ ] Review `-h`, `--help`, and the `man` page.
|
||||
- [ ] Run `fd -h` and copy the output to the *"Command-line options"* section in
|
||||
the README. This can be done by running `gawk -i inplace -f scripts/update-help.awk README.md`.
|
||||
- [ ] Push all changes and wait for CI to succeed (before continuing with the
|
||||
next section).
|
||||
- [ ] Optional: manually test the new features and command-line options described
|
||||
in the `CHANGELOG.md`.
|
||||
- [ ] Run `cargo publish --dry-run` to make sure that it will succeed later
|
||||
(after creating the GitHub release).
|
||||
|
||||
## Release
|
||||
|
||||
- [ ] Merge your release branch (should be a fast-forward merge).
|
||||
- [ ] Create a tag and push it: `git tag vX.Y.Z; git push origin tag vX.Y.Z`.
|
||||
This will trigger the deployment via GitHub Actions.
|
||||
REMINDER: If your `origin` is a fork, don't forget to push to e.g. `upstream`
|
||||
instead.
|
||||
- [ ] Go to https://github.com/sharkdp/fd/releases/new to create the new
|
||||
release. Select the new tag and also use it as the release title. For the
|
||||
release notes, copy the corresponding section from `CHANGELOG.md` and
|
||||
possibly add additional remarks for package maintainers.
|
||||
Publish the release.
|
||||
- [ ] Check if the binary deployment works (archives and Debian packages should
|
||||
appear when the CI run *for the Git tag* has finished).
|
||||
- [ ] Publish to crates.io by running `cargo publish` in a *clean* repository.
|
||||
One way to do this is to clone a fresh copy.
|
||||
|
||||
## Post-release
|
||||
|
||||
- [ ] Prepare a new *"Upcoming release"* section at the top of `CHANGELOG.md`.
|
||||
Put this at the top:
|
||||
|
||||
# Upcoming release
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
## Bugfixes
|
||||
|
||||
|
||||
## Changes
|
||||
|
||||
|
||||
## Other
|
||||
|
||||
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# Designed to be executed via svg-term from the fd root directory:
|
||||
# svg-term --command="bash doc/screencast.sh" --out doc/screencast.svg --padding=10
|
||||
# Then run this (workaround for #1003):
|
||||
# sed -i '' 's/<text/<text font-size="1.67"/g' doc/screencast.svg
|
||||
set -e
|
||||
set -u
|
||||
|
||||
PROMPT="▶"
|
||||
|
||||
enter() {
|
||||
INPUT=$1
|
||||
DELAY=1
|
||||
|
||||
prompt
|
||||
sleep "$DELAY"
|
||||
type "$INPUT"
|
||||
sleep 0.5
|
||||
printf '%b' "\\n"
|
||||
eval "$INPUT"
|
||||
type "\\n"
|
||||
}
|
||||
|
||||
prompt() {
|
||||
printf '%b ' "$PROMPT" | pv -q
|
||||
}
|
||||
|
||||
type() {
|
||||
printf '%b' "$1" | pv -qL $((10+(-2 + RANDOM%5)))
|
||||
}
|
||||
|
||||
main() {
|
||||
IFS='%'
|
||||
|
||||
enter "fd"
|
||||
|
||||
enter "fd app"
|
||||
|
||||
enter "fd fi"
|
||||
|
||||
enter "fd fi --type f"
|
||||
|
||||
enter "fd --type d"
|
||||
|
||||
enter "fd -e md"
|
||||
|
||||
enter "fd -e md --exec wc -l"
|
||||
|
||||
enter "fd '^[A-Z]'"
|
||||
|
||||
enter "fd --exclude src"
|
||||
|
||||
enter "fd --hidden sample"
|
||||
|
||||
prompt
|
||||
|
||||
sleep 3
|
||||
|
||||
echo ""
|
||||
|
||||
unset IFS
|
||||
}
|
||||
|
||||
main
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 124 KiB |
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
## Sponsors
|
||||
|
||||
`fd` development is sponsored by many individuals and companies. Thank you very much!
|
||||
|
||||
Please note, that being sponsored does not affect the individuality of the `fd`
|
||||
project or affect the maintainers' actions in any way.
|
||||
We remain impartial and continue to assess pull requests solely on merit - the
|
||||
features added, bugs solved, and effect on the overall complexity of the code.
|
||||
No issue will have a different priority based on sponsorship status of the
|
||||
reporter.
|
||||
|
||||
Contributions from anybody are most welcomed, please see our [`CONTRIBUTING.md`](../CONTRIBUTING.md) guide.
|
||||
@@ -0,0 +1 @@
|
||||
# Defaults are used
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Create a .tar.gz archive of the fd project for distribution
|
||||
#
|
||||
# Expected environment variables:
|
||||
#
|
||||
# TARGET: rust target to publish. Defaults to default rustc target
|
||||
# BIN_PATH: path to fd binary
|
||||
# PKG_STAGING: directory to use for staging files to include in the archive
|
||||
# ARCHIVE_NAME: Name of output file, minus extension, defaults to "fd" with a version appended
|
||||
# if VERSION is specified
|
||||
# VERSION: version to publish
|
||||
# GITHUB_OUTPUT: file to store github output variables in
|
||||
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
TARGET="$(rustc -vV | sed -n 's/host: //p')"
|
||||
fi
|
||||
|
||||
PKG_suffix=".tar.gz"
|
||||
EXE_suffix=""
|
||||
case ${TARGET} in
|
||||
*-pc-windows-*)
|
||||
PKG_suffix=".zip"
|
||||
EXE_suffix=".exe"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -z $BIN_PATH ]]; then
|
||||
BIN_PATH=target/${PROFILE:-release}/fd
|
||||
fi
|
||||
|
||||
if [[ -z "$ARCHIVE_NAME" ]]; then
|
||||
ARCHIVE_NAME=fd
|
||||
[[ -n "$VERSION" ]] && ARCHIVE_NAME+="-v$VERSION"
|
||||
fi
|
||||
|
||||
PKG_NAME=${ARCHIVE_NAME}${PKG_suffix}
|
||||
|
||||
staging_dir="${PKG_STAGING:-package}"
|
||||
ARCHIVE_DIR="${staging_dir}/${ARCHIVE_NAME}/"
|
||||
mkdir -p "${ARCHIVE_DIR}"
|
||||
|
||||
# Binary
|
||||
cp "${BIN_PATH}" "$ARCHIVE_DIR"
|
||||
|
||||
# README, LICENSE and CHANGELOG files
|
||||
cp "README.md" "LICENSE-MIT" "LICENSE-APACHE" "CHANGELOG.md" "$ARCHIVE_DIR"
|
||||
|
||||
# Man page
|
||||
cp "doc/fd.1" "$ARCHIVE_DIR"
|
||||
|
||||
# Autocompletion files
|
||||
cp -r autocomplete "${ARCHIVE_DIR}"
|
||||
|
||||
# base compressed package
|
||||
pushd "${staging_dir}/" >/dev/null
|
||||
case ${TARGET} in
|
||||
*-pc-windows-*) 7z -y a "${PKG_NAME}" "${ARCHIVE_NAME}"/* | tail -2 ;;
|
||||
*) tar czf "${PKG_NAME}" "${ARCHIVE_NAME}"/* ;;
|
||||
esac;
|
||||
popd >/dev/null
|
||||
|
||||
if [[ -n "$GITHUB_OUTPUT" ]]; then
|
||||
echo "PKG_NAME=${PKG_NAME}" >> $GITHUB_OUTPUT
|
||||
# Let subsequent steps know where to find the compressed package
|
||||
echo "PKG_PATH=${PKG_STAGING}/${PKG_NAME}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
Executable
+135
@@ -0,0 +1,135 @@
|
||||
#!/bin/bash
|
||||
COPYRIGHT_YEARS="2018 - "$(date "+%Y")
|
||||
MAINTAINER="David Peter <mail@david-peter.de>"
|
||||
REPO="https://github.com/sharkdp/fd"
|
||||
DPKG_STAGING="${CICD_INTERMEDIATES_DIR:-.}/debian-package"
|
||||
DPKG_DIR="${DPKG_STAGING}/dpkg"
|
||||
mkdir -p "${DPKG_DIR}"
|
||||
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
TARGET="$(rustc -vV | sed -n 's|host: \(.*\)|\1|p')"
|
||||
fi
|
||||
|
||||
case "$TARGET" in
|
||||
*-musl*)
|
||||
DPKG_BASENAME=fd-musl
|
||||
DPKG_CONFLICTS="fd, fd-find"
|
||||
;;
|
||||
*)
|
||||
DPKG_BASENAME=fd
|
||||
DPKG_CONFLICTS="fd-musl, fd-find"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -z "$DPKG_VERSION" ]]; then
|
||||
DPKG_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r .packages[0].version)
|
||||
fi
|
||||
|
||||
unset DPKG_ARCH
|
||||
case "${TARGET}" in
|
||||
aarch64-*-linux-*) DPKG_ARCH=arm64 ;;
|
||||
arm-*-linux-*hf) DPKG_ARCH=armhf ;;
|
||||
i686-*-linux-*) DPKG_ARCH=i686 ;;
|
||||
x86_64-*-linux-*) DPKG_ARCH=amd64 ;;
|
||||
*) DPKG_ARCH=notset ;;
|
||||
esac;
|
||||
|
||||
DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb"
|
||||
|
||||
BIN_PATH=${BIN_PATH:-target/${TARGET}/release/fd}
|
||||
|
||||
# Binary
|
||||
install -Dm755 "${BIN_PATH}" "${DPKG_DIR}/usr/bin/fd"
|
||||
|
||||
# Man page
|
||||
install -Dm644 'doc/fd.1' "${DPKG_DIR}/usr/share/man/man1/fd.1"
|
||||
gzip -n --best "${DPKG_DIR}/usr/share/man/man1/fd.1"
|
||||
|
||||
# Autocompletion files
|
||||
install -Dm644 'autocomplete/fd.bash' "${DPKG_DIR}/usr/share/bash-completion/completions/fd"
|
||||
install -Dm644 'autocomplete/fd.fish' "${DPKG_DIR}/usr/share/fish/vendor_completions.d/fd.fish"
|
||||
install -Dm644 'autocomplete/_fd' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_fd"
|
||||
|
||||
# README and LICENSE
|
||||
install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md"
|
||||
install -Dm644 "LICENSE-MIT" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-MIT"
|
||||
install -Dm644 "LICENSE-APACHE" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-APACHE"
|
||||
install -Dm644 "CHANGELOG.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog"
|
||||
gzip -n --best "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog"
|
||||
|
||||
# Create symlink and completions so fdfind can be used as well:
|
||||
ln -s "/usr/bin/fd" "${DPKG_DIR}/usr/bin/fdfind"
|
||||
|
||||
install -Dm644 'autocomplete/fdfind.bash' "${DPKG_DIR}/usr/share/bash-completion/completions/fdfind"
|
||||
install -Dm644 'autocomplete/fdfind.fish' "${DPKG_DIR}/usr/share/fish/vendor_completions.d/fdfind.fish"
|
||||
install -Dm644 'autocomplete/_fdfind' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_fdfind"
|
||||
|
||||
cat > "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright" <<EOF
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: fd
|
||||
Source: ${REPO}
|
||||
|
||||
Files: *
|
||||
Copyright: ${MAINTAINER}
|
||||
Copyright: $COPYRIGHT_YEARS ${MAINTAINER}
|
||||
License: Apache-2.0 or MIT
|
||||
|
||||
License: Apache-2.0
|
||||
On Debian systems, the complete text of the Apache-2.0 can be found in the
|
||||
file /usr/share/common-licenses/Apache-2.0.
|
||||
|
||||
License: MIT
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
EOF
|
||||
chmod 644 "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright"
|
||||
|
||||
# control file
|
||||
mkdir -p "${DPKG_DIR}/DEBIAN"
|
||||
cat > "${DPKG_DIR}/DEBIAN/control" <<EOF
|
||||
Package: ${DPKG_BASENAME}
|
||||
Version: ${DPKG_VERSION}
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Maintainer: ${MAINTAINER}
|
||||
Homepage: ${REPO}
|
||||
Architecture: ${DPKG_ARCH}
|
||||
Provides: fd
|
||||
Conflicts: ${DPKG_CONFLICTS}
|
||||
Description: simple, fast and user-friendly alternative to find
|
||||
fd is a program to find entries in your filesystem.
|
||||
It is a simple, fast and user-friendly alternative to find.
|
||||
While it does not aim to support all of finds powerful functionality, it provides
|
||||
sensible (opinionated) defaults for a majority of use cases.
|
||||
EOF
|
||||
|
||||
DPKG_PATH="${DPKG_STAGING}/${DPKG_NAME}"
|
||||
|
||||
if [[ -n $GITHUB_OUTPUT ]]; then
|
||||
echo "DPKG_NAME=${DPKG_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "DPKG_PATH=${DPKG_PATH}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# build dpkg
|
||||
fakeroot dpkg-deb --build "${DPKG_DIR}" "${DPKG_PATH}"
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/gawk -f
|
||||
|
||||
BEGIN {
|
||||
inSection = 0
|
||||
inBlock = 0
|
||||
}
|
||||
|
||||
|
||||
/Command-line options/ { inSection=1 }
|
||||
|
||||
inSection && /^```/ {
|
||||
inBlock=1
|
||||
inSection=0
|
||||
# print the starting fence then move to next line
|
||||
print
|
||||
next
|
||||
}
|
||||
|
||||
inBlock && /^```/ {
|
||||
cmd="cargo run --release --quiet -- -h"
|
||||
# Output the results of fd -h
|
||||
u=0
|
||||
while (cmd | getline line) {
|
||||
# Skip everything before the usage line
|
||||
if (line ~ /^Usage/) {
|
||||
u=1;
|
||||
}
|
||||
if (u) {
|
||||
print line
|
||||
}
|
||||
}
|
||||
status = close(cmd)
|
||||
if (status) {
|
||||
print "failed to generate help output" > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
inBlock = 0
|
||||
}
|
||||
|
||||
!inBlock
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
# This script automates the "Version bump" section
|
||||
|
||||
version="$1"
|
||||
|
||||
if [[ -z $version ]]; then
|
||||
echo "Usage: must supply version as first argument" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git switch -C "release-$version"
|
||||
sed -i -e "0,/^\[badges/{s/^version =.*/version = \"$version\"/}" Cargo.toml
|
||||
|
||||
msrv="$(grep -F rust-version Cargo.toml | sed -e 's/^rust-version= "\(.*\)"/\1/')"
|
||||
|
||||
sed -i -e "s/Note that rust version \*[0-9.]+\* or later/Note that rust version *$msrv* or later/" README.md
|
||||
|
||||
sed -i -e "s/^# Upcoming release/# $version/" CHANGELOG.md
|
||||
|
||||
@@ -0,0 +1,983 @@
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use clap::{
|
||||
Arg, ArgAction, ArgGroup, ArgMatches, Command, Parser, ValueEnum, builder::ArgPredicate,
|
||||
error::ErrorKind, value_parser,
|
||||
};
|
||||
#[cfg(feature = "completions")]
|
||||
use clap_complete::Shell;
|
||||
use normpath::PathExt;
|
||||
|
||||
use crate::exec::CommandSet;
|
||||
use crate::filesystem;
|
||||
#[cfg(unix)]
|
||||
use crate::filter::OwnerFilter;
|
||||
use crate::filter::SizeFilter;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "fd",
|
||||
version,
|
||||
about = "A program to find entries in your filesystem with regex and glob based matching. By default, fd respects gitignore rules, ignores hidden directories, and is case insensitive.",
|
||||
after_long_help = "Bugs can be reported on GitHub: https://github.com/sharkdp/fd/issues",
|
||||
max_term_width = 98,
|
||||
args_override_self = true,
|
||||
group(ArgGroup::new("execs").args(&["exec", "exec_batch", "list_details"]).conflicts_with_all(&[
|
||||
"max_results", "quiet", "max_one_result"])),
|
||||
)]
|
||||
pub struct Opts {
|
||||
/// Include hidden directories and files in the search results (default:
|
||||
/// hidden files and directories are skipped). Files and directories are
|
||||
/// considered to be hidden if their name starts with a `.` sign (dot).
|
||||
/// Any files or directories that are ignored due to the rules described by
|
||||
/// --no-ignore are still ignored unless otherwise specified.
|
||||
/// The flag can be overridden with --no-hidden.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'H',
|
||||
help = "Search hidden files and directories",
|
||||
long_help,
|
||||
default_value_ifs([
|
||||
("no_hidden", ArgPredicate::IsPresent, Some("false")),
|
||||
("rg_alias_hidden_ignore", ArgPredicate::IsPresent, Some("true")),
|
||||
])
|
||||
)]
|
||||
pub hidden: bool,
|
||||
|
||||
/// Overrides --hidden
|
||||
#[arg(long, overrides_with = "hidden", hide = true, action = ArgAction::SetTrue)]
|
||||
no_hidden: (),
|
||||
|
||||
/// Show search results from files and directories that would otherwise be
|
||||
/// ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file.
|
||||
/// The flag can be overridden with --ignore.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'I',
|
||||
help = "Do not respect .(git|fd)ignore files",
|
||||
long_help,
|
||||
default_value_ifs([
|
||||
("ignore", ArgPredicate::IsPresent, Some("false")),
|
||||
("rg_alias_hidden_ignore", ArgPredicate::IsPresent, Some("true")),
|
||||
])
|
||||
)]
|
||||
pub no_ignore: bool,
|
||||
|
||||
/// Overrides --no-ignore
|
||||
#[arg(long, overrides_with = "no_ignore", hide = true, action = ArgAction::SetTrue)]
|
||||
ignore: (),
|
||||
|
||||
///Show search results from files and directories that
|
||||
///would otherwise be ignored by '.gitignore' files.
|
||||
///The flag can be overridden with --ignore-vcs.
|
||||
#[arg(
|
||||
long,
|
||||
hide_short_help = true,
|
||||
help = "Do not respect .gitignore files",
|
||||
long_help
|
||||
)]
|
||||
pub no_ignore_vcs: bool,
|
||||
|
||||
/// Overrides --no-ignore-vcs
|
||||
#[arg(long, overrides_with = "no_ignore_vcs", hide = true, action = ArgAction::SetTrue)]
|
||||
ignore_vcs: (),
|
||||
|
||||
/// Do not require a git repository to respect gitignores.
|
||||
/// By default, fd will only respect global gitignore rules, .gitignore rules,
|
||||
/// and local exclude rules if fd detects that you are searching inside a
|
||||
/// git repository. This flag allows you to relax this restriction such that
|
||||
/// fd will respect all git related ignore rules regardless of whether you're
|
||||
/// searching in a git repository or not.
|
||||
///
|
||||
///
|
||||
/// This flag can be disabled with --require-git.
|
||||
#[arg(
|
||||
long,
|
||||
overrides_with = "require_git",
|
||||
hide_short_help = true,
|
||||
// same description as ripgrep's flag: ripgrep/crates/core/app.rs
|
||||
long_help
|
||||
)]
|
||||
pub no_require_git: bool,
|
||||
|
||||
/// Overrides --no-require-git
|
||||
#[arg(long, overrides_with = "no_require_git", hide = true, action = ArgAction::SetTrue)]
|
||||
require_git: (),
|
||||
|
||||
/// Show search results from files and directories that would otherwise be
|
||||
/// ignored by '.gitignore', '.ignore', or '.fdignore' files in parent directories.
|
||||
/// The flag can be overridden with --ignore-parent.
|
||||
#[arg(
|
||||
long,
|
||||
hide_short_help = true,
|
||||
help = "Do not respect .(git|fd)ignore files in parent directories",
|
||||
long_help
|
||||
)]
|
||||
pub no_ignore_parent: bool,
|
||||
|
||||
/// Overrides --no-ignore-parent
|
||||
#[arg(long, overrides_with = "no_ignore_parent", hide = true, action = ArgAction::SetTrue)]
|
||||
ignore_parent: (),
|
||||
|
||||
/// Do not respect the global ignore file
|
||||
#[arg(long, hide = true)]
|
||||
pub no_global_ignore_file: bool,
|
||||
|
||||
/// Perform an unrestricted search, including ignored and hidden files. This is
|
||||
/// an alias for '--no-ignore --hidden'.
|
||||
#[arg(
|
||||
long = "unrestricted",
|
||||
short = 'u',
|
||||
hide_short_help = true,
|
||||
help = "Unrestricted search, alias for '--no-ignore --hidden'",
|
||||
long_help
|
||||
)]
|
||||
rg_alias_hidden_ignore: bool,
|
||||
|
||||
/// Case-sensitive search (default: smart case)
|
||||
#[arg(
|
||||
long,
|
||||
short = 's',
|
||||
overrides_with("ignore_case"),
|
||||
long_help = "Perform a case-sensitive search. By default, fd uses case-insensitive \
|
||||
searches, unless the pattern contains an uppercase character (smart \
|
||||
case)."
|
||||
)]
|
||||
pub case_sensitive: bool,
|
||||
|
||||
/// Perform a case-insensitive search. By default, fd uses case-insensitive
|
||||
/// searches, unless the pattern contains an uppercase character (smart
|
||||
/// case).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'i',
|
||||
overrides_with("case_sensitive"),
|
||||
help = "Case-insensitive search (default: smart case)",
|
||||
long_help
|
||||
)]
|
||||
pub ignore_case: bool,
|
||||
|
||||
/// Perform a glob-based search instead of a regular expression search.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'g',
|
||||
conflicts_with("fixed_strings"),
|
||||
help = "Glob-based search (default: regular expression)",
|
||||
long_help
|
||||
)]
|
||||
pub glob: bool,
|
||||
|
||||
/// Perform a regular-expression based search (default). This can be used to
|
||||
/// override --glob.
|
||||
#[arg(
|
||||
long,
|
||||
overrides_with("glob"),
|
||||
hide_short_help = true,
|
||||
help = "Regular-expression based search (default)",
|
||||
long_help
|
||||
)]
|
||||
pub regex: bool,
|
||||
|
||||
/// Treat the pattern as a literal string instead of a regular expression. Note
|
||||
/// that this also performs substring comparison. If you want to match on an
|
||||
/// exact filename, consider using '--glob' or '--exact' instead.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'F',
|
||||
alias = "literal",
|
||||
hide_short_help = true,
|
||||
help = "Treat pattern as literal string instead of regex",
|
||||
long_help
|
||||
)]
|
||||
pub fixed_strings: bool,
|
||||
|
||||
/// Perform an exact match. This is equivalent to '--fixed-strings' but requires
|
||||
/// the pattern to match the entire filename (or path if '--full-path' is used),
|
||||
/// rather than a substring. Special regex characters in the pattern are treated
|
||||
/// as literal characters.
|
||||
#[arg(
|
||||
long,
|
||||
conflicts_with_all(["glob", "fixed_strings"]),
|
||||
hide_short_help = true,
|
||||
help = "Match the entire filename exactly (literal, non-substring)",
|
||||
long_help
|
||||
)]
|
||||
pub exact: bool,
|
||||
|
||||
/// Add additional required search patterns, all of which must be matched. Multiple
|
||||
/// additional patterns can be specified. The patterns are regular
|
||||
/// expressions, unless '--glob' or '--fixed-strings' is used.
|
||||
#[arg(
|
||||
long = "and",
|
||||
value_name = "pattern",
|
||||
help = "Additional search patterns that need to be matched",
|
||||
long_help,
|
||||
hide_short_help = true,
|
||||
allow_hyphen_values = true
|
||||
)]
|
||||
pub exprs: Option<Vec<String>>,
|
||||
|
||||
/// Shows the full path starting from the root as opposed to relative paths.
|
||||
/// The flag can be overridden with --relative-path.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'a',
|
||||
help = "Show absolute instead of relative paths",
|
||||
long_help
|
||||
)]
|
||||
pub absolute_path: bool,
|
||||
|
||||
/// Overrides --absolute-path
|
||||
#[arg(long, overrides_with = "absolute_path", hide = true, action = ArgAction::SetTrue)]
|
||||
relative_path: (),
|
||||
|
||||
/// Use a detailed listing format like 'ls -l'. This is basically an alias
|
||||
/// for '--exec-batch ls -l' with some additional 'ls' options. This can be
|
||||
/// used to see more metadata, to show symlink targets and to achieve a
|
||||
/// deterministic sort order.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'l',
|
||||
conflicts_with("absolute_path"),
|
||||
help = "Use a long listing format with file metadata",
|
||||
long_help
|
||||
)]
|
||||
pub list_details: bool,
|
||||
|
||||
/// Follow symbolic links
|
||||
#[arg(
|
||||
long,
|
||||
short = 'L',
|
||||
alias = "dereference",
|
||||
long_help = "By default, fd does not descend into symlinked directories. Using this \
|
||||
flag, symbolic links are also traversed. \
|
||||
Flag can be overridden with --no-follow."
|
||||
)]
|
||||
pub follow: bool,
|
||||
|
||||
/// Overrides --follow
|
||||
#[arg(long, overrides_with = "follow", hide = true, action = ArgAction::SetTrue)]
|
||||
no_follow: (),
|
||||
|
||||
/// By default, the search pattern is only matched against the filename (or directory name). Using this flag, the pattern is matched against the full (absolute) path. Example:
|
||||
/// fd --glob -p '**/.git/config'
|
||||
#[arg(
|
||||
long,
|
||||
short = 'p',
|
||||
help = "Search full abs. path (default: filename only)",
|
||||
long_help,
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
pub full_path: bool,
|
||||
|
||||
/// Separate search results by the null character (instead of newlines).
|
||||
/// Useful for piping results to 'xargs'.
|
||||
#[arg(
|
||||
long = "print0",
|
||||
short = '0',
|
||||
conflicts_with("list_details"),
|
||||
hide_short_help = true,
|
||||
help = "Separate search results by the null character",
|
||||
long_help
|
||||
)]
|
||||
pub null_separator: bool,
|
||||
|
||||
/// Limit the directory traversal to a given depth. By default, there is no
|
||||
/// limit on the search depth.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'd',
|
||||
value_name = "depth",
|
||||
alias("maxdepth"),
|
||||
help = "Set maximum search depth (default: none)",
|
||||
long_help
|
||||
)]
|
||||
max_depth: Option<usize>,
|
||||
|
||||
/// Only show search results starting at the given depth.
|
||||
/// See also: '--max-depth' and '--exact-depth'
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "depth",
|
||||
hide_short_help = true,
|
||||
alias("mindepth"),
|
||||
help = "Only show search results starting at the given depth.",
|
||||
long_help
|
||||
)]
|
||||
min_depth: Option<usize>,
|
||||
|
||||
/// Only show search results at the exact given depth. This is an alias for
|
||||
/// '--min-depth <depth> --max-depth <depth>'.
|
||||
#[arg(long, value_name = "depth", hide_short_help = true, conflicts_with_all(&["max_depth", "min_depth"]),
|
||||
help = "Only show search results at the exact given depth",
|
||||
long_help,
|
||||
)]
|
||||
exact_depth: Option<usize>,
|
||||
|
||||
/// Exclude files/directories that match the given glob pattern. This
|
||||
/// overrides any other ignore logic. Multiple exclude patterns can be
|
||||
/// specified.
|
||||
///
|
||||
/// Examples:
|
||||
/// {n} --exclude '*.pyc'
|
||||
/// {n} --exclude node_modules
|
||||
#[arg(
|
||||
long,
|
||||
short = 'E',
|
||||
value_name = "glob",
|
||||
help = "Exclude entries that match the given glob pattern",
|
||||
long_help
|
||||
)]
|
||||
pub exclude: Vec<String>,
|
||||
|
||||
/// Do not traverse into directories that match the search criteria. If
|
||||
/// you want to exclude specific directories, use the '--exclude=…' option.
|
||||
#[arg(long, hide_short_help = true, conflicts_with_all(&["size", "exact_depth"]),
|
||||
long_help,
|
||||
)]
|
||||
pub prune: bool,
|
||||
|
||||
/// Filter the search by type:
|
||||
/// {n} 'f' or 'file': regular files
|
||||
/// {n} 'd' or 'dir' or 'directory': directories
|
||||
/// {n} 'l' or 'symlink': symbolic links
|
||||
/// {n} 's' or 'socket': socket
|
||||
/// {n} 'p' or 'pipe': named pipe (FIFO)
|
||||
/// {n} 'b' or 'block-device': block device
|
||||
/// {n} 'c' or 'char-device': character device
|
||||
/// {n}{n} 'x' or 'executable': executables
|
||||
/// {n} 'e' or 'empty': empty files or directories
|
||||
///
|
||||
/// This option can be specified more than once to include multiple file types.
|
||||
/// Searching for '--type file --type symlink' will show both regular files as
|
||||
/// well as symlinks. Note that the 'executable' and 'empty' filters work differently:
|
||||
/// '--type executable' implies '--type file' by default. And '--type empty' searches
|
||||
/// for empty files and directories, unless either '--type file' or '--type directory'
|
||||
/// is specified in addition.
|
||||
///
|
||||
/// Examples:
|
||||
/// {n} - Only search for files:
|
||||
/// {n} fd --type file …
|
||||
/// {n} fd -tf …
|
||||
/// {n} - Find both files and symlinks
|
||||
/// {n} fd --type file --type symlink …
|
||||
/// {n} fd -tf -tl …
|
||||
/// {n} - Find executable files:
|
||||
/// {n} fd --type executable
|
||||
/// {n} fd -tx
|
||||
/// {n} - Find empty files:
|
||||
/// {n} fd --type empty --type file
|
||||
/// {n} fd -te -tf
|
||||
/// {n} - Find empty directories:
|
||||
/// {n} fd --type empty --type directory
|
||||
/// {n} fd -te -td
|
||||
#[arg(
|
||||
long = "type",
|
||||
short = 't',
|
||||
value_name = "filetype",
|
||||
hide_possible_values = true,
|
||||
value_enum,
|
||||
help = "Filter by type: file (f), directory (d/dir), symlink (l), \
|
||||
executable (x), empty (e), socket (s), pipe (p), \
|
||||
char-device (c), block-device (b)",
|
||||
long_help
|
||||
)]
|
||||
pub filetype: Option<Vec<FileType>>,
|
||||
|
||||
/// Filter results by extension. By default, this matches all entry types
|
||||
/// (including directories whose names end with the extension). Use `--type`
|
||||
/// to restrict the search to specific types. Multiple extensions can be specified.
|
||||
///
|
||||
/// If you want to search for files without extension,
|
||||
/// you can use the regex '^[^.]+$' as a normal search pattern.
|
||||
#[arg(
|
||||
long = "extension",
|
||||
short = 'e',
|
||||
value_name = "ext",
|
||||
help = "Filter by extension",
|
||||
long_help
|
||||
)]
|
||||
pub extensions: Option<Vec<String>>,
|
||||
|
||||
/// Limit results based on the size of files using the format <+-><NUM><UNIT>.
|
||||
/// '+': file size must be greater than or equal to this
|
||||
/// '-': file size must be less than or equal to this
|
||||
///
|
||||
/// If neither '+' nor '-' is specified, file size must be exactly equal to this.
|
||||
/// 'NUM': The numeric size (e.g. 500)
|
||||
/// 'UNIT': The units for NUM. They are not case-sensitive.
|
||||
/// Allowed unit values:
|
||||
/// 'b': bytes
|
||||
/// 'k': kilobytes (base ten, 10^3 = 1000 bytes)
|
||||
/// 'm': megabytes
|
||||
/// 'g': gigabytes
|
||||
/// 't': terabytes
|
||||
/// 'ki': kibibytes (base two, 2^10 = 1024 bytes)
|
||||
/// 'mi': mebibytes
|
||||
/// 'gi': gibibytes
|
||||
/// 'ti': tebibytes
|
||||
#[arg(long, short = 'S', value_parser = SizeFilter::from_string, allow_hyphen_values = true, verbatim_doc_comment, value_name = "size",
|
||||
help = "Limit results based on the size of files",
|
||||
long_help,
|
||||
verbatim_doc_comment,
|
||||
)]
|
||||
pub size: Vec<SizeFilter>,
|
||||
|
||||
/// Filter results based on the file modification time. Files with modification times
|
||||
/// greater than the argument are returned. The argument can be provided
|
||||
/// as a specific point in time (YYYY-MM-DD HH:MM:SS or @timestamp) or as a duration (10h, 1d, 35min).
|
||||
/// If the time is not specified, it defaults to 00:00:00.
|
||||
/// '--change-newer-than', '--newer', or '--changed-after' can be used as aliases.
|
||||
///
|
||||
/// Examples:
|
||||
/// {n} --changed-within 2weeks
|
||||
/// {n} --change-newer-than '2018-10-27 10:00:00'
|
||||
/// {n} --newer 2018-10-27
|
||||
/// {n} --changed-after 1day
|
||||
#[arg(
|
||||
long,
|
||||
alias("change-newer-than"),
|
||||
alias("newer"),
|
||||
alias("changed-after"),
|
||||
value_name = "date|dur",
|
||||
help = "Filter by file modification time (newer than)",
|
||||
long_help
|
||||
)]
|
||||
pub changed_within: Option<String>,
|
||||
|
||||
/// Filter results based on the file modification time. Files with modification times
|
||||
/// less than the argument are returned. The argument can be provided
|
||||
/// as a specific point in time (YYYY-MM-DD HH:MM:SS or @timestamp) or as a duration (10h, 1d, 35min).
|
||||
/// '--change-older-than' or '--older' can be used as aliases.
|
||||
///
|
||||
/// Examples:
|
||||
/// {n} --changed-before '2018-10-27 10:00:00'
|
||||
/// {n} --change-older-than 2weeks
|
||||
/// {n} --older 2018-10-27
|
||||
#[arg(
|
||||
long,
|
||||
alias("change-older-than"),
|
||||
alias("older"),
|
||||
value_name = "date|dur",
|
||||
help = "Filter by file modification time (older than)",
|
||||
long_help
|
||||
)]
|
||||
pub changed_before: Option<String>,
|
||||
|
||||
/// Filter files by their user and/or group.
|
||||
/// Format: [(user|uid)][:(group|gid)]. Either side is optional.
|
||||
/// Precede either side with a '!' to exclude files instead.
|
||||
///
|
||||
/// Examples:
|
||||
/// {n} --owner john
|
||||
/// {n} --owner :students
|
||||
/// {n} --owner '!john:students'
|
||||
#[cfg(unix)]
|
||||
#[arg(long, short = 'o', value_parser = OwnerFilter::from_string, value_name = "user:group",
|
||||
help = "Filter by owning user and/or group",
|
||||
long_help,
|
||||
)]
|
||||
pub owner: Option<OwnerFilter>,
|
||||
|
||||
/// Instead of printing the file normally, print the format string with the following placeholders replaced:
|
||||
/// '{}': path (of the current search result)
|
||||
/// '{/}': basename
|
||||
/// '{//}': parent directory
|
||||
/// '{.}': path without file extension
|
||||
/// '{/.}': basename without file extension
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "fmt",
|
||||
help = "Print results according to template",
|
||||
conflicts_with = "list_details",
|
||||
allow_hyphen_values = true
|
||||
)]
|
||||
pub format: Option<String>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub exec: Exec,
|
||||
|
||||
/// Maximum number of arguments to pass to the command given with -X.
|
||||
/// If the number of results is greater than the given size,
|
||||
/// the command given with -X is run again with remaining arguments.
|
||||
/// A batch size of zero means there is no limit (default), but note
|
||||
/// that batching might still happen due to OS restrictions on the
|
||||
/// maximum length of command lines.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "size",
|
||||
hide_short_help = true,
|
||||
requires("exec_batch"),
|
||||
value_parser = value_parser!(usize),
|
||||
default_value_t,
|
||||
help = "Max number of arguments to run as a batch size with -X",
|
||||
long_help,
|
||||
)]
|
||||
pub batch_size: usize,
|
||||
|
||||
/// Add a custom ignore-file in '.gitignore' format. These files have a low precedence.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "path",
|
||||
hide_short_help = true,
|
||||
help = "Add a custom ignore-file in '.gitignore' format",
|
||||
long_help
|
||||
)]
|
||||
pub ignore_file: Vec<PathBuf>,
|
||||
|
||||
/// Declare when to use color for the pattern match output
|
||||
#[arg(
|
||||
long,
|
||||
short = 'c',
|
||||
value_enum,
|
||||
default_value_t = ColorWhen::Auto,
|
||||
value_name = "when",
|
||||
help = "When to use colors",
|
||||
long_help,
|
||||
)]
|
||||
pub color: ColorWhen,
|
||||
|
||||
/// Add a terminal hyperlink to a file:// url for each path in the output.
|
||||
///
|
||||
/// Auto mode is used if no argument is given to this option.
|
||||
///
|
||||
/// This doesn't do anything for --exec and --exec-batch.
|
||||
#[arg(
|
||||
long,
|
||||
alias = "hyper",
|
||||
value_name = "when",
|
||||
require_equals = true,
|
||||
value_enum,
|
||||
default_value_t = HyperlinkWhen::Never,
|
||||
default_missing_value = "auto",
|
||||
num_args = 0..=1,
|
||||
help = "Add hyperlinks to output paths"
|
||||
)]
|
||||
pub hyperlink: HyperlinkWhen,
|
||||
|
||||
/// Ignore directories containing the named entry.
|
||||
#[arg(long, value_name = "name")]
|
||||
pub ignore_contain: Vec<String>,
|
||||
|
||||
/// Set number of threads to use for searching & executing (default: number
|
||||
/// of available CPU cores, up to a maximum of 64)
|
||||
#[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = str::parse::<NonZeroUsize>)]
|
||||
pub threads: Option<NonZeroUsize>,
|
||||
|
||||
/// Milliseconds to buffer before streaming search results to console
|
||||
///
|
||||
/// Amount of time in milliseconds to buffer, before streaming the search
|
||||
/// results to the console.
|
||||
#[arg(long, hide = true, value_parser = parse_millis)]
|
||||
pub max_buffer_time: Option<Duration>,
|
||||
|
||||
///Limit the number of search results to 'count' and quit immediately.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "count",
|
||||
hide_short_help = true,
|
||||
overrides_with("max_one_result"),
|
||||
help = "Limit the number of search results",
|
||||
long_help
|
||||
)]
|
||||
max_results: Option<usize>,
|
||||
|
||||
/// Limit the search to a single result and quit immediately.
|
||||
/// This is an alias for '--max-results=1'.
|
||||
#[arg(
|
||||
short = '1',
|
||||
hide_short_help = true,
|
||||
overrides_with("max_results"),
|
||||
help = "Limit search to a single result",
|
||||
long_help
|
||||
)]
|
||||
max_one_result: bool,
|
||||
|
||||
/// When the flag is present, the program does not print anything and will
|
||||
/// return with an exit code of 0 if there is at least one match. Otherwise, the
|
||||
/// exit code will be 1.
|
||||
/// '--has-results' can be used as an alias.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'q',
|
||||
alias = "has-results",
|
||||
hide_short_help = true,
|
||||
conflicts_with("max_results"),
|
||||
help = "Print nothing, exit code 0 if match found, 1 otherwise",
|
||||
long_help
|
||||
)]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Enable the display of filesystem errors for situations such as
|
||||
/// insufficient permissions or dead symlinks.
|
||||
#[arg(
|
||||
long,
|
||||
hide_short_help = true,
|
||||
help = "Show filesystem errors",
|
||||
long_help
|
||||
)]
|
||||
pub show_errors: bool,
|
||||
|
||||
/// Change the current working directory of fd to the provided path. This
|
||||
/// means that search results will be shown with respect to the given base
|
||||
/// path. Note that relative paths which are passed to fd via the positional
|
||||
/// <path> argument or the '--search-path' option will also be resolved
|
||||
/// relative to this directory.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'C',
|
||||
value_name = "path",
|
||||
hide_short_help = true,
|
||||
help = "Change current working directory",
|
||||
long_help
|
||||
)]
|
||||
pub base_directory: Option<PathBuf>,
|
||||
|
||||
/// the search pattern which is either a regular expression (default) or a glob
|
||||
/// pattern (if --glob is used). If no pattern has been specified, every entry
|
||||
/// is considered a match. If your pattern starts with a dash (-), make sure to
|
||||
/// pass '--' first, or it will be considered as a flag (fd -- '-foo').
|
||||
#[arg(
|
||||
value_name = "pattern",
|
||||
help = "the search pattern (a regular expression, unless '--glob' is used; optional)",
|
||||
long_help
|
||||
)]
|
||||
pub pattern: Option<String>,
|
||||
|
||||
/// Set the path separator to use when printing file paths. The default is
|
||||
/// the OS-specific separator ('/' on Unix, '\' on Windows).
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "separator",
|
||||
hide_short_help = true,
|
||||
help = "Set path separator when printing file paths",
|
||||
long_help
|
||||
)]
|
||||
pub path_separator: Option<String>,
|
||||
|
||||
/// The directory where the filesystem search is rooted (optional). If
|
||||
/// omitted, search the current working directory.
|
||||
/// If supplied, the pattern must come first.
|
||||
#[arg(action = ArgAction::Append,
|
||||
value_name = "path",
|
||||
help = "the root directories for the filesystem search (optional, requires pattern first)",
|
||||
long_help,
|
||||
)]
|
||||
path: Vec<PathBuf>,
|
||||
|
||||
/// Provide paths to search as an alternative to the positional <path>
|
||||
/// argument. Changes the usage to `fd [OPTIONS] --search-path <path>
|
||||
/// --search-path <path2> [<pattern>]`
|
||||
#[arg(
|
||||
long,
|
||||
conflicts_with("path"),
|
||||
value_name = "search-path",
|
||||
hide_short_help = true,
|
||||
help = "Provides paths to search as an alternative to the positional <path> argument",
|
||||
long_help
|
||||
)]
|
||||
search_path: Vec<PathBuf>,
|
||||
|
||||
/// By default, relative paths are prefixed with './' when -x/--exec,
|
||||
/// -X/--exec-batch, or -0/--print0 are given, to reduce the risk of a
|
||||
/// path starting with '-' being treated as a command line option. Use
|
||||
/// this flag to change this behavior. If this flag is used without a value,
|
||||
/// it is equivalent to passing "always".
|
||||
#[arg(long, conflicts_with_all(&["path", "search_path"]), value_name = "when", hide_short_help = true, require_equals = true, long_help)]
|
||||
strip_cwd_prefix: Option<Option<StripCwdWhen>>,
|
||||
|
||||
/// By default, fd will traverse the file system tree as far as other options
|
||||
/// dictate. With this flag, fd ensures that it does not descend into a
|
||||
/// different file system than the one it started in. Comparable to the -mount
|
||||
/// or -xdev filters of find(1).
|
||||
#[cfg(any(unix, windows))]
|
||||
#[arg(long, aliases(&["mount", "xdev"]), hide_short_help = true, long_help)]
|
||||
pub one_file_system: bool,
|
||||
|
||||
#[cfg(feature = "completions")]
|
||||
#[arg(long, hide = true, exclusive = true)]
|
||||
gen_completions: Option<Option<Shell>>,
|
||||
}
|
||||
|
||||
impl Opts {
|
||||
pub fn search_paths(&self) -> anyhow::Result<Vec<PathBuf>> {
|
||||
// would it make sense to concatenate these?
|
||||
let paths = if !self.path.is_empty() {
|
||||
&self.path
|
||||
} else if !self.search_path.is_empty() {
|
||||
&self.search_path
|
||||
} else {
|
||||
let current_directory = Path::new("./");
|
||||
ensure_current_directory_exists(current_directory)?;
|
||||
return Ok(vec![self.normalize_path(current_directory)]);
|
||||
};
|
||||
Ok(paths
|
||||
.iter()
|
||||
.filter_map(|path| {
|
||||
if filesystem::is_existing_directory(path) {
|
||||
Some(self.normalize_path(path))
|
||||
} else {
|
||||
// We use debug for the path to make it more readable if it has special characters
|
||||
// Should we use a more reliable escape?
|
||||
print_error!(
|
||||
"Search path {:?} is not a directory.",
|
||||
path.to_string_lossy().as_ref()
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn normalize_path(&self, path: &Path) -> PathBuf {
|
||||
if self.absolute_path {
|
||||
filesystem::absolute_path(path.normalize().unwrap().as_path()).unwrap()
|
||||
} else if path == Path::new(".") {
|
||||
// Change "." to "./" as a workaround for https://github.com/BurntSushi/ripgrep/pull/2711
|
||||
PathBuf::from("./")
|
||||
} else if path == Path::new("-") {
|
||||
// Prefix "-" so the underlying walker treats it as a path.
|
||||
// See sharkdp/fd#849.
|
||||
Path::new(".").join(path)
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn no_search_paths(&self) -> bool {
|
||||
self.path.is_empty() && self.search_path.is_empty()
|
||||
}
|
||||
|
||||
pub fn max_depth(&self) -> Option<usize> {
|
||||
self.max_depth.or(self.exact_depth)
|
||||
}
|
||||
|
||||
pub fn min_depth(&self) -> Option<usize> {
|
||||
self.min_depth.or(self.exact_depth)
|
||||
}
|
||||
|
||||
pub fn threads(&self) -> NonZeroUsize {
|
||||
self.threads.unwrap_or_else(default_num_threads)
|
||||
}
|
||||
|
||||
pub fn max_results(&self) -> Option<usize> {
|
||||
self.max_results
|
||||
.filter(|&m| m > 0)
|
||||
.or_else(|| self.max_one_result.then_some(1))
|
||||
}
|
||||
|
||||
pub fn strip_cwd_prefix<P: FnOnce() -> bool>(&self, auto_pred: P) -> bool {
|
||||
use self::StripCwdWhen::*;
|
||||
self.no_search_paths()
|
||||
&& match self.strip_cwd_prefix.map_or(Auto, |o| o.unwrap_or(Always)) {
|
||||
Auto => auto_pred(),
|
||||
Always => true,
|
||||
Never => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "completions")]
|
||||
pub fn gen_completions(&self) -> anyhow::Result<Option<Shell>> {
|
||||
self.gen_completions
|
||||
.map(|maybe_shell| match maybe_shell {
|
||||
Some(sh) => Ok(sh),
|
||||
None => {
|
||||
Shell::from_env().ok_or_else(|| anyhow!("Unable to get shell from environment"))
|
||||
}
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default number of threads to use, if not explicitly specified.
|
||||
fn default_num_threads() -> NonZeroUsize {
|
||||
// If we can't get the amount of parallelism for some reason, then
|
||||
// default to a single thread, because that is safe.
|
||||
let fallback = NonZeroUsize::MIN;
|
||||
// To limit startup overhead on massively parallel machines, don't use more
|
||||
// than 64 threads.
|
||||
let limit = NonZeroUsize::new(64).unwrap();
|
||||
|
||||
std::thread::available_parallelism()
|
||||
.unwrap_or(fallback)
|
||||
.min(limit)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
|
||||
pub enum FileType {
|
||||
#[value(alias = "f")]
|
||||
File,
|
||||
#[value(alias = "d", alias = "dir")]
|
||||
Directory,
|
||||
#[value(alias = "l")]
|
||||
Symlink,
|
||||
#[value(alias = "b")]
|
||||
BlockDevice,
|
||||
#[value(alias = "c")]
|
||||
CharDevice,
|
||||
/// A file which is executable by the current effective user
|
||||
#[value(alias = "x")]
|
||||
Executable,
|
||||
#[value(alias = "e")]
|
||||
Empty,
|
||||
#[value(alias = "s")]
|
||||
Socket,
|
||||
#[value(alias = "p")]
|
||||
Pipe,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||
pub enum ColorWhen {
|
||||
/// show colors if the output goes to an interactive console (default)
|
||||
Auto,
|
||||
/// always use colorized output
|
||||
Always,
|
||||
/// do not use colorized output
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||
pub enum StripCwdWhen {
|
||||
/// Use the default behavior
|
||||
Auto,
|
||||
/// Always strip the ./ at the beginning of paths
|
||||
Always,
|
||||
/// Never strip the ./
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||
pub enum HyperlinkWhen {
|
||||
/// Use hyperlinks only if color is enabled
|
||||
Auto,
|
||||
/// Always use hyperlinks when printing file paths
|
||||
Always,
|
||||
/// Never use hyperlinks
|
||||
Never,
|
||||
}
|
||||
|
||||
// there isn't a derive api for getting grouped values yet,
|
||||
// so we have to use hand-rolled parsing for exec and exec-batch
|
||||
pub struct Exec {
|
||||
pub command: Option<CommandSet>,
|
||||
}
|
||||
|
||||
impl clap::FromArgMatches for Exec {
|
||||
fn from_arg_matches(matches: &ArgMatches) -> clap::error::Result<Self> {
|
||||
let command = matches
|
||||
.get_occurrences::<String>("exec")
|
||||
.map(CommandSet::new)
|
||||
.or_else(|| {
|
||||
matches
|
||||
.get_occurrences::<String>("exec_batch")
|
||||
.map(CommandSet::new_batch)
|
||||
})
|
||||
.transpose()
|
||||
.map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?;
|
||||
Ok(Self { command })
|
||||
}
|
||||
|
||||
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> clap::error::Result<()> {
|
||||
*self = Self::from_arg_matches(matches)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl clap::Args for Exec {
|
||||
fn augment_args(cmd: Command) -> Command {
|
||||
cmd.arg(Arg::new("exec")
|
||||
.action(ArgAction::Append)
|
||||
.long("exec")
|
||||
.short('x')
|
||||
.num_args(1..)
|
||||
.allow_hyphen_values(true)
|
||||
.value_terminator(";")
|
||||
.value_name("cmd")
|
||||
.conflicts_with("list_details")
|
||||
.help("Execute a command for each search result")
|
||||
.long_help(
|
||||
"Execute a command for each search result in parallel (use --threads=1 for sequential command execution). \
|
||||
The order in which different search results are processed and their output is printed is not guaranteed, even with --threads=1. \
|
||||
All positional arguments following --exec are considered to be arguments to the command - not to fd. \
|
||||
It is therefore recommended to place the '-x'/'--exec' option last. \
|
||||
Use '\\;' to terminate the command template if you need to continue passing fd arguments afterwards.\n\n\
|
||||
Specify this option multiple times to run several commands for each file or directory found. \
|
||||
For each file or directory, fd runs these commands one after another in the order they appear on the command line. \
|
||||
Terminate each command except the last with '\\;'. \
|
||||
When running in parallel, fd buffers command output and prints it together for each search result, \
|
||||
without interleaving it with command output for other results.\n\n\
|
||||
The following placeholders are substituted before the command is executed:\n \
|
||||
'{}': path (of the current search result)\n \
|
||||
'{/}': basename\n \
|
||||
'{//}': parent directory\n \
|
||||
'{.}': path without file extension\n \
|
||||
'{/.}': basename without file extension\n \
|
||||
'{{': literal '{' (for escaping)\n \
|
||||
'}}': literal '}' (for escaping)\n\n\
|
||||
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
|
||||
Examples:\n\n \
|
||||
- find all *.zip files and unzip them:\n\n \
|
||||
fd -e zip -x unzip\n\n \
|
||||
- find *.h and *.cpp files and run \"clang-format -i ..\" for each of them:\n\n \
|
||||
fd -e h -e cpp -x clang-format -i\n\n \
|
||||
- search within `src/` and echo each match (place `-x` last):\n\n \
|
||||
fd . src -x echo\n\n \
|
||||
- Convert all *.jpg files to *.png files:\n\n \
|
||||
fd -e jpg -x convert {} {.}.png\
|
||||
",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("exec_batch")
|
||||
.action(ArgAction::Append)
|
||||
.long("exec-batch")
|
||||
.short('X')
|
||||
.num_args(1..)
|
||||
.allow_hyphen_values(true)
|
||||
.value_terminator(";")
|
||||
.value_name("cmd")
|
||||
.conflicts_with_all(["exec", "list_details"])
|
||||
.help("Execute a command with all search results at once")
|
||||
.long_help(
|
||||
"Execute the given command once, with all search results as arguments.\n\
|
||||
The order of the arguments is non-deterministic, and should not be relied upon.\n\
|
||||
One of the following placeholders is substituted before the command is executed:\n \
|
||||
'{}': path (of all search results)\n \
|
||||
'{/}': basename\n \
|
||||
'{//}': parent directory\n \
|
||||
'{.}': path without file extension\n \
|
||||
'{/.}': basename without file extension\n \
|
||||
'{{': literal '{' (for escaping)\n \
|
||||
'}}': literal '}' (for escaping)\n\n\
|
||||
If no placeholder is present, an implicit \"{}\" at the end is assumed.\n\n\
|
||||
Examples:\n\n \
|
||||
- Find all test_*.py files and open them in your favorite editor:\n\n \
|
||||
fd -g 'test_*.py' -X vim\n\n \
|
||||
- Find all *.rs files and count the lines with \"wc -l ...\":\n\n \
|
||||
fd -e rs -X wc -l\
|
||||
"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn augment_args_for_update(cmd: Command) -> Command {
|
||||
Self::augment_args(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_millis(arg: &str) -> Result<Duration, std::num::ParseIntError> {
|
||||
Ok(Duration::from_millis(arg.parse()?))
|
||||
}
|
||||
|
||||
fn ensure_current_directory_exists(current_directory: &Path) -> anyhow::Result<()> {
|
||||
if filesystem::is_existing_directory(current_directory) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Could not retrieve current directory (has it been deleted?)."
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use lscolors::LsColors;
|
||||
use regex::bytes::RegexSet;
|
||||
|
||||
use crate::exec::CommandSet;
|
||||
use crate::filetypes::FileTypes;
|
||||
#[cfg(unix)]
|
||||
use crate::filter::OwnerFilter;
|
||||
use crate::filter::{SizeFilter, TimeFilter};
|
||||
use crate::fmt::FormatTemplate;
|
||||
|
||||
/// Configuration options for *fd*.
|
||||
pub struct Config {
|
||||
/// Cached current working directory for absolute path construction.
|
||||
/// Populated when `--full-path` is set; `None` means search by filename only.
|
||||
pub full_path_base: Option<PathBuf>,
|
||||
|
||||
/// Whether to ignore hidden files and directories (or not).
|
||||
pub ignore_hidden: bool,
|
||||
|
||||
/// Whether to respect `.fdignore` files or not.
|
||||
pub read_fdignore: bool,
|
||||
|
||||
/// Whether to respect ignore files in parent directories or not.
|
||||
pub read_parent_ignore: bool,
|
||||
|
||||
/// Whether to respect VCS ignore files (`.gitignore`, ..) or not.
|
||||
pub read_vcsignore: bool,
|
||||
|
||||
/// Whether to require a `.git` directory to respect gitignore files.
|
||||
pub require_git_to_read_vcsignore: bool,
|
||||
|
||||
/// Whether to respect the global ignore file or not.
|
||||
pub read_global_ignore: bool,
|
||||
|
||||
/// Whether to follow symlinks or not.
|
||||
pub follow_links: bool,
|
||||
|
||||
/// Whether to limit the search to starting file system or not.
|
||||
pub one_file_system: bool,
|
||||
|
||||
/// Whether elements of output should be separated by a null character
|
||||
pub null_separator: bool,
|
||||
|
||||
/// The maximum search depth, or `None` if no maximum search depth should be set.
|
||||
///
|
||||
/// A depth of `1` includes all files under the current directory, a depth of `2` also includes
|
||||
/// all files under subdirectories of the current directory, etc.
|
||||
pub max_depth: Option<usize>,
|
||||
|
||||
/// The minimum depth for reported entries, or `None`.
|
||||
pub min_depth: Option<usize>,
|
||||
|
||||
/// Whether to stop traversing into matching directories.
|
||||
pub prune: bool,
|
||||
|
||||
/// The number of threads to use.
|
||||
pub threads: usize,
|
||||
|
||||
/// If true, the program doesn't print anything and will instead return an exit code of 0
|
||||
/// if there's at least one match. Otherwise, the exit code will be 1.
|
||||
pub quiet: bool,
|
||||
|
||||
/// Time to buffer results internally before streaming to the console. This is useful to
|
||||
/// provide a sorted output, in case the total execution time is shorter than
|
||||
/// `max_buffer_time`.
|
||||
pub max_buffer_time: Option<Duration>,
|
||||
|
||||
/// `None` if the output should not be colorized. Otherwise, a `LsColors` instance that defines
|
||||
/// how to style different filetypes.
|
||||
pub ls_colors: Option<LsColors>,
|
||||
|
||||
/// Whether or not we are writing to an interactive terminal
|
||||
#[cfg_attr(not(unix), allow(unused))]
|
||||
pub interactive_terminal: bool,
|
||||
|
||||
/// The type of file to search for. If set to `None`, all file types are displayed. If
|
||||
/// set to `Some(..)`, only the types that are specified are shown.
|
||||
pub file_types: Option<FileTypes>,
|
||||
|
||||
/// The extension to search for. Only entries matching the extension will be included.
|
||||
///
|
||||
/// The value (if present) will be a lowercase string without leading dots.
|
||||
pub extensions: Option<RegexSet>,
|
||||
|
||||
/// A format string to use to format results, similarly to exec
|
||||
pub format: Option<FormatTemplate>,
|
||||
|
||||
/// If a value is supplied, each item found will be used to generate and execute commands.
|
||||
pub command: Option<Arc<CommandSet>>,
|
||||
|
||||
/// Maximum number of search results to pass to each `command`. If zero, the number is
|
||||
/// unlimited.
|
||||
pub batch_size: usize,
|
||||
|
||||
/// A list of glob patterns that should be excluded from the search.
|
||||
pub exclude_patterns: Vec<String>,
|
||||
|
||||
/// A list of custom ignore files.
|
||||
pub ignore_files: Vec<PathBuf>,
|
||||
|
||||
/// The given constraints on the size of returned files
|
||||
pub size_constraints: Vec<SizeFilter>,
|
||||
|
||||
/// Constraints on last modification time of files
|
||||
pub time_constraints: Vec<TimeFilter>,
|
||||
|
||||
#[cfg(unix)]
|
||||
/// User/group ownership constraint
|
||||
pub owner_constraint: Option<OwnerFilter>,
|
||||
|
||||
/// Whether or not to display filesystem errors
|
||||
pub show_filesystem_errors: bool,
|
||||
|
||||
/// The separator used to print file paths.
|
||||
pub path_separator: Option<String>,
|
||||
|
||||
/// The actual separator, either the system default separator or `path_separator`
|
||||
pub actual_path_separator: String,
|
||||
|
||||
/// The maximum number of search results
|
||||
pub max_results: Option<usize>,
|
||||
|
||||
/// Whether or not to strip the './' prefix for search results
|
||||
pub strip_cwd_prefix: bool,
|
||||
|
||||
/// Whether or not to use hyperlinks on paths
|
||||
pub hyperlink: bool,
|
||||
|
||||
/// Names that should stop traversal down their parent. (e.g. https://bford.info/cachedir/).
|
||||
pub ignore_contain: Vec<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Check whether results are being printed.
|
||||
pub fn is_printing(&self) -> bool {
|
||||
self.command.is_none()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
use std::cell::OnceCell;
|
||||
use std::ffi::OsString;
|
||||
use std::fs::{FileType, Metadata};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use lscolors::{Colorable, LsColors, Style};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::filesystem::strip_current_dir;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DirEntryInner {
|
||||
Normal(ignore::DirEntry),
|
||||
// Broken symlinks reach us as walk errors rather than entries, so we carry
|
||||
// over the depth the walker recorded on the error.
|
||||
BrokenSymlink { path: PathBuf, depth: Option<usize> },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DirEntry {
|
||||
inner: DirEntryInner,
|
||||
metadata: OnceCell<Option<Metadata>>,
|
||||
style: OnceCell<Option<Style>>,
|
||||
}
|
||||
|
||||
impl DirEntry {
|
||||
#[inline]
|
||||
pub fn normal(e: ignore::DirEntry) -> Self {
|
||||
Self {
|
||||
inner: DirEntryInner::Normal(e),
|
||||
metadata: OnceCell::new(),
|
||||
style: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn broken_symlink(path: PathBuf, depth: Option<usize>) -> Self {
|
||||
Self {
|
||||
inner: DirEntryInner::BrokenSymlink { path, depth },
|
||||
metadata: OnceCell::new(),
|
||||
style: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
match &self.inner {
|
||||
DirEntryInner::Normal(e) => e.path(),
|
||||
DirEntryInner::BrokenSymlink { path, .. } => path.as_path(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_path(self) -> PathBuf {
|
||||
match self.inner {
|
||||
DirEntryInner::Normal(e) => e.into_path(),
|
||||
DirEntryInner::BrokenSymlink { path, .. } => path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path as it should be presented to the user.
|
||||
/// When stripping `./` would leave the path starting with `-`, keep the original
|
||||
/// (with `./`) so downstream tools don't interpret it as an option.
|
||||
pub fn stripped_path(&self, config: &Config) -> &Path {
|
||||
let path = self.path();
|
||||
if config.strip_cwd_prefix {
|
||||
let stripped = strip_current_dir(path);
|
||||
if starts_with_dash(stripped) {
|
||||
path
|
||||
} else {
|
||||
stripped
|
||||
}
|
||||
} else {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path as it should be presented to the user.
|
||||
pub fn into_stripped_path(self, config: &Config) -> PathBuf {
|
||||
if config.strip_cwd_prefix {
|
||||
self.stripped_path(config).to_path_buf()
|
||||
} else {
|
||||
self.into_path()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_type(&self) -> Option<FileType> {
|
||||
match &self.inner {
|
||||
DirEntryInner::Normal(e) => e.file_type(),
|
||||
DirEntryInner::BrokenSymlink { .. } => self.metadata().map(|m| m.file_type()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metadata(&self) -> Option<&Metadata> {
|
||||
self.metadata
|
||||
.get_or_init(|| match &self.inner {
|
||||
DirEntryInner::Normal(e) => e.metadata().ok(),
|
||||
DirEntryInner::BrokenSymlink { path, .. } => path.symlink_metadata().ok(),
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub fn depth(&self) -> Option<usize> {
|
||||
match &self.inner {
|
||||
DirEntryInner::Normal(e) => Some(e.depth()),
|
||||
DirEntryInner::BrokenSymlink { depth, .. } => *depth,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(&self, ls_colors: &LsColors) -> Option<&Style> {
|
||||
self.style
|
||||
.get_or_init(|| ls_colors.style_for(self).cloned())
|
||||
.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
fn starts_with_dash(path: &Path) -> bool {
|
||||
path.as_os_str().as_encoded_bytes().first() == Some(&b'-')
|
||||
}
|
||||
|
||||
impl PartialEq for DirEntry {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.path() == other.path()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for DirEntry {}
|
||||
|
||||
impl PartialOrd for DirEntry {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for DirEntry {
|
||||
#[inline]
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.path().cmp(other.path())
|
||||
}
|
||||
}
|
||||
|
||||
impl Colorable for DirEntry {
|
||||
fn path(&self) -> PathBuf {
|
||||
self.path().to_owned()
|
||||
}
|
||||
|
||||
fn file_name(&self) -> OsString {
|
||||
let name = match &self.inner {
|
||||
DirEntryInner::Normal(e) => e.file_name(),
|
||||
DirEntryInner::BrokenSymlink { path, .. } => {
|
||||
// Path::file_name() only works if the last component is Normal,
|
||||
// but we want it for all component types, so we open code it.
|
||||
// Copied from LsColors::style_for_path_with_metadata().
|
||||
path.components()
|
||||
.next_back()
|
||||
.map(|c| c.as_os_str())
|
||||
.unwrap_or_else(|| path.as_os_str())
|
||||
}
|
||||
};
|
||||
name.to_owned()
|
||||
}
|
||||
|
||||
fn file_type(&self) -> Option<FileType> {
|
||||
self.file_type()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Option<Metadata> {
|
||||
self.metadata().cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::starts_with_dash;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn dash_prefixed_paths_detected() {
|
||||
assert!(starts_with_dash(Path::new("-rf")));
|
||||
assert!(starts_with_dash(Path::new("--delete")));
|
||||
assert!(starts_with_dash(Path::new("-")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safe_paths_not_flagged() {
|
||||
assert!(!starts_with_dash(Path::new("foo")));
|
||||
assert!(!starts_with_dash(Path::new("./foo")));
|
||||
assert!(!starts_with_dash(Path::new("sub/-rf")));
|
||||
assert!(!starts_with_dash(Path::new("")));
|
||||
assert!(!starts_with_dash(Path::new(" -rf")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use std::fmt::Display;
|
||||
use std::io;
|
||||
|
||||
use crate::sanitize::write_sanitized;
|
||||
|
||||
/// Print an error message using a format string, sanitizing any arguments if necessary
|
||||
macro_rules! print_error {
|
||||
($fmt:literal, $($arg:expr),*) => {
|
||||
eprintln!(concat!("[fd error]: ", $fmt), $($crate::error::SanitizeErr::sanitize($arg)),*)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for specifying how to sanitize a type for error display, if needed.
|
||||
pub(crate) trait SanitizeErr {
|
||||
type Sanitized: Display;
|
||||
|
||||
fn sanitize(self) -> Self::Sanitized;
|
||||
}
|
||||
|
||||
impl SanitizeErr for &str {
|
||||
type Sanitized = Self;
|
||||
|
||||
fn sanitize(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl SanitizeErr for io::Error {
|
||||
type Sanitized = SanitizedError<io::Error>;
|
||||
|
||||
fn sanitize(self) -> Self::Sanitized {
|
||||
SanitizedError(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl SanitizeErr for ignore::Error {
|
||||
type Sanitized = SanitizedError<ignore::Error>;
|
||||
|
||||
fn sanitize(self) -> Self::Sanitized {
|
||||
SanitizedError(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SanitizedError<E>(E);
|
||||
|
||||
impl<E: std::error::Error> Display for SanitizedError<E> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let raw = self.0.to_string();
|
||||
write_sanitized(f, &raw)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
|
||||
use argmax::Command;
|
||||
|
||||
use crate::exit_codes::ExitCode;
|
||||
|
||||
struct Outputs {
|
||||
stdout: Vec<u8>,
|
||||
stderr: Vec<u8>,
|
||||
}
|
||||
pub struct OutputBuffer {
|
||||
null_separator: bool,
|
||||
outputs: Vec<Outputs>,
|
||||
}
|
||||
|
||||
impl OutputBuffer {
|
||||
pub fn new(null_separator: bool) -> Self {
|
||||
Self {
|
||||
null_separator,
|
||||
outputs: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, stdout: Vec<u8>, stderr: Vec<u8>) {
|
||||
self.outputs.push(Outputs { stdout, stderr });
|
||||
}
|
||||
|
||||
fn write(self) {
|
||||
// Avoid taking the lock if there is nothing to do.
|
||||
// If null_separator is true, then we still need to write the
|
||||
// null separator, because the output may have been written directly
|
||||
// to stdout
|
||||
if self.outputs.is_empty() && !self.null_separator {
|
||||
return;
|
||||
}
|
||||
|
||||
let stdout = io::stdout();
|
||||
let stderr = io::stderr();
|
||||
|
||||
// While we hold these locks, only this thread will be able
|
||||
// to write its outputs.
|
||||
let mut stdout = stdout.lock();
|
||||
let mut stderr = stderr.lock();
|
||||
|
||||
for output in self.outputs.iter() {
|
||||
let _ = stdout.write_all(&output.stdout);
|
||||
let _ = stderr.write_all(&output.stderr);
|
||||
}
|
||||
if self.null_separator {
|
||||
// If null_separator is enabled, then we should write a \0 at the end
|
||||
// of the output for this entry
|
||||
let _ = stdout.write_all(b"\0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes a command.
|
||||
pub fn execute_commands<I: Iterator<Item = io::Result<Command>>>(
|
||||
cmds: I,
|
||||
mut output_buffer: OutputBuffer,
|
||||
enable_output_buffering: bool,
|
||||
) -> ExitCode {
|
||||
for result in cmds {
|
||||
let mut cmd = match result {
|
||||
Ok(cmd) => cmd,
|
||||
Err(e) => return handle_cmd_error(None, e),
|
||||
};
|
||||
|
||||
// Spawn the supplied command.
|
||||
let output = if enable_output_buffering {
|
||||
cmd.output()
|
||||
} else {
|
||||
// If running on only one thread, don't buffer output
|
||||
// Allows for viewing and interacting with intermediate command output
|
||||
cmd.spawn().and_then(|c| c.wait_with_output())
|
||||
};
|
||||
|
||||
// Then wait for the command to exit, if it was spawned.
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if enable_output_buffering {
|
||||
output_buffer.push(output.stdout, output.stderr);
|
||||
}
|
||||
if output.status.code() != Some(0) {
|
||||
output_buffer.write();
|
||||
return ExitCode::GeneralError;
|
||||
}
|
||||
}
|
||||
Err(why) => {
|
||||
output_buffer.write();
|
||||
return handle_cmd_error(Some(&cmd), why);
|
||||
}
|
||||
}
|
||||
}
|
||||
output_buffer.write();
|
||||
ExitCode::Success
|
||||
}
|
||||
|
||||
pub fn handle_cmd_error(cmd: Option<&Command>, err: io::Error) -> ExitCode {
|
||||
match (cmd, err) {
|
||||
(Some(cmd), err) if err.kind() == io::ErrorKind::NotFound => {
|
||||
print_error!(
|
||||
"Command not found: {:?}",
|
||||
cmd.get_program().to_string_lossy().as_ref()
|
||||
);
|
||||
ExitCode::GeneralError
|
||||
}
|
||||
(_, err) => {
|
||||
print_error!("Problem while executing command: {}", err);
|
||||
ExitCode::GeneralError
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use crate::config::Config;
|
||||
use crate::exit_codes::{ExitCode, merge_exitcodes};
|
||||
use crate::walk::WorkerResult;
|
||||
|
||||
use super::CommandSet;
|
||||
|
||||
/// An event loop that listens for inputs from the `rx` receiver. Each received input will
|
||||
/// generate a command with the supplied command template. The generated command will then
|
||||
/// be executed, and this process will continue until the receiver's sender has closed.
|
||||
pub fn job(
|
||||
results: impl IntoIterator<Item = WorkerResult>,
|
||||
cmd: &CommandSet,
|
||||
config: &Config,
|
||||
) -> ExitCode {
|
||||
// Output should be buffered when only running a single thread
|
||||
let buffer_output: bool = config.threads > 1;
|
||||
|
||||
let mut ret = ExitCode::Success;
|
||||
for result in results {
|
||||
// Obtain the next result from the receiver, else if the channel
|
||||
// has closed, exit from the loop
|
||||
let dir_entry = match result {
|
||||
WorkerResult::Entry(dir_entry) => dir_entry,
|
||||
WorkerResult::Error(err) => {
|
||||
if config.show_filesystem_errors {
|
||||
print_error!("{}", err);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Generate a command, execute it and store its exit code.
|
||||
let code = cmd.execute(
|
||||
dir_entry.stripped_path(config),
|
||||
config.path_separator.as_deref(),
|
||||
config.null_separator,
|
||||
buffer_output,
|
||||
);
|
||||
ret = merge_exitcodes([ret, code]);
|
||||
}
|
||||
// Returns error in case of any error.
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn batch(
|
||||
results: impl IntoIterator<Item = WorkerResult>,
|
||||
cmd: &CommandSet,
|
||||
config: &Config,
|
||||
) -> ExitCode {
|
||||
let paths = results
|
||||
.into_iter()
|
||||
.filter_map(|worker_result| match worker_result {
|
||||
WorkerResult::Entry(dir_entry) => Some(dir_entry.into_stripped_path(config)),
|
||||
WorkerResult::Error(err) => {
|
||||
if config.show_filesystem_errors {
|
||||
print_error!("{}", err);
|
||||
}
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
cmd.execute_batch(paths, config.batch_size, config.path_separator.as_deref())
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
mod command;
|
||||
mod job;
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::io;
|
||||
use std::iter;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use argmax::Command;
|
||||
|
||||
use crate::exec::command::OutputBuffer;
|
||||
use crate::exit_codes::{ExitCode, merge_exitcodes};
|
||||
use crate::fmt::{FormatTemplate, Token};
|
||||
|
||||
use self::command::{execute_commands, handle_cmd_error};
|
||||
pub use self::job::{batch, job};
|
||||
|
||||
/// Execution mode of the command
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ExecutionMode {
|
||||
/// Command is executed for each search result
|
||||
OneByOne,
|
||||
/// Command is run for a batch of results at once
|
||||
Batch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CommandSet {
|
||||
mode: ExecutionMode,
|
||||
commands: Vec<CommandTemplate>,
|
||||
}
|
||||
|
||||
impl CommandSet {
|
||||
pub fn new<I, T, S>(input: I) -> Result<Self>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
Ok(Self {
|
||||
mode: ExecutionMode::OneByOne,
|
||||
commands: input
|
||||
.into_iter()
|
||||
.map(|args| CommandTemplate::new(args, ExecutionMode::OneByOne))
|
||||
.collect::<Result<_>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_batch<I, T, S>(input: I) -> Result<Self>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
Ok(Self {
|
||||
mode: ExecutionMode::Batch,
|
||||
commands: input
|
||||
.into_iter()
|
||||
.map(|args| {
|
||||
let cmd = CommandTemplate::new(args, ExecutionMode::Batch)?;
|
||||
if cmd.number_of_tokens() > 1 {
|
||||
bail!("Only one placeholder allowed for batch commands");
|
||||
}
|
||||
Ok(cmd)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn in_batch_mode(&self) -> bool {
|
||||
self.mode == ExecutionMode::Batch
|
||||
}
|
||||
|
||||
pub fn execute(
|
||||
&self,
|
||||
input: &Path,
|
||||
path_separator: Option<&str>,
|
||||
null_separator: bool,
|
||||
buffer_output: bool,
|
||||
) -> ExitCode {
|
||||
let commands = self
|
||||
.commands
|
||||
.iter()
|
||||
.map(|c| c.generate(input, path_separator));
|
||||
execute_commands(commands, OutputBuffer::new(null_separator), buffer_output)
|
||||
}
|
||||
|
||||
pub fn execute_batch<I>(&self, paths: I, limit: usize, path_separator: Option<&str>) -> ExitCode
|
||||
where
|
||||
I: Iterator<Item = PathBuf>,
|
||||
{
|
||||
let builders: io::Result<Vec<_>> = self
|
||||
.commands
|
||||
.iter()
|
||||
.map(|c| CommandBuilder::new(c, limit))
|
||||
.collect();
|
||||
|
||||
match builders {
|
||||
Ok(mut builders) => {
|
||||
for path in paths {
|
||||
for builder in &mut builders {
|
||||
if let Err(e) = builder.push(&path, path_separator) {
|
||||
return handle_cmd_error(Some(&builder.cmd), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for builder in &mut builders {
|
||||
if let Err(e) = builder.finish() {
|
||||
return handle_cmd_error(Some(&builder.cmd), e);
|
||||
}
|
||||
}
|
||||
|
||||
merge_exitcodes(builders.iter().map(|b| b.exit_code()))
|
||||
}
|
||||
Err(e) => handle_cmd_error(None, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a multi-exec command as it is built.
|
||||
#[derive(Debug)]
|
||||
struct CommandBuilder {
|
||||
pre_args: Vec<OsString>,
|
||||
path_arg: FormatTemplate,
|
||||
post_args: Vec<OsString>,
|
||||
cmd: Command,
|
||||
count: usize,
|
||||
limit: usize,
|
||||
exit_code: ExitCode,
|
||||
}
|
||||
|
||||
impl CommandBuilder {
|
||||
fn new(template: &CommandTemplate, limit: usize) -> io::Result<Self> {
|
||||
let mut pre_args = vec![];
|
||||
let mut path_arg = None;
|
||||
let mut post_args = vec![];
|
||||
|
||||
for arg in &template.args {
|
||||
if arg.has_tokens() {
|
||||
path_arg = Some(arg.clone());
|
||||
} else if path_arg.is_none() {
|
||||
pre_args.push(arg.generate("", None));
|
||||
} else {
|
||||
post_args.push(arg.generate("", None));
|
||||
}
|
||||
}
|
||||
|
||||
let cmd = Self::new_command(&pre_args)?;
|
||||
|
||||
Ok(Self {
|
||||
pre_args,
|
||||
path_arg: path_arg.unwrap(),
|
||||
post_args,
|
||||
cmd,
|
||||
count: 0,
|
||||
limit,
|
||||
exit_code: ExitCode::Success,
|
||||
})
|
||||
}
|
||||
|
||||
fn new_command(pre_args: &[OsString]) -> io::Result<Command> {
|
||||
let mut cmd = Command::new(&pre_args[0]);
|
||||
cmd.stdin(Stdio::inherit());
|
||||
cmd.stdout(Stdio::inherit());
|
||||
cmd.stderr(Stdio::inherit());
|
||||
cmd.try_args(&pre_args[1..])?;
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
fn push(&mut self, path: &Path, separator: Option<&str>) -> io::Result<()> {
|
||||
if self.limit > 0 && self.count >= self.limit {
|
||||
self.finish()?;
|
||||
}
|
||||
|
||||
let arg = self.path_arg.generate(path, separator);
|
||||
if !self
|
||||
.cmd
|
||||
.args_would_fit(iter::once(&arg).chain(&self.post_args))
|
||||
{
|
||||
self.finish()?;
|
||||
}
|
||||
|
||||
self.cmd.try_arg(arg)?;
|
||||
self.count += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish(&mut self) -> io::Result<()> {
|
||||
if self.count > 0 {
|
||||
self.cmd.try_args(&self.post_args)?;
|
||||
if !self.cmd.status()?.success() {
|
||||
self.exit_code = ExitCode::GeneralError;
|
||||
}
|
||||
|
||||
self.cmd = Self::new_command(&self.pre_args)?;
|
||||
self.count = 0;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exit_code(&self) -> ExitCode {
|
||||
self.exit_code
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a template that is utilized to generate command strings.
|
||||
///
|
||||
/// The template is meant to be coupled with an input in order to generate a command. The
|
||||
/// `generate_and_execute()` method will be used to generate a command and execute it.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct CommandTemplate {
|
||||
args: Vec<FormatTemplate>,
|
||||
}
|
||||
|
||||
impl CommandTemplate {
|
||||
fn new<I, S>(input: I, mode: ExecutionMode) -> Result<Self>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let mut args = Vec::new();
|
||||
let mut has_placeholder = false;
|
||||
|
||||
for arg in input {
|
||||
let arg = arg.as_ref();
|
||||
|
||||
let tmpl = FormatTemplate::parse(arg);
|
||||
has_placeholder |= tmpl.has_tokens();
|
||||
args.push(tmpl);
|
||||
}
|
||||
|
||||
// We need to check that we have at least one argument, because if not
|
||||
// it will try to execute each file and directory it finds.
|
||||
//
|
||||
// Sadly, clap can't currently handle this for us, see
|
||||
// https://github.com/clap-rs/clap/issues/3542
|
||||
if args.is_empty() {
|
||||
bail!("No executable provided for --exec or --exec-batch");
|
||||
}
|
||||
|
||||
// A placeholder as the executable is meaningful for `--exec` but never for `--exec-batch`.
|
||||
if mode == ExecutionMode::Batch && args[0].has_tokens() {
|
||||
bail!("First argument of --exec-batch must be a fixed executable, not a placeholder");
|
||||
}
|
||||
|
||||
// If a placeholder token was not supplied, append one at the end of the command.
|
||||
if !has_placeholder {
|
||||
args.push(FormatTemplate::Tokens(vec![Token::Placeholder]));
|
||||
}
|
||||
|
||||
Ok(Self { args })
|
||||
}
|
||||
|
||||
fn number_of_tokens(&self) -> usize {
|
||||
self.args.iter().filter(|arg| arg.has_tokens()).count()
|
||||
}
|
||||
|
||||
/// Generates and executes a command.
|
||||
///
|
||||
/// Using the internal `args` field, and a supplied `input` variable, a `Command` will be
|
||||
/// build.
|
||||
fn generate(&self, input: &Path, path_separator: Option<&str>) -> io::Result<Command> {
|
||||
let mut cmd = Command::new(self.args[0].generate(input, path_separator));
|
||||
for arg in &self.args[1..] {
|
||||
cmd.try_arg(arg.generate(input, path_separator))?;
|
||||
}
|
||||
Ok(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn generate_str(template: &CommandTemplate, input: &str) -> Vec<String> {
|
||||
template
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg| arg.generate(input, None).into_string().unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_with_placeholder() {
|
||||
assert_eq!(
|
||||
CommandSet::new(vec![vec![&"echo", &"${SHELL}:"]]).unwrap(),
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Text("${SHELL}:".into()),
|
||||
FormatTemplate::Tokens(vec![Token::Placeholder]),
|
||||
]
|
||||
}],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_with_no_extension() {
|
||||
assert_eq!(
|
||||
CommandSet::new(vec![vec!["echo", "{.}"]]).unwrap(),
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::NoExt]),
|
||||
],
|
||||
}],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_with_basename() {
|
||||
assert_eq!(
|
||||
CommandSet::new(vec![vec!["echo", "{/}"]]).unwrap(),
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::Basename]),
|
||||
],
|
||||
}],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_with_parent() {
|
||||
assert_eq!(
|
||||
CommandSet::new(vec![vec!["echo", "{//}"]]).unwrap(),
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::Parent]),
|
||||
],
|
||||
}],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_with_basename_no_extension() {
|
||||
assert_eq!(
|
||||
CommandSet::new(vec![vec!["echo", "{/.}"]]).unwrap(),
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::BasenameNoExt]),
|
||||
],
|
||||
}],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_with_literal_braces() {
|
||||
let template =
|
||||
CommandTemplate::new(vec!["{{}}", "{{", "{.}}"], ExecutionMode::OneByOne).unwrap();
|
||||
assert_eq!(
|
||||
generate_str(&template, "foo"),
|
||||
vec!["{}", "{", "{.}", "foo"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_with_literal_braces_and_placeholder() {
|
||||
let template =
|
||||
CommandTemplate::new(vec!["echo", "{{{},end}"], ExecutionMode::OneByOne).unwrap();
|
||||
assert_eq!(generate_str(&template, "foo"), vec!["echo", "{foo,end}"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_multiple() {
|
||||
assert_eq!(
|
||||
CommandSet::new(vec![vec!["cp", "{}", "{/.}.ext"]]).unwrap(),
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
FormatTemplate::Text("cp".into()),
|
||||
FormatTemplate::Tokens(vec![Token::Placeholder]),
|
||||
FormatTemplate::Tokens(vec![
|
||||
Token::BasenameNoExt,
|
||||
Token::Text(".ext".into())
|
||||
]),
|
||||
],
|
||||
}],
|
||||
mode: ExecutionMode::OneByOne,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_single_batch() {
|
||||
assert_eq!(
|
||||
CommandSet::new_batch(vec![vec!["echo", "{.}"]]).unwrap(),
|
||||
CommandSet {
|
||||
commands: vec![CommandTemplate {
|
||||
args: vec![
|
||||
FormatTemplate::Text("echo".into()),
|
||||
FormatTemplate::Tokens(vec![Token::NoExt]),
|
||||
],
|
||||
}],
|
||||
mode: ExecutionMode::Batch,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_multiple_batch() {
|
||||
assert!(CommandSet::new_batch(vec![vec!["echo", "{.}", "{}"]]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn template_no_args() {
|
||||
assert!(
|
||||
CommandTemplate::new::<Vec<_>, &'static str>(vec![], ExecutionMode::OneByOne).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_set_no_args() {
|
||||
assert!(CommandSet::new(vec![vec!["echo"], vec![]]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_as_executable_rejected() {
|
||||
assert!(CommandSet::new(vec![vec!["{}"]]).is_ok());
|
||||
assert!(CommandSet::new(vec![vec!["{/}", "arg"]]).is_ok());
|
||||
assert!(CommandSet::new_batch(vec![vec!["{}"]]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_custom_path_separator() {
|
||||
let arg = FormatTemplate::Tokens(vec![Token::Placeholder]);
|
||||
macro_rules! check {
|
||||
($input:expr, $expected:expr) => {
|
||||
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
|
||||
};
|
||||
}
|
||||
|
||||
check!("foo", "foo");
|
||||
check!("foo/bar", "foo#bar");
|
||||
check!("/foo/bar/baz", "#foo#bar#baz");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn generate_custom_path_separator_windows() {
|
||||
let arg = FormatTemplate::Tokens(vec![Token::Placeholder]);
|
||||
macro_rules! check {
|
||||
($input:expr, $expected:expr) => {
|
||||
assert_eq!(arg.generate($input, Some("#")), OsString::from($expected));
|
||||
};
|
||||
}
|
||||
|
||||
// path starting with a drive letter
|
||||
check!(r"C:\foo\bar", "C:#foo#bar");
|
||||
// UNC path
|
||||
check!(r"\\server\share\path", "##server#share#path");
|
||||
// Drive Relative path - no separator after the colon omits the RootDir path component.
|
||||
// This is uncommon, but valid
|
||||
check!(r"C:foo\bar", "C:foo#bar");
|
||||
|
||||
// forward slashes should get normalized and interpreted as separators
|
||||
check!("C:/foo/bar", "C:#foo#bar");
|
||||
check!("C:foo/bar", "C:foo#bar");
|
||||
|
||||
// Rust does not interpret "//server/share" as a UNC path, but rather as a normal
|
||||
// absolute path that begins with RootDir, and the two slashes get combined together as
|
||||
// a single path separator during normalization.
|
||||
//check!("//server/share/path", "##server#share#path");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use std::process;
|
||||
|
||||
#[cfg(unix)]
|
||||
use nix::sys::signal::{SigHandler, Signal, raise, signal};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ExitCode {
|
||||
Success,
|
||||
HasResults(bool),
|
||||
GeneralError,
|
||||
KilledBySigint,
|
||||
}
|
||||
|
||||
impl From<ExitCode> for i32 {
|
||||
fn from(code: ExitCode) -> Self {
|
||||
match code {
|
||||
ExitCode::Success => 0,
|
||||
ExitCode::HasResults(has_results) => !has_results as Self,
|
||||
ExitCode::GeneralError => 1,
|
||||
ExitCode::KilledBySigint => 130,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExitCode {
|
||||
fn is_error(self) -> bool {
|
||||
i32::from(self) != 0
|
||||
}
|
||||
|
||||
/// Exit the process with the appropriate code.
|
||||
pub fn exit(self) -> ! {
|
||||
#[cfg(unix)]
|
||||
if self == Self::KilledBySigint {
|
||||
// Get rid of the SIGINT handler, if present, and raise SIGINT
|
||||
unsafe {
|
||||
if signal(Signal::SIGINT, SigHandler::SigDfl).is_ok() {
|
||||
let _ = raise(Signal::SIGINT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process::exit(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge_exitcodes(results: impl IntoIterator<Item = ExitCode>) -> ExitCode {
|
||||
if results.into_iter().any(ExitCode::is_error) {
|
||||
return ExitCode::GeneralError;
|
||||
}
|
||||
ExitCode::Success
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn success_when_no_results() {
|
||||
assert_eq!(merge_exitcodes([]), ExitCode::Success);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn general_error_if_at_least_one_error() {
|
||||
assert_eq!(
|
||||
merge_exitcodes([ExitCode::GeneralError]),
|
||||
ExitCode::GeneralError
|
||||
);
|
||||
assert_eq!(
|
||||
merge_exitcodes([ExitCode::KilledBySigint]),
|
||||
ExitCode::GeneralError
|
||||
);
|
||||
assert_eq!(
|
||||
merge_exitcodes([ExitCode::KilledBySigint, ExitCode::Success]),
|
||||
ExitCode::GeneralError
|
||||
);
|
||||
assert_eq!(
|
||||
merge_exitcodes([ExitCode::Success, ExitCode::GeneralError]),
|
||||
ExitCode::GeneralError
|
||||
);
|
||||
assert_eq!(
|
||||
merge_exitcodes([ExitCode::GeneralError, ExitCode::KilledBySigint]),
|
||||
ExitCode::GeneralError
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_if_no_error() {
|
||||
assert_eq!(merge_exitcodes([ExitCode::Success]), ExitCode::Success);
|
||||
assert_eq!(
|
||||
merge_exitcodes([ExitCode::Success, ExitCode::Success]),
|
||||
ExitCode::Success
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use normpath::PathExt;
|
||||
|
||||
use crate::dir_entry;
|
||||
|
||||
pub fn path_absolute_form(path: &Path) -> io::Result<PathBuf> {
|
||||
if path.is_absolute() {
|
||||
return Ok(path.to_path_buf());
|
||||
}
|
||||
|
||||
let path = path.strip_prefix(".").unwrap_or(path);
|
||||
env::current_dir().map(|path_buf| path_buf.join(path))
|
||||
}
|
||||
|
||||
pub fn absolute_path(path: &Path) -> io::Result<PathBuf> {
|
||||
let path_buf = path_absolute_form(path)?;
|
||||
|
||||
#[cfg(windows)]
|
||||
let path_buf = strip_verbatim_prefix(&path_buf);
|
||||
|
||||
Ok(path_buf)
|
||||
}
|
||||
|
||||
/// Remove the `\\?\` verbatim prefix from a path without turning it into a
|
||||
/// relative path.
|
||||
///
|
||||
/// The prefix is dropped by rewriting the path *component by component*, so
|
||||
/// that the meaning of the remaining components is preserved:
|
||||
///
|
||||
/// * `\\?\C:\foo` -> `C:\foo` (verbatim disk path)
|
||||
/// * `\\?\UNC\server\share\foo` -> `\\server\share\foo` (verbatim UNC path)
|
||||
///
|
||||
/// Verbatim paths that cannot be expressed without the prefix (for example
|
||||
/// `\\?\Volume{GUID}\foo`, where `Volume{GUID}` is a device name and not a
|
||||
/// relative file name) are returned unchanged, and so are all non-verbatim
|
||||
/// paths such as `\\server\share\foo` or `\\.\COM1`.
|
||||
///
|
||||
/// Unlike the previous string trimming, this never goes through
|
||||
/// `to_string_lossy()`, so paths that are not valid UTF-8 survive unchanged.
|
||||
#[cfg(windows)]
|
||||
fn strip_verbatim_prefix(path: &Path) -> PathBuf {
|
||||
use std::ffi::OsString;
|
||||
use std::path::{Component, Prefix};
|
||||
|
||||
let mut components = path.components();
|
||||
let Some(Component::Prefix(prefix)) = components.next() else {
|
||||
return path.to_path_buf();
|
||||
};
|
||||
|
||||
let has_root = matches!(components.clone().next(), Some(Component::RootDir));
|
||||
let mut base = OsString::new();
|
||||
// `Prefix::Disk` / `Prefix::UNC` cannot rebuild this prefix: a `Prefix` is a
|
||||
// parse label, not the bytes of the path, and `PrefixComponent` has no stable
|
||||
// constructor. Writing it out by hand is the only way to drop `\\?\`.
|
||||
match prefix.kind() {
|
||||
Prefix::VerbatimDisk(drive) => {
|
||||
base.push(format!("{}:", drive as char));
|
||||
if has_root {
|
||||
base.push(r"\");
|
||||
}
|
||||
}
|
||||
Prefix::VerbatimUNC(server, share) => {
|
||||
base.push(r"\\");
|
||||
base.push(server);
|
||||
base.push(r"\");
|
||||
base.push(share);
|
||||
if has_root {
|
||||
base.push(r"\");
|
||||
}
|
||||
}
|
||||
// `Prefix::Verbatim(_)` (device paths like `\\?\Volume{GUID}`) and
|
||||
// `Prefix::DeviceNS(_)` cannot be written without their prefix.
|
||||
_ => return path.to_path_buf(),
|
||||
}
|
||||
|
||||
if has_root {
|
||||
// The root separator is already part of `base`, and `join` would append
|
||||
// it again; skipping the root component keeps the prefix we just built
|
||||
// instead of letting `join` replace it.
|
||||
let _ = components.next();
|
||||
}
|
||||
if components.as_path().is_empty() {
|
||||
// `\\?\C:` and `\\?\UNC\server\share`: the verbatim form is the whole
|
||||
// path, and joining an empty tail would append a separator the input
|
||||
// did not have.
|
||||
return PathBuf::from(base);
|
||||
}
|
||||
Path::new(&base).join(components)
|
||||
}
|
||||
|
||||
pub fn is_existing_directory(path: &Path) -> bool {
|
||||
// Note: we do not use `.exists()` here, as `.` always exists, even if
|
||||
// the CWD has been deleted.
|
||||
path.is_dir() && (path.file_name().is_some() || path.normalize().is_ok())
|
||||
}
|
||||
|
||||
pub fn is_empty(entry: &dir_entry::DirEntry) -> bool {
|
||||
if let Some(file_type) = entry.file_type() {
|
||||
if file_type.is_dir() {
|
||||
if let Ok(mut entries) = fs::read_dir(entry.path()) {
|
||||
entries.next().is_none()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else if file_type.is_file() {
|
||||
entry.metadata().map(|m| m.len() == 0).unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
pub fn is_block_device(ft: fs::FileType) -> bool {
|
||||
ft.is_block_device()
|
||||
}
|
||||
|
||||
#[cfg(any(windows, target_os = "wasi"))]
|
||||
pub fn is_block_device(_: fs::FileType) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
pub fn is_char_device(ft: fs::FileType) -> bool {
|
||||
ft.is_char_device()
|
||||
}
|
||||
|
||||
#[cfg(any(windows, target_os = "wasi"))]
|
||||
pub fn is_char_device(_: fs::FileType) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
pub fn is_socket(ft: fs::FileType) -> bool {
|
||||
ft.is_socket()
|
||||
}
|
||||
|
||||
#[cfg(any(windows, target_os = "wasi"))]
|
||||
pub fn is_socket(_: fs::FileType) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
pub fn is_pipe(ft: fs::FileType) -> bool {
|
||||
ft.is_fifo()
|
||||
}
|
||||
|
||||
#[cfg(any(windows, target_os = "wasi"))]
|
||||
pub fn is_pipe(_: fs::FileType) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Remove the `./` prefix from a path.
|
||||
pub fn strip_current_dir(path: &Path) -> &Path {
|
||||
path.strip_prefix(".").unwrap_or(path)
|
||||
}
|
||||
|
||||
/// Default value for the path_separator, mainly for MSYS/MSYS2, which set the MSYSTEM
|
||||
/// environment variable, and we set fd's path separator to '/' rather than Rust's default of '\'.
|
||||
///
|
||||
/// Returns Some to use a nonstandard path separator, or None to use rust's default on the target
|
||||
/// platform.
|
||||
pub fn default_path_separator() -> Option<String> {
|
||||
if cfg!(windows) {
|
||||
let msystem = env::var("MSYSTEM").ok()?;
|
||||
if !msystem.is_empty() {
|
||||
return Some("/".to_owned());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::strip_current_dir;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn strip_current_dir_basic() {
|
||||
assert_eq!(strip_current_dir(Path::new("./foo")), Path::new("foo"));
|
||||
assert_eq!(strip_current_dir(Path::new("foo")), Path::new("foo"));
|
||||
assert_eq!(
|
||||
strip_current_dir(Path::new("./foo/bar/baz")),
|
||||
Path::new("foo/bar/baz")
|
||||
);
|
||||
assert_eq!(
|
||||
strip_current_dir(Path::new("foo/bar/baz")),
|
||||
Path::new("foo/bar/baz")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, windows))]
|
||||
mod windows_tests {
|
||||
use super::{absolute_path, strip_verbatim_prefix};
|
||||
use std::ffi::OsString;
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[test]
|
||||
fn strip_verbatim_prefix_verbatim_disk() {
|
||||
assert_eq!(
|
||||
strip_verbatim_prefix(Path::new(r"\\?\C:\foo\bar")),
|
||||
Path::new(r"C:\foo\bar")
|
||||
);
|
||||
// A bare drive root keeps its root separator.
|
||||
assert_eq!(
|
||||
strip_verbatim_prefix(Path::new(r"\\?\C:\")),
|
||||
Path::new(r"C:\")
|
||||
);
|
||||
// Drive-relative verbatim path (`\\?\C:`) must not gain a root.
|
||||
assert_eq!(
|
||||
strip_verbatim_prefix(Path::new(r"\\?\C:")),
|
||||
Path::new(r"C:")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_verbatim_prefix_verbatim_unc() {
|
||||
let stripped = strip_verbatim_prefix(Path::new(r"\\?\UNC\server\share\foo"));
|
||||
assert_eq!(stripped, Path::new(r"\\server\share\foo"));
|
||||
// The whole point of the bug: the result must stay absolute.
|
||||
assert!(stripped.is_absolute());
|
||||
assert_eq!(
|
||||
stripped.components().next(),
|
||||
Path::new(r"\\server\share\foo").components().next()
|
||||
);
|
||||
|
||||
// The shape produced by the old `trim_start_matches(r"\\?\")` code.
|
||||
assert_ne!(stripped, Path::new(r"UNC\server\share\foo"));
|
||||
assert_ne!(stripped, Path::new(r"\server\share\foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_verbatim_prefix_verbatim_unc_without_trailing_path() {
|
||||
assert_eq!(
|
||||
strip_verbatim_prefix(Path::new(r"\\?\UNC\server\share")),
|
||||
Path::new(r"\\server\share")
|
||||
);
|
||||
// ... and keeps the exact bytes: joining an empty tail would have
|
||||
// appended a separator the input did not have.
|
||||
assert_eq!(
|
||||
strip_verbatim_prefix(Path::new(r"\\?\UNC\server\share"))
|
||||
.as_os_str()
|
||||
.as_encoded_bytes(),
|
||||
Path::new(r"\\server\share").as_os_str().as_encoded_bytes()
|
||||
);
|
||||
assert_eq!(
|
||||
strip_verbatim_prefix(Path::new(r"\\?\UNC\server\share\")),
|
||||
Path::new(r"\\server\share\")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_verbatim_prefix_leaves_other_paths_alone() {
|
||||
// Verbatim device paths cannot be expressed without the prefix.
|
||||
let volume = Path::new(r"\\?\Volume{2b1e7a6b-1f2c-4c66-8c47-9b0e0e2f3a11}\foo");
|
||||
assert_eq!(strip_verbatim_prefix(volume), volume);
|
||||
let pipe = Path::new(r"\\?\pipe\fd");
|
||||
assert_eq!(strip_verbatim_prefix(pipe), pipe);
|
||||
|
||||
// Non-verbatim paths are untouched.
|
||||
for path in [
|
||||
r"C:\foo\bar",
|
||||
r"\\server\share\foo",
|
||||
r"\\.\COM1",
|
||||
r"C:",
|
||||
r"foo\bar",
|
||||
] {
|
||||
assert_eq!(strip_verbatim_prefix(Path::new(path)), Path::new(path));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_path_keeps_unc_absolute() {
|
||||
assert_eq!(
|
||||
absolute_path(Path::new(r"\\?\UNC\server\share\foo")).unwrap(),
|
||||
Path::new(r"\\server\share\foo")
|
||||
);
|
||||
assert_eq!(
|
||||
absolute_path(Path::new(r"\\?\C:\foo")).unwrap(),
|
||||
Path::new(r"C:\foo")
|
||||
);
|
||||
assert_eq!(
|
||||
absolute_path(Path::new(r"\\server\share\foo")).unwrap(),
|
||||
Path::new(r"\\server\share\foo")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_path_preserves_non_utf8_paths() {
|
||||
// A lone surrogate is not valid UTF-8 but is a valid UTF-16 code unit,
|
||||
// and therefore a valid Windows path component. `to_string_lossy()`
|
||||
// used to replace it with U+FFFD.
|
||||
let mut wide: Vec<u16> = r"\\?\D:\".encode_utf16().collect();
|
||||
wide.push(0xD800);
|
||||
wide.extend("x".encode_utf16());
|
||||
let path = PathBuf::from(OsString::from_wide(&wide));
|
||||
|
||||
let expected = {
|
||||
let mut wide: Vec<u16> = r"D:\".encode_utf16().collect();
|
||||
wide.push(0xD800);
|
||||
wide.extend("x".encode_utf16());
|
||||
PathBuf::from(OsString::from_wide(&wide))
|
||||
};
|
||||
assert_eq!(absolute_path(&path).unwrap(), expected);
|
||||
assert_eq!(
|
||||
absolute_path(&path).unwrap().as_os_str().as_encoded_bytes(),
|
||||
expected.as_os_str().as_encoded_bytes()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
use crate::cli::FileType;
|
||||
use crate::dir_entry;
|
||||
use crate::filesystem;
|
||||
|
||||
use faccess::PathExt;
|
||||
|
||||
/// Whether or not to show
|
||||
#[derive(Default, Debug, PartialEq, Eq)]
|
||||
pub struct FileTypes {
|
||||
pub files: bool,
|
||||
pub directories: bool,
|
||||
pub symlinks: bool,
|
||||
pub block_devices: bool,
|
||||
pub char_devices: bool,
|
||||
pub sockets: bool,
|
||||
pub pipes: bool,
|
||||
pub executables_only: bool,
|
||||
pub empty_only: bool,
|
||||
}
|
||||
|
||||
impl FileTypes {
|
||||
pub fn from_types(types: &[FileType]) -> Self {
|
||||
let mut file_types = FileTypes::default();
|
||||
for value in types {
|
||||
match value {
|
||||
FileType::File => file_types.files = true,
|
||||
FileType::Directory => file_types.directories = true,
|
||||
FileType::Symlink => file_types.symlinks = true,
|
||||
FileType::Executable => {
|
||||
file_types.executables_only = true;
|
||||
file_types.files = true;
|
||||
}
|
||||
FileType::Empty => file_types.empty_only = true,
|
||||
FileType::BlockDevice => file_types.block_devices = true,
|
||||
FileType::CharDevice => file_types.char_devices = true,
|
||||
FileType::Socket => file_types.sockets = true,
|
||||
FileType::Pipe => file_types.pipes = true,
|
||||
}
|
||||
}
|
||||
|
||||
// If only 'empty' was specified, search for both files and directories:
|
||||
if file_types.empty_only && !(file_types.files || file_types.directories) {
|
||||
file_types.files = true;
|
||||
file_types.directories = true;
|
||||
}
|
||||
|
||||
file_types
|
||||
}
|
||||
|
||||
pub fn should_ignore(&self, entry: &dir_entry::DirEntry) -> bool {
|
||||
if let Some(ref entry_type) = entry.file_type() {
|
||||
(!self.files && entry_type.is_file())
|
||||
|| (!self.directories && entry_type.is_dir())
|
||||
|| (!self.symlinks && entry_type.is_symlink())
|
||||
|| (!self.block_devices && filesystem::is_block_device(*entry_type))
|
||||
|| (!self.char_devices && filesystem::is_char_device(*entry_type))
|
||||
|| (!self.sockets && filesystem::is_socket(*entry_type))
|
||||
|| (!self.pipes && filesystem::is_pipe(*entry_type))
|
||||
|| (self.executables_only && !entry.path().executable())
|
||||
|| (self.empty_only && !filesystem::is_empty(entry))
|
||||
|| !(entry_type.is_file()
|
||||
|| entry_type.is_dir()
|
||||
|| entry_type.is_symlink()
|
||||
|| filesystem::is_block_device(*entry_type)
|
||||
|| filesystem::is_char_device(*entry_type)
|
||||
|| filesystem::is_socket(*entry_type)
|
||||
|| filesystem::is_pipe(*entry_type))
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_only_implies_files_and_directories() {
|
||||
let ft = FileTypes::from_types(&[FileType::Empty]);
|
||||
assert!(ft.empty_only);
|
||||
assert!(ft.files);
|
||||
assert!(ft.directories);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_with_explicit_file_keeps_directories_off() {
|
||||
let ft = FileTypes::from_types(&[FileType::Empty, FileType::File]);
|
||||
assert!(ft.empty_only);
|
||||
assert!(ft.files);
|
||||
assert!(!ft.directories);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executable_implies_files() {
|
||||
let ft = FileTypes::from_types(&[FileType::Executable]);
|
||||
assert!(ft.executables_only);
|
||||
assert!(ft.files);
|
||||
assert!(!ft.directories);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_of_multiple_types() {
|
||||
let ft = FileTypes::from_types(&[FileType::File, FileType::Symlink]);
|
||||
assert!(ft.files);
|
||||
assert!(ft.symlinks);
|
||||
assert!(!ft.directories);
|
||||
assert!(!ft.executables_only);
|
||||
assert!(!ft.empty_only);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
pub use self::size::SizeFilter;
|
||||
pub use self::time::TimeFilter;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub use self::owner::OwnerFilter;
|
||||
|
||||
mod size;
|
||||
mod time;
|
||||
|
||||
#[cfg(unix)]
|
||||
mod owner;
|
||||
@@ -0,0 +1,140 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use nix::unistd::{Group, User};
|
||||
use std::fs;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct OwnerFilter {
|
||||
uid: Check<u32>,
|
||||
gid: Check<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Check<T> {
|
||||
Equal(T),
|
||||
NotEq(T),
|
||||
Ignore,
|
||||
}
|
||||
|
||||
impl OwnerFilter {
|
||||
const IGNORE: Self = Self {
|
||||
uid: Check::Ignore,
|
||||
gid: Check::Ignore,
|
||||
};
|
||||
|
||||
/// Parses an owner constraint
|
||||
/// Returns an error if the string is invalid
|
||||
/// Returns Ok(None) when string is acceptable but a noop (such as "" or ":")
|
||||
pub fn from_string(input: &str) -> Result<Self> {
|
||||
let mut it = input.split(':');
|
||||
let (fst, snd) = (it.next(), it.next());
|
||||
|
||||
if it.next().is_some() {
|
||||
return Err(anyhow!(
|
||||
"more than one ':' present in owner string '{}'. See 'fd --help'.",
|
||||
input
|
||||
));
|
||||
}
|
||||
|
||||
let uid = Check::parse(fst, |s| {
|
||||
if let Ok(uid) = s.parse() {
|
||||
Ok(uid)
|
||||
} else {
|
||||
User::from_name(s)?
|
||||
.map(|user| user.uid.as_raw())
|
||||
.ok_or_else(|| anyhow!("'{}' is not a recognized user name", s))
|
||||
}
|
||||
})?;
|
||||
let gid = Check::parse(snd, |s| {
|
||||
if let Ok(gid) = s.parse() {
|
||||
Ok(gid)
|
||||
} else {
|
||||
Group::from_name(s)?
|
||||
.map(|group| group.gid.as_raw())
|
||||
.ok_or_else(|| anyhow!("'{}' is not a recognized group name", s))
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(Self { uid, gid })
|
||||
}
|
||||
|
||||
/// If self is a no-op (ignore both uid and gid) then return `None`, otherwise wrap in a `Some`
|
||||
pub fn filter_ignore(self) -> Option<Self> {
|
||||
if self == Self::IGNORE {
|
||||
None
|
||||
} else {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(&self, md: &fs::Metadata) -> bool {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
self.uid.check(md.uid()) && self.gid.check(md.gid())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> Check<T> {
|
||||
fn check(&self, v: T) -> bool {
|
||||
match self {
|
||||
Self::Equal(x) => v == *x,
|
||||
Self::NotEq(x) => v != *x,
|
||||
Self::Ignore => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse<F>(s: Option<&str>, f: F) -> Result<Self>
|
||||
where
|
||||
F: Fn(&str) -> Result<T>,
|
||||
{
|
||||
let (s, equality) = match s {
|
||||
Some("") | None => return Ok(Self::Ignore),
|
||||
Some(s) if s.starts_with('!') => (&s[1..], false),
|
||||
Some(s) => (s, true),
|
||||
};
|
||||
|
||||
f(s).map(|x| {
|
||||
if equality {
|
||||
Self::Equal(x)
|
||||
} else {
|
||||
Self::NotEq(x)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod owner_parsing {
|
||||
use super::OwnerFilter;
|
||||
|
||||
macro_rules! owner_tests {
|
||||
($($name:ident: $value:expr => $result:pat,)*) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() {
|
||||
let o = OwnerFilter::from_string($value);
|
||||
match o {
|
||||
$result => {},
|
||||
_ => panic!("{:?} does not match {}", o, stringify!($result)),
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
use super::Check::*;
|
||||
owner_tests! {
|
||||
empty: "" => Ok(OwnerFilter::IGNORE),
|
||||
uid_only: "5" => Ok(OwnerFilter { uid: Equal(5), gid: Ignore }),
|
||||
uid_gid: "9:3" => Ok(OwnerFilter { uid: Equal(9), gid: Equal(3) }),
|
||||
gid_only: ":8" => Ok(OwnerFilter { uid: Ignore, gid: Equal(8) }),
|
||||
colon_only: ":" => Ok(OwnerFilter::IGNORE),
|
||||
trailing: "5:" => Ok(OwnerFilter { uid: Equal(5), gid: Ignore }),
|
||||
|
||||
uid_negate: "!5" => Ok(OwnerFilter { uid: NotEq(5), gid: Ignore }),
|
||||
both_negate:"!4:!3" => Ok(OwnerFilter { uid: NotEq(4), gid: NotEq(3) }),
|
||||
uid_not_gid:"6:!8" => Ok(OwnerFilter { uid: Equal(6), gid: NotEq(8) }),
|
||||
|
||||
more_colons:"3:5:" => Err(_),
|
||||
only_colons:"::" => Err(_),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use regex::Regex;
|
||||
|
||||
static SIZE_CAPTURES: OnceLock<Regex> = OnceLock::new();
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SizeFilter {
|
||||
Max(u64),
|
||||
Min(u64),
|
||||
Equals(u64),
|
||||
}
|
||||
|
||||
// SI prefixes (powers of 10)
|
||||
const KILO: u64 = 1000;
|
||||
const MEGA: u64 = KILO * 1000;
|
||||
const GIGA: u64 = MEGA * 1000;
|
||||
const TERA: u64 = GIGA * 1000;
|
||||
|
||||
// Binary prefixes (powers of 2)
|
||||
const KIBI: u64 = 1024;
|
||||
const MEBI: u64 = KIBI * 1024;
|
||||
const GIBI: u64 = MEBI * 1024;
|
||||
const TEBI: u64 = GIBI * 1024;
|
||||
|
||||
impl SizeFilter {
|
||||
pub fn from_string(s: &str) -> anyhow::Result<Self> {
|
||||
Self::parse_opt(s)
|
||||
.ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", s))
|
||||
}
|
||||
|
||||
fn parse_opt(s: &str) -> Option<Self> {
|
||||
let pattern =
|
||||
SIZE_CAPTURES.get_or_init(|| Regex::new(r"(?i)^([+-]?)(\d+)(b|[kmgt]i?b?)$").unwrap());
|
||||
if !pattern.is_match(s) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let captures = pattern.captures(s)?;
|
||||
let limit_kind = captures.get(1).map_or("+", |m| m.as_str());
|
||||
let quantity = captures
|
||||
.get(2)
|
||||
.and_then(|v| v.as_str().parse::<u64>().ok())?;
|
||||
|
||||
let multiplier = match &captures.get(3).map_or("b", |m| m.as_str()).to_lowercase()[..] {
|
||||
v if v.starts_with("ki") => KIBI,
|
||||
v if v.starts_with('k') => KILO,
|
||||
v if v.starts_with("mi") => MEBI,
|
||||
v if v.starts_with('m') => MEGA,
|
||||
v if v.starts_with("gi") => GIBI,
|
||||
v if v.starts_with('g') => GIGA,
|
||||
v if v.starts_with("ti") => TEBI,
|
||||
v if v.starts_with('t') => TERA,
|
||||
"b" => 1,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let size = quantity.checked_mul(multiplier)?;
|
||||
|
||||
match limit_kind {
|
||||
"+" => Some(Self::Min(size)),
|
||||
"-" => Some(Self::Max(size)),
|
||||
"" => Some(Self::Equals(size)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_within(&self, size: u64) -> bool {
|
||||
match *self {
|
||||
Self::Max(limit) => size <= limit,
|
||||
Self::Min(limit) => size >= limit,
|
||||
Self::Equals(limit) => size == limit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
macro_rules! gen_size_filter_parse_test {
|
||||
($($name: ident: $val: expr,)*) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() {
|
||||
let (txt, expected) = $val;
|
||||
let actual = SizeFilter::from_string(txt).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
// Parsing and size conversion tests data. Ensure that each type gets properly interpreted.
|
||||
// Call with higher base values to ensure expected multiplication (only need a couple)
|
||||
gen_size_filter_parse_test! {
|
||||
byte_plus: ("+1b", SizeFilter::Min(1)),
|
||||
byte_plus_multiplier: ("+10b", SizeFilter::Min(10)),
|
||||
byte_minus: ("-1b", SizeFilter::Max(1)),
|
||||
kilo_plus: ("+1k", SizeFilter::Min(1000)),
|
||||
kilo_plus_suffix: ("+1kb", SizeFilter::Min(1000)),
|
||||
kilo_minus: ("-1k", SizeFilter::Max(1000)),
|
||||
kilo_minus_multiplier: ("-100k", SizeFilter::Max(100_000)),
|
||||
kilo_minus_suffix: ("-1kb", SizeFilter::Max(1000)),
|
||||
kilo_plus_upper: ("+1K", SizeFilter::Min(1000)),
|
||||
kilo_plus_suffix_upper: ("+1KB", SizeFilter::Min(1000)),
|
||||
kilo_minus_upper: ("-1K", SizeFilter::Max(1000)),
|
||||
kilo_minus_suffix_upper: ("-1Kb", SizeFilter::Max(1000)),
|
||||
kibi_plus: ("+1ki", SizeFilter::Min(1024)),
|
||||
kibi_plus_multiplier: ("+10ki", SizeFilter::Min(10_240)),
|
||||
kibi_plus_suffix: ("+1kib", SizeFilter::Min(1024)),
|
||||
kibi_minus: ("-1ki", SizeFilter::Max(1024)),
|
||||
kibi_minus_multiplier: ("-100ki", SizeFilter::Max(102_400)),
|
||||
kibi_minus_suffix: ("-1kib", SizeFilter::Max(1024)),
|
||||
kibi_plus_upper: ("+1KI", SizeFilter::Min(1024)),
|
||||
kibi_plus_suffix_upper: ("+1KiB", SizeFilter::Min(1024)),
|
||||
kibi_minus_upper: ("-1Ki", SizeFilter::Max(1024)),
|
||||
kibi_minus_suffix_upper: ("-1KIB", SizeFilter::Max(1024)),
|
||||
mega_plus: ("+1m", SizeFilter::Min(1_000_000)),
|
||||
mega_plus_suffix: ("+1mb", SizeFilter::Min(1_000_000)),
|
||||
mega_minus: ("-1m", SizeFilter::Max(1_000_000)),
|
||||
mega_minus_suffix: ("-1mb", SizeFilter::Max(1_000_000)),
|
||||
mega_plus_upper: ("+1M", SizeFilter::Min(1_000_000)),
|
||||
mega_plus_suffix_upper: ("+1MB", SizeFilter::Min(1_000_000)),
|
||||
mega_minus_upper: ("-1M", SizeFilter::Max(1_000_000)),
|
||||
mega_minus_suffix_upper: ("-1Mb", SizeFilter::Max(1_000_000)),
|
||||
mebi_plus: ("+1mi", SizeFilter::Min(1_048_576)),
|
||||
mebi_plus_suffix: ("+1mib", SizeFilter::Min(1_048_576)),
|
||||
mebi_minus: ("-1mi", SizeFilter::Max(1_048_576)),
|
||||
mebi_minus_suffix: ("-1mib", SizeFilter::Max(1_048_576)),
|
||||
mebi_plus_upper: ("+1MI", SizeFilter::Min(1_048_576)),
|
||||
mebi_plus_suffix_upper: ("+1MiB", SizeFilter::Min(1_048_576)),
|
||||
mebi_minus_upper: ("-1Mi", SizeFilter::Max(1_048_576)),
|
||||
mebi_minus_suffix_upper: ("-1MIB", SizeFilter::Max(1_048_576)),
|
||||
giga_plus: ("+1g", SizeFilter::Min(1_000_000_000)),
|
||||
giga_plus_suffix: ("+1gb", SizeFilter::Min(1_000_000_000)),
|
||||
giga_minus: ("-1g", SizeFilter::Max(1_000_000_000)),
|
||||
giga_minus_suffix: ("-1gb", SizeFilter::Max(1_000_000_000)),
|
||||
giga_plus_upper: ("+1G", SizeFilter::Min(1_000_000_000)),
|
||||
giga_plus_suffix_upper: ("+1GB", SizeFilter::Min(1_000_000_000)),
|
||||
giga_minus_upper: ("-1G", SizeFilter::Max(1_000_000_000)),
|
||||
giga_minus_suffix_upper: ("-1Gb", SizeFilter::Max(1_000_000_000)),
|
||||
gibi_plus: ("+1gi", SizeFilter::Min(1_073_741_824)),
|
||||
gibi_plus_suffix: ("+1gib", SizeFilter::Min(1_073_741_824)),
|
||||
gibi_minus: ("-1gi", SizeFilter::Max(1_073_741_824)),
|
||||
gibi_minus_suffix: ("-1gib", SizeFilter::Max(1_073_741_824)),
|
||||
gibi_plus_upper: ("+1GI", SizeFilter::Min(1_073_741_824)),
|
||||
gibi_plus_suffix_upper: ("+1GiB", SizeFilter::Min(1_073_741_824)),
|
||||
gibi_minus_upper: ("-1Gi", SizeFilter::Max(1_073_741_824)),
|
||||
gibi_minus_suffix_upper: ("-1GIB", SizeFilter::Max(1_073_741_824)),
|
||||
tera_plus: ("+1t", SizeFilter::Min(1_000_000_000_000)),
|
||||
tera_plus_suffix: ("+1tb", SizeFilter::Min(1_000_000_000_000)),
|
||||
tera_minus: ("-1t", SizeFilter::Max(1_000_000_000_000)),
|
||||
tera_minus_suffix: ("-1tb", SizeFilter::Max(1_000_000_000_000)),
|
||||
tera_plus_upper: ("+1T", SizeFilter::Min(1_000_000_000_000)),
|
||||
tera_plus_suffix_upper: ("+1TB", SizeFilter::Min(1_000_000_000_000)),
|
||||
tera_minus_upper: ("-1T", SizeFilter::Max(1_000_000_000_000)),
|
||||
tera_minus_suffix_upper: ("-1Tb", SizeFilter::Max(1_000_000_000_000)),
|
||||
tebi_plus: ("+1ti", SizeFilter::Min(1_099_511_627_776)),
|
||||
tebi_plus_suffix: ("+1tib", SizeFilter::Min(1_099_511_627_776)),
|
||||
tebi_minus: ("-1ti", SizeFilter::Max(1_099_511_627_776)),
|
||||
tebi_minus_suffix: ("-1tib", SizeFilter::Max(1_099_511_627_776)),
|
||||
tebi_plus_upper: ("+1TI", SizeFilter::Min(1_099_511_627_776)),
|
||||
tebi_plus_suffix_upper: ("+1TiB", SizeFilter::Min(1_099_511_627_776)),
|
||||
tebi_minus_upper: ("-1Ti", SizeFilter::Max(1_099_511_627_776)),
|
||||
tebi_minus_suffix_upper: ("-1TIB", SizeFilter::Max(1_099_511_627_776)),
|
||||
}
|
||||
|
||||
/// Invalid parse testing
|
||||
macro_rules! gen_size_filter_failure {
|
||||
($($name:ident: $value:expr,)*) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() {
|
||||
let i = SizeFilter::from_string($value);
|
||||
assert!(i.is_err());
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
// Invalid parse data
|
||||
gen_size_filter_failure! {
|
||||
ensure_missing_number_returns_none: "+g",
|
||||
ensure_missing_unit_returns_none: "+18",
|
||||
ensure_bad_format_returns_none_1: "$10M",
|
||||
ensure_bad_format_returns_none_2: "badval",
|
||||
ensure_bad_format_returns_none_3: "9999",
|
||||
ensure_invalid_unit_returns_none_1: "+50a",
|
||||
ensure_invalid_unit_returns_none_2: "-10v",
|
||||
ensure_invalid_unit_returns_none_3: "+1Mv",
|
||||
ensure_bib_format_returns_none: "+1bib",
|
||||
ensure_bb_format_returns_none: "+1bb",
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_within_less_than() {
|
||||
let f = SizeFilter::from_string("-1k").unwrap();
|
||||
assert!(f.is_within(999));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_within_less_than_equal() {
|
||||
let f = SizeFilter::from_string("-1k").unwrap();
|
||||
assert!(f.is_within(1000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_within_greater_than() {
|
||||
let f = SizeFilter::from_string("+1k").unwrap();
|
||||
assert!(f.is_within(1001));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_within_greater_than_equal() {
|
||||
let f = SizeFilter::from_string("+1K").unwrap();
|
||||
assert!(f.is_within(1000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_size_multiplication_overflow_returns_error() {
|
||||
let invalid = [
|
||||
"+18446744073709551615k",
|
||||
"-18446744073709551615m",
|
||||
"18446744073709551615g",
|
||||
"+18446744073709551615ti",
|
||||
"-18446744073709551615tib",
|
||||
];
|
||||
|
||||
for input in invalid {
|
||||
assert!(
|
||||
SizeFilter::from_string(input).is_err(),
|
||||
"Expected {input:?} to fail due to overflow"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_size_multiplication_boundary() {
|
||||
let max_valid = u64::MAX / KILO;
|
||||
|
||||
assert_eq!(
|
||||
SizeFilter::from_string(&format!("{max_valid}k")).unwrap(),
|
||||
SizeFilter::Equals(max_valid * KILO)
|
||||
);
|
||||
|
||||
assert!(SizeFilter::from_string(&format!("{}k", max_valid + 1)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_max_u64_does_not_overflow() {
|
||||
assert_eq!(
|
||||
SizeFilter::from_string("18446744073709551615b").unwrap(),
|
||||
SizeFilter::Equals(u64::MAX)
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user