diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 357ffeb..f0c34e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: - name: Create Release uses: softprops/action-gh-release@v1 with: - name: cpx v${{ steps.get_version.outputs.version }} + name: copy v${{ steps.get_version.outputs.version }} generate_release_notes: true build-release: @@ -34,20 +34,20 @@ jobs: include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu - artifact_name: cpx - asset_name: cpx-linux-x86_64 + artifact_name: copy + asset_name: copy-linux-x86_64 - os: ubuntu-latest target: x86_64-unknown-linux-musl - artifact_name: cpx - asset_name: cpx-linux-x86_64-musl + artifact_name: copy + asset_name: copy-linux-x86_64-musl - os: ubuntu-latest target: aarch64-unknown-linux-gnu - artifact_name: cpx - asset_name: cpx-linux-aarch64 + artifact_name: copy + asset_name: copy-linux-aarch64 - os: ubuntu-latest target: armv7-unknown-linux-gnueabihf - artifact_name: cpx - asset_name: cpx-linux-armv7 + artifact_name: copy + asset_name: copy-linux-armv7 steps: - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index ea8c4bf..388a1c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ /target + +# Nix build outputs +/result +/result-* + +# Guix vendored crate sources (generated by `cargo vendor guix/vendor`) +/guix/vendor diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ac5e910 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,84 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +`copy` is a modern, parallel `cp` replacement for Linux, written in Rust (edition 2024). Explicit binary `[[bin]] name = "copy"` (`src/main.rs`) plus a library (`src/lib.rs`, crate name `copy`) that holds all the logic so it can be unit-tested. The binary is a thin shell: parse args → validate → dispatch to `core::copy`. + +This is a fork: `origin` is `github.com/UnbreakableMJ/copy`, `upstream` is `github.com/11happy/cpx`. License is MIT (not the umbrella GPL/Standard posture — this crate predates and lives outside the Spacecraft Software Standard; do not retrofit Standard headers/posture files here unless asked). + +## Commands + +```bash +cargo build # debug build +cargo build --release # optimized binary at target/release/copy +cargo test # all unit + integration tests +cargo test test_copy_single_file # run one test by name +cargo test --test intergration # integration suite only (note: file is mis-spelled "intergration.rs") +cargo clippy -- -D warnings # CI gate: warnings are errors +cargo fmt -- --check # CI gate: formatting (rustfmt.toml uses defaults) +cargo run -- -r src_dir/ dst_dir/ # run locally; args after `--` go to copy +``` + +CI (`.github/workflows/ci.yml`) runs exactly: build → test → `clippy -D warnings` → `fmt --check`. All four must pass. + +The `selinux-support` feature is **off by default** and gates the `selinux` dependency: +```bash +cargo build --features selinux-support +``` + +### GNU compatibility tests + +`tests/gnu/*.sh` are standalone shell scripts (reimplementations of coreutils `cp` tests) that invoke a `copy` binary on `PATH` — they are **not** wired into `cargo test`. To run one, build first and put the binary on PATH: +```bash +cargo build --release +PATH="$PWD/target/release:$PATH" sh tests/gnu/abuse.sh +``` + +## Architecture + +Two-phase pipeline: **plan, then execute.** Nothing is copied until a complete `CopyPlan` is built; this is what makes parallelism, resume, and conflict-detection tractable. + +``` +main.rs → installs SIGINT/SIGTERM handler that flips an AtomicBool ("abort"), + then calls copy() / multiple_copy() +cli/args.rs → CLIArgs::parse (copy is the root action; `config` subcommand intercepted), + validate() → (sources, destination, CopyOptions) +config/ → layered config load, merged BEFORE cli overrides +utility/preprocess → walks the tree (jwalk), builds CopyPlan (files/dirs/symlinks/hardlinks) +core/copy.rs → execute_copy(): rayon thread pool fans plan.files out to copy_core() +core/fast_copy.rs → Linux copy_file_range() zero-copy fast path +utility/preserve → applies mode/ownership/timestamps/xattr/ACL/SELinux after each copy +``` + +### Options precedence (in `args.rs::validate`) +`CopyOptions::none()` defaults → overlaid by `from_config()` if a config file applies → overlaid by `apply_cli_overrides(&mut options, &CLIArgs)` (CLI always wins) → `validate_conflicts()`. The two constructors (`none`, `from_config`) must stay in sync when you add a field to `CopyOptions` — there is no derive doing this. The unit tests in `args.rs` construct `CLIArgs` with every field listed explicitly, so adding a copy field touches those tests too. + +### CLI structure (flattened: copy is root, `config` is the only subcommand) +Copy is the **root** command — `copy ` — so `CLIArgs` holds the copy args directly, plus `command: Option` for the `config` subcommand (`copy config show`). Because `sources` is a greedy variadic that would swallow `config show` as two paths, `CLIArgs::parse()` special-cases a **leading** `config` argument and routes it through the dedicated `ConfigInvocation` parser (then `with_config()` builds a config-mode `CLIArgs`); everything else is parsed as a copy invocation. `validate()` runs the config subcommand and exits when `command` is `Some`, otherwise returns the copy inputs. The `command` field is still declared on `CLIArgs` so `--help` lists `config`. Consequence: a source literally named `config` as the first argument must be written `./config`. + +### CopyPlan construction (`preprocess.rs`) +`preprocess_file` / `preprocess_directory` / `preprocess_multiple` produce a `CopyPlan`. Key invariants: +- **Last source wins:** `add_file_with_inode` calls `remove_existing_task` first, so a later source targeting the same destination replaces earlier file/symlink/hardlink tasks (prevents symlink write-through bugs — see `tests/gnu/abuse.sh`). +- **Resume:** when `options.resume`, `should_skip_file` compares xxh3 (`Xxh3`) checksums of source vs existing destination and marks the file skipped instead of queuing it. +- **Hard-link preservation** (`preserve.links`): files with `nlink > 1` are grouped by inode in `inode_groups`; at execution a `HardLinkTracker` (behind a `Mutex`) creates a hardlink for the 2nd+ member instead of re-copying. +- Mutually exclusive routing: a `FileTask` becomes a symlink, hardlink, resume-skip, or real copy — decided here, not at execution time. + +### Execution & concurrency (`core/copy.rs::execute_copy`) +- Directories are created sequentially first, then files are copied in parallel via a `rayon::ThreadPoolBuilder` sized to `options.parallel` (`-j`). +- `--interactive` forces a **sequential** path (prompts can't be parallel). +- Per file, `copy_core` tries in order: hardlink-tracker shortcut → reflink (`reflink_copy`) → `fast_copy` (Linux `copy_file_range`) → buffered read/write fallback. Buffer size scales with file size (64 KiB → 2 MiB). +- **Cooperative cancellation:** every copy loop polls `options.abort` (the AtomicBool from `main.rs`); on abort it deletes the partial destination and returns `io::ErrorKind::Interrupted`. `main.rs` maps that to exit code **130** and prints the `--resume` hint. Other errors → exit **1**. +- Errors from parallel workers are collected, not fail-fast; up to 3 are printed. + +### Errors (`error.rs`) +Domain-specific enums (`CopyError`, `ConfigError`, `ExcludeError`, `PreserveError`) each with a `*Result` alias, funneled into the top-level `CliError`. Add new failure modes to the matching enum rather than stringly-typing. + +## Conventions + +- **Platform:** Linux-first. Linux-only code (`fast_copy`, `copy_file_range`) is behind `#[cfg(target_os = "linux")]`; unix-only metadata behind `#[cfg(unix)]`. Keep new platform-specific code gated so the crate still type-checks elsewhere. +- **Edition 2024** idioms are used heavily — `let ... &&` let-chains in `if`, `std::fs::exists`. Match the surrounding style. +- Two README files: `README.md` (GitHub) and `README_CRATES.md` (the `readme =` in `Cargo.toml`, shown on crates.io). Update both when user-facing behavior changes. +- User-facing docs live in `docs/` (`configuration.md`, `examples.md`, `benchmarks.md`); benchmark scripts/results in `benchmarks/`. +- **Packaging:** `flake.nix` + `nix/package.nix` (Nix; `nix/package.nix` is the single `callPackage` derivation shared by the flake package and `overlays.default`, built from a filtered local `src` with `cargoLock.lockFile = ../Cargo.lock`), `packaging/aur/PKGBUILD` + `.SRCINFO` (Arch, build-from-source), and `guix.scm` (GNU Guix; builds offline against a locally vendored `guix/vendor` dir — run `cargo vendor guix/vendor` first, it's gitignored). When bumping the crate version, update `version` in `nix/package.nix`, `pkgver` in the PKGBUILD/`.SRCINFO`, and `version` in `guix.scm` to match `Cargo.toml`. Regenerate `.SRCINFO` with `makepkg --printsrcinfo`. Note: Guix needs `rust` ≥ 1.85 for edition 2024. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index da3d3b1..9f41ea1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,23 +1,23 @@ -# Contributing to CPX +# Contributing to Copy -Thanks for your interest in making CPX better! 🎉 +Thanks for your interest in making Copy better! 🎉 ## Ways to Contribute ### 🐛 Report Bugs Found a bug? Help us fix it: -1. Check [existing issues](https://github.com/11happy/cpx/issues) first +1. Check [existing issues](https://github.com/UnbreakableMJ/copy/issues) first 2. Open a new issue with: - What you expected to happen - What actually happened - Steps to reproduce - - Your OS and CPX version (`cpx --version`) + - Your OS and Copy version (`copy --version`) ### 💡 Request Features Have an idea? We'd love to hear it: -1. Check [existing issues](https://github.com/11happy/cpx/issues) for similar requests +1. Check [existing issues](https://github.com/UnbreakableMJ/copy/issues) for similar requests 2. Open a new issue describing: - The feature you want - Why it would be useful @@ -36,8 +36,8 @@ Ready to contribute code? Great! #### Setup ```bash # Fork and clone -git clone https://github.com/11happy/cpx -cd cpx +git clone https://github.com/UnbreakableMJ/copy +cd copy # Build and test cargo build diff --git a/Cargo.lock b/Cargo.lock index 15bc0ef..f62fd93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ dependencies = [ ] [[package]] -name = "cpx" +name = "copy" version = "0.1.4" dependencies = [ "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index 28334c2..c85ef6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "cpx" +name = "copy" description = "A modern, fast file copying tool" -repository = "https://github.com/11happy/cpx" +repository = "https://github.com/UnbreakableMJ/copy" readme = "README_CRATES.md" keywords = ["copy", "file", "cli", "backup", "utility"] version = "0.1.4" @@ -9,6 +9,10 @@ authors = ["Bhuminjay Soni "] license = "MIT" edition = "2024" +[[bin]] +name = "copy" +path = "src/main.rs" + [dependencies] clap = { version = "4.5.53", features = ["derive"] } filetime = "0.2.26" diff --git a/README.md b/README.md index 3771751..13d56d7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# cpx +# copy
**A modern, fast file copy tool for Linux with progress bars, resume capability, and more.** -[![Crates.io](https://img.shields.io/crates/v/cpx.svg)](https://crates.io/crates/cpx) +[![Crates.io](https://img.shields.io/crates/v/copy.svg)](https://crates.io/crates/copy) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE-MIT) -[![CI](https://github.com/11happy/cpx/actions/workflows/ci.yml/badge.svg)](https://github.com/11happy/cpx/actions/workflows/ci.yml) +[![CI](https://github.com/UnbreakableMJ/copy/actions/workflows/ci.yml/badge.svg)](https://github.com/UnbreakableMJ/copy/actions/workflows/ci.yml) [Features](#features) • @@ -18,9 +18,9 @@ --- -## Why cpx? +## Why copy? -`cpx` is a modern replacement for the traditional `cp` command, built with Rust for maximum performance and safety on Linux systems. +`copy` is a modern replacement for the traditional `cp` command, built with Rust for maximum performance and safety on Linux systems. ![one](https://github.com/user-attachments/assets/85fdbe39-2635-41b0-a00a-27ba7d2e8e60) @@ -41,68 +41,80 @@ ### Quick Install ```bash -curl -fsSL https://raw.githubusercontent.com/11happy/cpx/main/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/UnbreakableMJ/copy/main/install.sh | bash ``` Or with wget: ```bash -wget -qO- https://raw.githubusercontent.com/11happy/cpx/main/install.sh | bash +wget -qO- https://raw.githubusercontent.com/UnbreakableMJ/copy/main/install.sh | bash ``` ### From Crates.io ```bash -cargo install cpx +cargo install copy ``` ### Arch Linux (AUR) -> Added by community +A first-party PKGBUILD lives in [`packaging/aur/`](packaging/aur/). Build and install it from a checkout: ```bash -yay -S cpx-copy +cd packaging/aur && makepkg -si ``` +> An official AUR upload will track this PKGBUILD. (The older community `cpx-copy` package predates the rename.) ### Nix / NixOS -> Added by community +This repo is a flake. Run or install `copy` straight from GitHub: ```bash -nix-shell -p cpx +nix run github:UnbreakableMJ/copy -- --help +nix profile install github:UnbreakableMJ/copy ``` +For a dev shell with the Rust toolchain, run `nix develop`. + + +### GNU Guix +A [`guix.scm`](guix.scm) is provided. Guix builds offline, so vendor the crates once, then build: +```bash +cargo vendor guix/vendor +guix build -f guix.scm +``` +(Needs a Guix `rust` ≥ 1.85 for edition 2024.) ### From Source ```bash -cargo install --git https://github.com/11happy/cpx -cpx --version +cargo install --git https://github.com/UnbreakableMJ/copy +copy --version ``` ### Pre-built Binaries -Download from [Releases](https://github.com/11happy/cpx/releases) +Download from [Releases](https://github.com/UnbreakableMJ/copy/releases) ## Quick Start ### Basic Usage ```bash # Copy a file -cpx source.txt dest.txt +copy source.txt dest.txt # Copy directory recursively -cpx -r source_dir/ dest_dir/ +copy -r source_dir/ dest_dir/ # exclude build artifacts -cpx -r -e "node_modules" -e ".git" -e "target" my-project/ /backup/ +copy -r -e "node_modules" -e ".git" -e "target" my-project/ /backup/ # Resume interrupted transfer -cpx -r --resume large_dataset/ /backup/ +copy -r --resume large_dataset/ /backup/ # Copy with full attribute preservation -cpx -r -p=all photos/ /backup/photos/ +copy -r -p=all photos/ /backup/photos/ ``` **See [examples.md](docs/examples.md) for detailed workflows and real-world scenarios.** ## Key Options ``` -cpx [OPTIONS] ... +copy [OPTIONS] ... Arguments: ... Source file(s) or directory(ies) @@ -152,28 +164,28 @@ Other: For complete usage examples, see [examples.md](docs/examples.md) -For complete option reference, run `cpx --help` +For complete option reference, run `copy --help` ## Configuration Set defaults with configuration files: ```bash # Create config with defaults -cpx config init +copy config init # View active configuration -cpx config show +copy config show # See config file location -cpx config path +copy config path ``` **Config locations (in priority order):** -1. `./cpxconfig.toml` (project-level) -2. `~/.config/cpx/cpxconfig.toml` (user-level) -3. `/etc/cpx/cpxconfig.toml` (system-level, Unix only) +1. `./copyconfig.toml` (project-level) +2. `~/.config/copy/copyconfig.toml` (user-level) +3. `/etc/copy/copyconfig.toml` (system-level, Unix only) -**Example config** (`~/.config/cpx/cpxconfig.toml`): +**Example config** (`~/.config/copy/copyconfig.toml`): ```toml [exclude] patterns = ["*.tmp", "*.log", "node_modules", ".git"] @@ -196,9 +208,9 @@ mode = "auto" ## Performance -`cpx` is built for speed. Quick comparison: +`copy` is built for speed. Quick comparison: -| Task | cp | cpx -j16 | speedup | +| Task | cp | copy -j16 | speedup | |------|-----|-------|-----| | VsCode (~15k files) | 1084ms | 263ms | 4.12x | | rust (~65k files) | 4.553s | 1.091s | 4.17x | @@ -221,8 +233,8 @@ mode = "auto" ## Quick Start for Developers ```bash -git clone https://github.com/11happy/cpx.git -cd cpx +git clone https://github.com/UnbreakableMJ/copy.git +cd copy # Run tests cargo test @@ -236,13 +248,13 @@ cargo run -- -r test_data/ test_dest/ ## Tests -Some tests are already ported from the [GNU coreutils cp test suite](https://github.com/coreutils/coreutils/tree/master/tests/cp), still porting more [GNU ported tests](https://github.com/11happy/cpx/tree/main/tests/gnu). +Some tests are already ported from the [GNU coreutils cp test suite](https://github.com/coreutils/coreutils/tree/master/tests/cp), still porting more [GNU ported tests](https://github.com/UnbreakableMJ/copy/tree/main/tests/gnu). -Found wrong behavior? [File an issue](https://github.com/11happy/cpx/issues), PRs for more tests are always welcome! +Found wrong behavior? [File an issue](https://github.com/UnbreakableMJ/copy/issues), PRs for more tests are always welcome! ## License -- MIT [LICENSE](https://github.com/11happy/cpx/blob/main/LICENSE) +- MIT [LICENSE](https://github.com/UnbreakableMJ/copy/blob/main/LICENSE) ## Acknowledgments diff --git a/README_CRATES.md b/README_CRATES.md index 780ff3b..66b5887 100644 --- a/README_CRATES.md +++ b/README_CRATES.md @@ -1,10 +1,10 @@ -# cpx +# copy
**A modern, fast file copy tool for Linux with progress bars, resume capability, and more.** -[![Crates.io](https://img.shields.io/crates/v/cpx.svg)](https://crates.io/crates/cpx) +[![Crates.io](https://img.shields.io/crates/v/copy.svg)](https://crates.io/crates/copy) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE-MIT) @@ -17,11 +17,11 @@ --- -## Why cpx? +## Why copy? -`cpx` is a modern replacement for the traditional `cp` command, built with Rust for maximum performance and safety on Linux systems. +`copy` is a modern replacement for the traditional `cp` command, built with Rust for maximum performance and safety on Linux systems. ```bash -cpx -r projects/ /backup/ +copy -r projects/ /backup/ Copying 51% ████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ETA:00:06 ``` ## Features @@ -43,80 +43,92 @@ Copying 51% ██████████████████████ ### Quick Install (Recommended) ```bash -curl -fsSL https://raw.githubusercontent.com/11happy/cpx/main/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/UnbreakableMJ/copy/main/install.sh | bash ``` Or with wget: ```bash -wget -qO- https://raw.githubusercontent.com/11happy/cpx/main/install.sh | bash +wget -qO- https://raw.githubusercontent.com/UnbreakableMJ/copy/main/install.sh | bash ``` ### From Crates.io ```bash -cargo install cpx +cargo install copy ``` ### Arch Linux (AUR) -> Added by community +A first-party PKGBUILD lives in [`packaging/aur/`](packaging/aur/). Build and install it from a checkout: ```bash -yay -S cpx-copy +cd packaging/aur && makepkg -si ``` +> An official AUR upload will track this PKGBUILD. (The older community `cpx-copy` package predates the rename.) ### Nix / NixOS -> Added by community +This repo is a flake. Run or install `copy` straight from GitHub: ```bash -nix-shell -p cpx +nix run github:UnbreakableMJ/copy -- --help +nix profile install github:UnbreakableMJ/copy ``` +For a dev shell with the Rust toolchain, run `nix develop`. + + +### GNU Guix +A [`guix.scm`](guix.scm) is provided. Guix builds offline, so vendor the crates once, then build: +```bash +cargo vendor guix/vendor +guix build -f guix.scm +``` +(Needs a Guix `rust` ≥ 1.85 for edition 2024.) ### From Source ```bash -cargo install --git https://github.com/11happy/cpx -cpx --version +cargo install --git https://github.com/UnbreakableMJ/copy +copy --version ``` ### Pre-built Binaries -Download from [Releases](https://github.com/11happy/cpx/releases) +Download from [Releases](https://github.com/UnbreakableMJ/copy/releases) ## Quick Start ### Basic Usage ```bash # Copy a file -cpx source.txt dest.txt +copy source.txt dest.txt # Copy directory recursively -cpx -r source_dir/ dest_dir/ +copy -r source_dir/ dest_dir/ # Copy with progress bar -cpx -r large_dir/ /backup/ +copy -r large_dir/ /backup/ ``` ### Common Use Cases ```bash # Backup project (exclude build artifacts) -cpx -r -e "node_modules" -e ".git" -e "target" my-project/ /backup/ +copy -r -e "node_modules" -e ".git" -e "target" my-project/ /backup/ # Resume interrupted transfer -cpx -r --resume large_dataset/ /backup/ +copy -r --resume large_dataset/ /backup/ # Deploy with safety (interactive + backups) -cpx -ri -b=numbered dist/ /var/www/production/ +copy -ri -b=numbered dist/ /var/www/production/ # Instant snapshot on Btrfs/XFS -cpx -r --reflink=always /data/ /snapshots/backup-$(date +%Y-%m-%d)/ +copy -r --reflink=always /data/ /snapshots/backup-$(date +%Y-%m-%d)/ # Copy with full attribute preservation -cpx -r -p=all photos/ /backup/photos/ +copy -r -p=all photos/ /backup/photos/ ``` **See [examples.md](docs/examples.md) for detailed workflows and real-world scenarios.** ## Key Options ``` -cpx [OPTIONS] ... +copy [OPTIONS] ... Arguments: ... Source file(s) or directory(ies) @@ -166,28 +178,28 @@ Other: For complete usage examples, see [examples.md](docs/examples.md) -For complete option reference, run `cpx --help` +For complete option reference, run `copy --help` ## Configuration Set defaults with configuration files: ```bash # Create config with defaults -cpx config init +copy config init # View active configuration -cpx config show +copy config show # See config file location -cpx config path +copy config path ``` **Config locations (in priority order):** -1. `./cpxconfig.toml` (project-level) -2. `~/.config/cpx/cpxconfig.toml` (user-level) -3. `/etc/cpx/cpxconfig.toml` (system-level, Unix only) +1. `./copyconfig.toml` (project-level) +2. `~/.config/copy/copyconfig.toml` (user-level) +3. `/etc/copy/copyconfig.toml` (system-level, Unix only) -**Example config** (`~/.config/cpx/cpxconfig.toml`): +**Example config** (`~/.config/copy/copyconfig.toml`): ```toml [exclude] patterns = ["*.tmp", "*.log", "node_modules", ".git"] @@ -210,9 +222,9 @@ mode = "auto" ## Performance -`cpx` is built for speed. Quick comparison: +`copy` is built for speed. Quick comparison: -| Task | cp | cpx -j16 | speedup | +| Task | cp | copy -j16 | speedup | |------|-----|-------|-----| | VsCode (~15k files) | 1084ms | 263ms | 4.12x | | rust (~65k files) | 4.553s | 1.091s | 4.17x | @@ -235,8 +247,8 @@ mode = "auto" ## Quick Start for Developers ```bash -git clone https://github.com/11happy/cpx.git -cd cpx +git clone https://github.com/UnbreakableMJ/copy.git +cd copy # Run tests cargo test @@ -250,7 +262,7 @@ cargo run -- -r test_data/ test_dest/ ## License -- MIT [LICENSE](https://github.com/11happy/cpx/blob/main/LICENSE) +- MIT [LICENSE](https://github.com/UnbreakableMJ/copy/blob/main/LICENSE) ## Acknowledgments diff --git a/benchmarks/bench.sh b/benchmarks/bench.sh old mode 100755 new mode 100644 index 85bb5b3..895b811 --- a/benchmarks/bench.sh +++ b/benchmarks/bench.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# CPX vs GNU cp — Real-World Benchmark (Cold + Warm Cache) +# Copy vs GNU cp — Real-World Benchmark (Cold + Warm Cache) set -euo pipefail # ---------------------------------------------------------------------------- @@ -14,7 +14,7 @@ NC='\033[0m' # ---------------------------------------------------------------------------- # CONFIG # ---------------------------------------------------------------------------- -BENCH_DIR="${BENCH_DIR:-/tmp/cpx_multi_bench}" +BENCH_DIR="${BENCH_DIR:-/tmp/copy_multi_bench}" REPOS_DIR="$BENCH_DIR/repos" THREADS="${THREADS:-$(nproc)}" RUNS="${RUNS:-6}" @@ -22,27 +22,27 @@ RUNS="${RUNS:-6}" MODE="${1:-warm}" # warm | cold # ---------------------------------------------------------------------------- -# FIND CPX BINARY +# FIND Copy BINARY # ---------------------------------------------------------------------------- -find_cpx() { - # 1. Honour explicit CPX_PATH environment variable - if [ -n "${CPX_PATH:-}" ] && [ -x "$CPX_PATH" ]; then - echo "$CPX_PATH" +find_copy() { + # 1. Honour explicit COPY_PATH environment variable + if [ -n "${COPY_PATH:-}" ] && [ -x "$COPY_PATH" ]; then + echo "$COPY_PATH" return 0 fi - # 2. Check if cpx is already on PATH - if command -v cpx &>/dev/null; then - command -v cpx + # 2. Check if copy is already on PATH + if command -v copy &>/dev/null; then + command -v copy return 0 fi # 3. Common install locations local candidates=( - "$HOME/.local/bin/cpx" - "$HOME/.cargo/bin/cpx" - "/usr/local/bin/cpx" - "/usr/bin/cpx" + "$HOME/.local/bin/copy" + "$HOME/.cargo/bin/copy" + "/usr/local/bin/copy" + "/usr/bin/copy" ) for candidate in "${candidates[@]}"; do @@ -58,7 +58,7 @@ find_cpx() { local repo_dir repo_dir="$(dirname "$script_dir")" - for candidate in "$repo_dir/cpx" "$repo_dir/target/release/cpx" "$repo_dir/target/debug/cpx"; do + for candidate in "$repo_dir/copy" "$repo_dir/target/release/copy" "$repo_dir/target/debug/copy"; do if [ -x "$candidate" ]; then echo "$candidate" return 0 @@ -71,36 +71,36 @@ find_cpx() { # ---------------------------------------------------------------------------- # PRECHECKS # ---------------------------------------------------------------------------- -echo -e "${GREEN}=== CPX vs GNU cp Benchmark ($MODE cache) ===${NC}" +echo -e "${GREEN}=== Copy vs GNU cp Benchmark ($MODE cache) ===${NC}" echo "" -CPX_PATH="$(find_cpx || true)" +COPY_PATH="$(find_copy || true)" -if [ -z "$CPX_PATH" ]; then - echo -e "${RED}Error: cpx binary not found.${NC}" +if [ -z "$COPY_PATH" ]; then + echo -e "${RED}Error: copy binary not found.${NC}" echo "" echo -e "${YELLOW}Searched in:${NC}" - echo " • \$CPX_PATH environment variable" - echo " • \$PATH (command -v cpx)" - echo " • ~/.local/bin/cpx" - echo " • ~/.cargo/bin/cpx" - echo " • /usr/local/bin/cpx" - echo " • /usr/bin/cpx" - echo " • /cpx, /target/release/cpx, /target/debug/cpx" + echo " • \$COPY_PATH environment variable" + echo " • \$PATH (command -v copy)" + echo " • ~/.local/bin/copy" + echo " • ~/.cargo/bin/copy" + echo " • /usr/local/bin/copy" + echo " • /usr/bin/copy" + echo " • /copy, /target/release/copy, /target/debug/copy" echo "" - echo -e "${YELLOW}Install cpx using one of:${NC}" - echo " curl -fsSL https://raw.githubusercontent.com/11happy/cpx/main/install.sh | bash" - echo " cargo install cpx-cli" + echo -e "${YELLOW}Install copy using one of:${NC}" + echo " curl -fsSL https://raw.githubusercontent.com/UnbreakableMJ/copy/main/install.sh | bash" + echo " cargo install copy" echo "" - read -p "Would you like to install cpx now? (Y/n): " -n 1 -r + read -p "Would you like to install copy now? (Y/n): " -n 1 -r echo if [[ ! $REPLY =~ ^[Nn]$ ]]; then - echo -e "${BLUE}Installing cpx...${NC}" - curl -fsSL https://raw.githubusercontent.com/11happy/cpx/main/install.sh | bash + echo -e "${BLUE}Installing copy...${NC}" + curl -fsSL https://raw.githubusercontent.com/UnbreakableMJ/copy/main/install.sh | bash echo "" - CPX_PATH="$(find_cpx || true)" - if [ -z "$CPX_PATH" ]; then - echo -e "${RED}Installation succeeded but cpx still not found in expected locations.${NC}" + COPY_PATH="$(find_copy || true)" + if [ -z "$COPY_PATH" ]; then + echo -e "${RED}Installation succeeded but copy still not found in expected locations.${NC}" echo -e "${YELLOW}Try adding ~/.local/bin to your PATH and re-running.${NC}" exit 1 fi @@ -109,7 +109,7 @@ if [ -z "$CPX_PATH" ]; then fi fi -echo -e "${GREEN}Found cpx at: $CPX_PATH${NC}" +echo -e "${GREEN}Found copy at: $COPY_PATH${NC}" if ! command -v hyperfine &>/dev/null; then echo -e "${RED}Error: hyperfine not found (cargo install hyperfine)${NC}" @@ -191,10 +191,10 @@ for name in "${!REPOS[@]}"; do hyperfine \ --runs "$RUNS" \ --warmup 0 \ - --prepare "rm -rf $BENCH_DIR/dest_cp $BENCH_DIR/dest_cpx; sync; [ \"$MODE\" = cold ] && echo 3 > /proc/sys/vm/drop_caches || true" \ + --prepare "rm -rf $BENCH_DIR/dest_cp $BENCH_DIR/dest_copy; sync; [ \"$MODE\" = cold ] && echo 3 > /proc/sys/vm/drop_caches || true" \ --export-markdown "$BENCH_DIR/${name}_${MODE}.md" \ --export-json "$BENCH_DIR/${name}_${MODE}.json" \ - "$CPX_PATH -r -j=$THREADS $src $BENCH_DIR/dest_cpx" \ + "$COPY_PATH -r -j=$THREADS $src $BENCH_DIR/dest_copy" \ "cp -r $src $BENCH_DIR/dest_cp" echo "" @@ -209,17 +209,17 @@ echo "" hyperfine \ --runs "$RUNS" \ --warmup 0 \ - --prepare "rm -rf $BENCH_DIR/dest_cp $BENCH_DIR/dest_cpx; sync; [ \"$MODE\" = cold ] && echo 3 > /proc/sys/vm/drop_caches || true" \ + --prepare "rm -rf $BENCH_DIR/dest_cp $BENCH_DIR/dest_copy; sync; [ \"$MODE\" = cold ] && echo 3 > /proc/sys/vm/drop_caches || true" \ --export-markdown "$BENCH_DIR/full_${MODE}.md" \ --export-json "$BENCH_DIR/full_${MODE}.json" \ - "$CPX_PATH -r -j=$THREADS $REPOS_DIR $BENCH_DIR/dest_cpx" \ + "$COPY_PATH -r -j=$THREADS $REPOS_DIR $BENCH_DIR/dest_copy" \ "cp -r $REPOS_DIR $BENCH_DIR/dest_cp" # ---------------------------------------------------------------------------- # SUMMARY # ---------------------------------------------------------------------------- cat > "$BENCH_DIR/SUMMARY_${MODE}.md" < SSD > HDD), CPU cores, and filesystem (ext4/btrfs/xfs). diff --git a/docs/configuration.md b/docs/configuration.md index c684568..2860d28 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,6 @@ # Configuration Guide -`cpx` supports flexible configuration through TOML files, allowing you to set default behaviors without specifying command-line flags every time. +`copy` supports flexible configuration through TOML files, allowing you to set default behaviors without specifying command-line flags every time. ## Table of Contents @@ -20,11 +20,11 @@ ## Configuration File Locations -`cpx` looks for configuration files in the following locations (in order of priority): +`copy` looks for configuration files in the following locations (in order of priority): -1. **Project-level**: `./cpxconfig.toml` (in the current directory) -2. **User-level**: `~/.config/cpx/cpxconfig.toml` (Linux/macOS) -3. **System-level**: `/etc/cpx/cpxconfig.toml` (Unix systems only) +1. **Project-level**: `./copyconfig.toml` (in the current directory) +2. **User-level**: `~/.config/copy/copyconfig.toml` (Linux/macOS) +3. **System-level**: `/etc/copy/copyconfig.toml` (Unix systems only) ## Configuration Priority @@ -35,7 +35,7 @@ Defaults → System Config → User Config → Project Config → CLI Flags **Example:** - If `recursive = true` is set in user config -- But you run `cpx` without `-r` flag +- But you run `copy` without `-r` flag - The user config setting will be used **CLI flags always override config files.** @@ -46,42 +46,42 @@ Defaults → System Config → User Config → Project Config → CLI Flags Create a config file with default settings: ```bash -cpx config init +copy config init ``` -This creates `~/.config/cpx/cpxconfig.toml` with sensible defaults. +This creates `~/.config/copy/copyconfig.toml` with sensible defaults. To overwrite an existing config: ```bash -cpx config init --force +copy config init --force ``` ### View Current Configuration See the effective configuration (merged from all sources): ```bash -cpx config show +copy config show ``` ### View Config File Location See which config file is being used: ```bash -cpx config path +copy config path ``` ### Ignore All Config Files Use the `--no-config` flag to ignore all configuration files: ```bash -cpx --no-config source.txt dest.txt +copy --no-config source.txt dest.txt ``` ### Use Custom Config File Specify a custom config file location: ```bash -cpx --config /path/to/custom.toml source.txt dest.txt +copy --config /path/to/custom.toml source.txt dest.txt ``` ## Configuration Options @@ -121,7 +121,7 @@ patterns = [ **CLI Override:** ```bash -cpx -e "*.tmp" -e "node_modules" source/ dest/ +copy -e "*.tmp" -e "node_modules" source/ dest/ ``` ### Copy Settings @@ -204,9 +204,9 @@ mode = "mode,timestamps,ownership" **CLI Override:** ```bash -cpx -p source.txt dest.txt # Default preservation -cpx -p=mode,timestamps source.txt dest.txt # Custom attributes -cpx --attributes-only source.txt dest.txt # Preserve all (no data copy) +copy -p source.txt dest.txt # Default preservation +copy -p=mode,timestamps source.txt dest.txt # Custom attributes +copy --attributes-only source.txt dest.txt # Preserve all (no data copy) ``` ### Symlink Handling @@ -247,12 +247,12 @@ follow = "always" **CLI Override:** ```bash -cpx -s source/ dest/ # Auto symlink mode -cpx -s=absolute source/ dest/ # Absolute symlinks -cpx -s=relative source/ dest/ # Relative symlinks -cpx -L source/ dest/ # Follow all symlinks -cpx -P source/ dest/ # Don't follow symlinks -cpx -H source/ dest/ # Follow command-line symlinks only +copy -s source/ dest/ # Auto symlink mode +copy -s=absolute source/ dest/ # Absolute symlinks +copy -s=relative source/ dest/ # Relative symlinks +copy -L source/ dest/ # Follow all symlinks +copy -P source/ dest/ # Don't follow symlinks +copy -H source/ dest/ # Follow command-line symlinks only ``` ### Backup Settings @@ -287,9 +287,9 @@ After copying: **CLI Override:** ```bash -cpx -b source.txt dest.txt # Use existing mode -cpx -b=numbered source.txt dest.txt # Numbered backups -cpx -b=simple source.txt dest.txt # Simple backups +copy -b source.txt dest.txt # Use existing mode +copy -b=numbered source.txt dest.txt # Numbered backups +copy -b=simple source.txt dest.txt # Simple backups ``` ### Reflink (Copy-on-Write) @@ -326,9 +326,9 @@ mode = "auto" **CLI Override:** ```bash -cpx --reflink source.txt dest.txt # Auto mode -cpx --reflink=always source.txt dest.txt # Require reflink -cpx --reflink=never source.txt dest.txt # Disable reflink +copy --reflink source.txt dest.txt # Auto mode +copy --reflink=always source.txt dest.txt # Require reflink +copy --reflink=never source.txt dest.txt # Disable reflink ``` ### Progress Bar Customization @@ -380,7 +380,7 @@ Copying: 67/100 [=========================>·········] files 67% | 1.2GB Here's a fully documented configuration file with common settings: ```toml -# cpx configuration file +# copy configuration file # Exclude patterns (glob syntax supported) # Example: patterns = ["*.tmp", "*.log", "node_modules", ".git"] @@ -533,26 +533,26 @@ mode = "all" ## Tips -1. **Start with defaults**: Run `cpx config init` and modify from there -2. **Project-specific settings**: Create `./cpxconfig.toml` in project roots -3. **Check effective config**: Use `cpx config show` to see merged settings +1. **Start with defaults**: Run `copy config init` and modify from there +2. **Project-specific settings**: Create `./copyconfig.toml` in project roots +3. **Check effective config**: Use `copy config show` to see merged settings 4. **CLI always wins**: Command-line flags override all config files 5. **Test before committing**: Use `--no-config` to test without your settings ## Troubleshooting **Config not being used?** -- Check location: `cpx config path` +- Check location: `copy config path` - Verify syntax: Ensure valid TOML - Check permissions: File must be readable **Unexpected behavior?** -- View effective config: `cpx config show` +- View effective config: `copy config show` - Disable config: Use `--no-config` flag - Check priority: Project > User > System **Need per-project settings?** -- Create `./cpxconfig.toml` in your project directory +- Create `./copyconfig.toml` in your project directory - This overrides user and system configs --- diff --git a/docs/examples.md b/docs/examples.md index b4b1958..46bf892 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -1,6 +1,6 @@ # Examples -This guide provides practical examples of using `cpx` for common file copying tasks. +This guide provides practical examples of using `copy` for common file copying tasks. ## Table of Contents @@ -20,34 +20,34 @@ This guide provides practical examples of using `cpx` for common file copying ta ### Copy a Single File ```bash # Simple copy -cpx source.txt destination.txt +copy source.txt destination.txt # Copy to directory (destination filename matches source) -cpx source.txt /path/to/directory/ +copy source.txt /path/to/directory/ # Copy with overwrite confirmation -cpx -i source.txt destination.txt +copy -i source.txt destination.txt ``` ### Copy Multiple Files ```bash # Copy multiple files to a directory -cpx file1.txt file2.txt file3.txt /destination/ +copy file1.txt file2.txt file3.txt /destination/ # Using target directory flag -cpx -t /destination/ file1.txt file2.txt file3.txt +copy -t /destination/ file1.txt file2.txt file3.txt # Copy with pattern expansion -cpx *.txt /destination/ +copy *.txt /destination/ ``` ### Force Overwrite ```bash # Overwrite read-only files -cpx -f source.txt destination.txt +copy -f source.txt destination.txt # Remove destination before copying -cpx --remove-destination source.txt destination.txt +copy --remove-destination source.txt destination.txt ``` ## Directory Operations @@ -55,30 +55,30 @@ cpx --remove-destination source.txt destination.txt ### Copy Directory Recursively ```bash # Basic recursive copy -cpx -r source_dir/ destination_dir/ +copy -r source_dir/ destination_dir/ # Copy with specific parallel -cpx -r -j 8 source_dir/ destination_dir/ +copy -r -j 8 source_dir/ destination_dir/ # Interactive recursive copy -cpx -ri source_dir/ destination_dir/ +copy -ri source_dir/ destination_dir/ ``` ### Preserve Directory Structure ```bash # Copy with parent directories -cpx --parents src/components/Button.tsx /backup/ +copy --parents src/components/Button.tsx /backup/ # Result: /backup/src/components/Button.tsx # Multiple files with parents -cpx --parents src/**/*.tsx /backup/ +copy --parents src/**/*.tsx /backup/ ``` ### Copy Only Directory Structure (No Files) ```bash # Copy attributes only (creates dirs, updates permissions) -cpx -r --attributes-only source_dir/ destination_dir/ +copy -r --attributes-only source_dir/ destination_dir/ ``` ## Exclude Patterns @@ -86,28 +86,28 @@ cpx -r --attributes-only source_dir/ destination_dir/ ### Exclude by File Extension ```bash # Exclude temporary files -cpx -r -e "*.tmp" -e "*.swp" source/ dest/ +copy -r -e "*.tmp" -e "*.swp" source/ dest/ # Exclude multiple patterns at once -cpx -r -e "*.tmp,*.log,*.cache" source/ dest/ +copy -r -e "*.tmp,*.log,*.cache" source/ dest/ ``` ### Exclude Directories ```bash # Exclude node_modules -cpx -r -e "node_modules" project/ backup/ +copy -r -e "node_modules" project/ backup/ # Exclude multiple directories -cpx -r -e "node_modules" -e ".git" -e "target" project/ backup/ +copy -r -e "node_modules" -e ".git" -e "target" project/ backup/ # Exclude with comma-separated list -cpx -r -e "node_modules,.git,__pycache__" project/ backup/ +copy -r -e "node_modules,.git,__pycache__" project/ backup/ ``` ### Complex Exclusion Patterns ```bash # Exclude build artifacts from multiple languages -cpx -r \ +copy -r \ -e "node_modules" \ -e "target" \ -e "dist" \ @@ -117,16 +117,16 @@ cpx -r \ project/ backup/ # Exclude with glob patterns -cpx -r -e "test_*.py" -e "*.test.js" source/ dest/ +copy -r -e "test_*.py" -e "*.test.js" source/ dest/ # Exclude specific paths -cpx -r -e "src/generated/*" -e "docs/api/*" project/ backup/ +copy -r -e "src/generated/*" -e "docs/api/*" project/ backup/ ``` ### Development Project Backup ```bash # Skip all common development files -cpx -r \ +copy -r \ -e "node_modules" \ -e ".git" \ -e ".svn" \ @@ -147,30 +147,30 @@ cpx -r \ ### Preserve Default Attributes ```bash # Preserve mode, ownership, and timestamps (default with -p) -cpx -p source.txt destination.txt +copy -p source.txt destination.txt # Same as above (empty -p uses defaults) -cpx -p= source.txt destination.txt +copy -p= source.txt destination.txt ``` ### Preserve Specific Attributes ```bash # Preserve only file permissions -cpx -p=mode source.txt destination.txt +copy -p=mode source.txt destination.txt # Preserve permissions and timestamps -cpx -p=mode,timestamps source.txt destination.txt +copy -p=mode,timestamps source.txt destination.txt # Preserve everything -cpx -p=all source.txt destination.txt +copy -p=all source.txt destination.txt # Or use --attributes-only for dirs -cpx -r --attributes-only source/ dest/ +copy -r --attributes-only source/ dest/ ``` ### Preserve Hard Link Relationships ```bash # Preserve hard links between files -cpx -r -p=links source/ dest/ +copy -r -p=links source/ dest/ # This maintains hard link relationships in the destination # If source/file1 and source/file2 are hard linked, @@ -180,10 +180,10 @@ cpx -r -p=links source/ dest/ ### Update Only Attributes ```bash # Update permissions without copying data -cpx -p=mode --attributes-only source.txt destination.txt +copy -p=mode --attributes-only source.txt destination.txt # Update all attributes for directory tree -cpx -r -p=all --attributes-only source/ dest/ +copy -r -p=all --attributes-only source/ dest/ ``` ## Backup Strategies @@ -191,7 +191,7 @@ cpx -r -p=all --attributes-only source/ dest/ ### Simple Backup ```bash # Append ~ to existing files -cpx -b source.txt destination.txt +copy -b source.txt destination.txt # If destination.txt exists, it becomes destination.txt~ ``` @@ -199,7 +199,7 @@ cpx -b source.txt destination.txt ### Numbered Backups ```bash # Create numbered backups -cpx -b=numbered source.txt destination.txt +copy -b=numbered source.txt destination.txt # Creates: # - destination.txt (new file) @@ -210,22 +210,22 @@ cpx -b=numbered source.txt destination.txt ### Smart Backup Strategy ```bash # Use existing: numbered if backups exist, simple otherwise -cpx -b=existing source.txt destination.txt +copy -b=existing source.txt destination.txt ``` ### Backup Entire Directory ```bash # Create numbered backups for all files -cpx -r -b=numbered source/ dest/ +copy -r -b=numbered source/ dest/ # Useful for incremental updates -cpx -r -b=numbered --resume updated_project/ production/ +copy -r -b=numbered --resume updated_project/ production/ ``` ### Production Deployment with Backup ```bash # Safe deployment with interactive mode and backups -cpx -ri -b=numbered \ +copy -ri -b=numbered \ -e "*.log" \ -e "tmp/*" \ new_version/ /var/www/production/ @@ -236,32 +236,32 @@ cpx -ri -b=numbered \ ### Create Symbolic Links ```bash # Create symlinks instead of copying (auto mode) -cpx -s source.txt link.txt -cpx -s source_dir/ link_dir/ +copy -s source.txt link.txt +copy -s source_dir/ link_dir/ # Create absolute symlinks -cpx -s=absolute source.txt /path/to/link.txt +copy -s=absolute source.txt /path/to/link.txt # Create relative symlinks -cpx -s=relative source.txt ../links/link.txt +copy -s=relative source.txt ../links/link.txt ``` ### Copy Symlink Behavior ```bash # Don't follow symlinks (copy the link itself) -cpx -P -r source/ dest/ +copy -P -r source/ dest/ # Follow all symlinks (copy target files) -cpx -L -r source/ dest/ +copy -L -r source/ dest/ # Follow only command-line symlinks -cpx -H link_to_dir/ dest/ +copy -H link_to_dir/ dest/ ``` ### Create Link Farm ```bash # Create relative symlinks to all files -cpx -r -s=relative /media/music/ ~/music-links/ +copy -r -s=relative /media/music/ ~/music-links/ # Result: ~/music-links/ contains symlinks to originals ``` @@ -269,7 +269,7 @@ cpx -r -s=relative /media/music/ ~/music-links/ ### Mirror with Symlinks ```bash # Create symlink mirror of directory structure -cpx -r -s=relative \ +copy -r -s=relative \ -e ".git" \ ~/projects/my-app/ ~/links/my-app/ ``` @@ -279,19 +279,19 @@ cpx -r -s=relative \ ### Create Hard Links ```bash # Hard link instead of copying -cpx -l source.txt hardlink.txt +copy -l source.txt hardlink.txt # Hard link multiple files -cpx -l file1.txt file2.txt file3.txt /destination/ +copy -l file1.txt file2.txt file3.txt /destination/ # Hard link directory contents -cpx -rl source/ dest/ +copy -rl source/ dest/ ``` ### Space-Efficient Backups ```bash # Create hard-linked backup (saves space) -cpx -rl \ +copy -rl \ -e "*.log" \ -e "tmp/*" \ /var/www/app/ /backup/snapshots/2025-01-24/ @@ -303,7 +303,7 @@ cpx -rl \ ### Deduplication ```bash # Create hard links to deduplicate identical files -cpx -rl -p=links source/ deduplicated/ +copy -rl -p=links source/ deduplicated/ ``` ## Resume Interrupted Transfers @@ -311,10 +311,10 @@ cpx -rl -p=links source/ deduplicated/ ### Resume Large Copy Operation ```bash # Start copy -cpx -r large_dataset/ /backup/large_dataset/ +copy -r large_dataset/ /backup/large_dataset/ # If interrupted (Ctrl+C), resume with: -cpx -r --resume large_dataset/ /backup/large_dataset/ +copy -r --resume large_dataset/ /backup/large_dataset/ # Files already copied are skipped (verified by checksum) ``` @@ -322,7 +322,7 @@ cpx -r --resume large_dataset/ /backup/large_dataset/ ### Resume with Progress ```bash # Resume large transfer with detailed progress -cpx -r --resume -j 8 \ +copy -r --resume -j 8 \ /mnt/source/big_project/ \ /mnt/backup/big_project/ @@ -337,7 +337,7 @@ cpx -r --resume -j 8 \ # - Different content (checksum verified) # - Older modification time in source -cpx -r --resume source/ dest/ +copy -r --resume source/ dest/ ``` ## Advanced Scenarios @@ -345,19 +345,19 @@ cpx -r --resume source/ dest/ ### Copy-on-Write (Reflink) Copies ```bash # Instant copy on supporting filesystems (Btrfs, XFS, APFS) -cpx --reflink source.txt destination.txt +copy --reflink source.txt destination.txt # Require reflink (fail if not supported) -cpx --reflink=always source.txt destination.txt +copy --reflink=always source.txt destination.txt # Try reflink, fall back to regular copy -cpx --reflink=auto source.txt destination.txt +copy --reflink=auto source.txt destination.txt ``` ### Fast Snapshot on Btrfs ```bash # Instant snapshot using reflinks -cpx -r --reflink=always /home/user/ /snapshots/user-2025-01-24/ +copy -r --reflink=always /home/user/ /snapshots/user-2025-01-24/ # Takes seconds regardless of size # Space only used when files are modified @@ -366,7 +366,7 @@ cpx -r --reflink=always /home/user/ /snapshots/user-2025-01-24/ ### Sync-like Behavior ```bash # Update destination with newer files -cpx -r --resume \ +copy -r --resume \ -p=mode,timestamps \ source/ dest/ ``` @@ -374,7 +374,7 @@ cpx -r --resume \ ### Migrate with Verification ```bash # Copy with full preservation and resume capability -cpx -r --resume \ +copy -r --resume \ -p=all \ -b=numbered \ -j 16 \ @@ -384,13 +384,13 @@ cpx -r --resume \ ### Clone Git Repository (Files Only) ```bash # Copy git repo without .git directory -cpx -r -e ".git" my-project/ my-project-copy/ +copy -r -e ".git" my-project/ my-project-copy/ ``` ### Selective Directory Sync ```bash # Sync only specific file types -cpx -r \ +copy -r \ -e "!*.txt" \ -e "!*.md" \ -e "*" \ @@ -402,7 +402,7 @@ cpx -r \ ### Archive with Structure ```bash # Archive with full paths preserved -cpx --parents \ +copy --parents \ src/**/*.{rs,toml} \ tests/**/*.rs \ /archive/ @@ -411,7 +411,7 @@ cpx --parents \ ### Update Permissions Recursively ```bash # Update only permissions, don't copy data -cpx -r --attributes-only -p=mode template/ project/ +copy -r --attributes-only -p=mode template/ project/ ``` ## Performance Optimization @@ -419,28 +419,28 @@ cpx -r --attributes-only -p=mode template/ project/ ### Maximize Throughput ```bash # Use high parallel for many small files -cpx -r -j 16 many_small_files/ dest/ +copy -r -j 16 many_small_files/ dest/ # Use lower parallel for large files -cpx -r -j 2 few_large_files/ dest/ +copy -r -j 2 few_large_files/ dest/ ``` ### Fast Local Copy (SSD to SSD) ```bash # Maximum speed with reflink -cpx -r --reflink=auto -j 8 source/ dest/ +copy -r --reflink=auto -j 8 source/ dest/ ``` ### Network Copy Optimization ```bash # Lower parallel, resume support -cpx -r -j 4 --resume /local/data/ /network/mount/data/ +copy -r -j 4 --resume /local/data/ /network/mount/data/ ``` ### Large Dataset Transfer ```bash # Optimized for large transfers -cpx -r \ +copy -r \ -j 8 \ --resume \ -p=mode,timestamps \ @@ -455,7 +455,7 @@ cpx -r \ ### Minimal Overhead Copy ```bash # Skip all preservation for maximum speed -cpx -r -j 16 source/ dest/ +copy -r -j 16 source/ dest/ # No progress bar, no preservation # Fastest possible copy @@ -466,7 +466,7 @@ cpx -r -j 16 source/ dest/ ### Daily Development Backup ```bash # Backup work directory, excluding build artifacts -cpx -r --resume \ +copy -r --resume \ -b=numbered \ -e "node_modules,.git,target,dist,build" \ ~/projects/my-app/ \ @@ -476,7 +476,7 @@ cpx -r --resume \ ### Deploy Web Application ```bash # Deploy with backup and verification -cpx -ri \ +copy -ri \ -b=numbered \ -p=mode,timestamps \ -e "*.log" \ @@ -488,7 +488,7 @@ cpx -ri \ ### Create Development Environment ```bash # Clone project template without version control -cpx -r \ +copy -r \ -e ".git" \ -e "node_modules" \ -e ".env" \ @@ -499,7 +499,7 @@ cpx -r \ ### Photo Library Backup ```bash # Backup photos with preservation -cpx -r \ +copy -r \ -p=all \ -b=numbered \ --resume \ @@ -511,7 +511,7 @@ cpx -r \ ### Server Migration ```bash # Migrate server data with full preservation -cpx -r \ +copy -r \ --resume \ -p=all \ -e "*.log" \ @@ -523,7 +523,7 @@ cpx -r \ ### Create Project Snapshot ```bash # Fast snapshot using reflinks (Btrfs/XFS/APFS) -cpx -r --reflink=always \ +copy -r --reflink=always \ ~/projects/my-app/ \ ~/snapshots/my-app-$(date +%Y-%m-%d-%H%M)/ ``` @@ -531,7 +531,7 @@ cpx -r --reflink=always \ ### Deduplicated Backup ```bash # Create hard-linked backup (saves space) -cpx -rl \ +copy -rl \ -p=all \ ~/Documents/ \ /backup/incremental/$(date +%Y-%m-%d)/ @@ -542,14 +542,14 @@ cpx -rl \ ### Cross-Platform Copy ```bash # Copy preserving only timestamps (safe for Windows/Linux) -cpx -r -p=timestamps source/ dest/ +copy -r -p=timestamps source/ dest/ ``` ## Combining Options ### Safe Production Update ```bash -cpx -ri \ +copy -ri \ --resume \ -b=numbered \ -p=mode,timestamps \ @@ -569,7 +569,7 @@ cpx -ri \ ### Fast Bulk Transfer ```bash -cpx -r \ +copy -r \ -j 16 \ --resume \ --reflink=auto \ @@ -586,7 +586,7 @@ cpx -r \ ### Complete Preservation ```bash -cpx -r \ +copy -r \ -p=all \ --resume \ -b=numbered \ @@ -602,22 +602,22 @@ cpx -r \ ### Dry Run Simulation -While `cpx` doesn't have a built-in dry-run, you can test with: +While `copy` doesn't have a built-in dry-run, you can test with: ```bash # Use attributes-only to test without copying data -cpx -r --attributes-only source/ dest/ +copy -r --attributes-only source/ dest/ ``` ### Check What Will Be Excluded ```bash # Use a test directory to verify exclude patterns -cpx -r -e "*.tmp" -e "node_modules" test_source/ test_dest/ +copy -r -e "*.tmp" -e "node_modules" test_source/ test_dest/ ``` ### Resume After System Crash ```bash # Always safe to resume -cpx -r --resume /backup/incomplete/ /restore/location/ +copy -r --resume /backup/incomplete/ /restore/location/ # Checksums verify file integrity ``` @@ -625,14 +625,14 @@ cpx -r --resume /backup/incomplete/ /restore/location/ ### Space-Efficient Testing ```bash # Use symlinks for testing -cpx -r -s=relative test_data/ test_copy/ +copy -r -s=relative test_data/ test_copy/ # Use hard links to save space -cpx -rl test_data/ test_copy/ +copy -rl test_data/ test_copy/ ``` --- For configuration options, see [Configuration Guide](configuration.md). -For CLI reference, see `cpx --help`. +For CLI reference, see `copy --help`. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..4e41dd2 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1781074563, + "narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..4693f1b --- /dev/null +++ b/flake.nix @@ -0,0 +1,37 @@ +{ + description = "copy — a modern, fast, parallel file copying tool for Linux"; + + inputs = { + # nixos-unstable carries a Rust new enough for edition 2024 (rustc >= 1.85). + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + # copy is Linux-only (it uses copy_file_range), so only expose Linux systems. + flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" ] (system: + let + pkgs = import nixpkgs { inherit system; }; + copy = pkgs.callPackage ./nix/package.nix { }; + in + { + packages.default = copy; + packages.copy = copy; + + apps.default = flake-utils.lib.mkApp { + drv = copy; + name = "copy"; + }; + + devShells.default = pkgs.mkShell { + inputsFrom = [ copy ]; + packages = with pkgs; [ cargo rustc clippy rustfmt rust-analyzer ]; + }; + }) + // { + # Usable from other flakes: inputs.copy.overlays.default + overlays.default = final: _prev: { + copy = final.callPackage ./nix/package.nix { }; + }; + }; +} diff --git a/guix.scm b/guix.scm new file mode 100644 index 0000000..4e6afd4 --- /dev/null +++ b/guix.scm @@ -0,0 +1,75 @@ +;;; GNU Guix package definition for `copy`. +;;; +;;; Guix builds are hermetic (no network), so the crate dependencies must be +;;; supplied at build time. Rather than packaging every transitive crate, this +;;; definition builds against a locally vendored copy of the dependencies. +;;; +;;; One-time prep, then build: +;;; +;;; cargo vendor guix/vendor # writes the crate sources (gitignored) +;;; guix build -f guix.scm +;;; guix shell -f guix.scm # drop the binary into an environment +;;; +;;; The vendored directory is injected as a store item and cargo is pointed at +;;; it via .cargo/config.toml, so the build stays offline. +;;; +;;; Note: edition 2024 needs rustc >= 1.85; ensure your Guix `rust` is new +;;; enough. To build with SELinux xattr support, add `libselinux` to inputs and +;;; append "--features" "selinux-support" to the cargo invocation below. + +(use-modules (guix packages) + (guix gexp) + (guix git-download) + (guix build-system gnu) + (gnu packages rust) + ((guix licenses) #:prefix license:)) + +(define %vendor + ;; Generated by `cargo vendor guix/vendor` (see header). Evaluating this file + ;; before that directory exists is expected to fail. + (local-file "guix/vendor" "copy-vendor" #:recursive? #t)) + +(package + (name "copy") + (version "0.1.4") ; keep in sync with Cargo.toml + (source (local-file "." "copy-checkout" + #:recursive? #t + ;; Only tracked files — drops target/, result, .git and + ;; the untracked guix/vendor (supplied separately above). + #:select? (git-predicate (dirname (current-filename))))) + (build-system gnu-build-system) + (arguments + (list + #:phases + #~(modify-phases %standard-phases + (delete 'configure) + (delete 'check) ; tests run via `cargo test` / CI + (replace 'build + (lambda _ + (setenv "CARGO_HOME" (string-append (getcwd) "/.cargo-home")) + (setenv "CARGO_NET_OFFLINE" "true") + (mkdir-p ".cargo") + (call-with-output-file ".cargo/config.toml" + (lambda (port) + (format port "\ +[source.crates-io] +replace-with = \"vendored-sources\" + +[source.vendored-sources] +directory = ~s~%" #$%vendor))) + (invoke "cargo" "build" "--release" "--frozen" "--offline"))) + (replace 'install + (lambda _ + (install-file "target/release/copy" + (string-append #$output "/bin")) + (install-file "LICENSE" + (string-append #$output "/share/licenses/copy"))))))) + (native-inputs (list rust (list rust "cargo"))) + (home-page "https://github.com/UnbreakableMJ/copy") + (synopsis "A modern, fast file copying tool") + (description + "copy is a modern, parallel replacement for the @command{cp} command on +Linux. It copies files and directories in parallel with progress bars, can +resume interrupted transfers, preserves attributes, and supports reflink and +gitignore-style exclude patterns.") + (license license:expat)) diff --git a/install.sh b/install.sh index b2dc7bc..dca026e 100755 --- a/install.sh +++ b/install.sh @@ -1,11 +1,11 @@ #!/bin/bash set -e -# cpx installer script -# Usage: curl -fsSL https://raw.githubusercontent.com/11happy/cpx/main/install.sh | bash +# copy installer script +# Usage: curl -fsSL https://raw.githubusercontent.com/UnbreakableMJ/copy/main/install.sh | bash -REPO="11happy/cpx" +REPO="UnbreakableMJ/copy" INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" -BINARY_NAME="cpx" +BINARY_NAME="copy" # Colors RED='\033[0;31m' @@ -60,7 +60,7 @@ get_latest_version() { } # Download and install -install_cpx() { +install_copy() { local platform version download_url tarball_name platform=$(detect_platform) @@ -72,7 +72,7 @@ install_cpx() { fi info "Latest version: v$version" - tarball_name="cpx-${platform}.tar.gz" + tarball_name="copy-${platform}.tar.gz" download_url="https://github.com/$REPO/releases/download/v${version}/${tarball_name}" info "Downloading from: $download_url" @@ -101,7 +101,7 @@ install_cpx() { cp "$tmp_dir/$BINARY_NAME" "$INSTALL_DIR/$BINARY_NAME" chmod +x "$INSTALL_DIR/$BINARY_NAME" - info "✓ cpx v$version installed to $INSTALL_DIR/$BINARY_NAME" + info "✓ copy v$version installed to $INSTALL_DIR/$BINARY_NAME" # Check if in PATH if ! echo "$PATH" | grep -q "$INSTALL_DIR"; then @@ -124,14 +124,14 @@ install_cpx() { # Main main() { echo "╔═══════════════════════════════════╗" - echo "║ cpx - Modern File Copy Tool ║" + echo "║ copy - Modern File Copy Tool ║" echo "╚═══════════════════════════════════╝" echo "" - install_cpx + install_copy echo "" - echo "To get started, run: cpx --help" + echo "To get started, run: copy --help" } main diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 0000000..853ed47 --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,42 @@ +# Build definition for `copy`, shared by the flake's per-system package and the +# overlay (so the derivation lives in exactly one place). Invoke with +# `pkgs.callPackage ./nix/package.nix { }`. +{ lib, rustPlatform }: + +let + # Only the tracked sources — drop build outputs and VCS data so the store + # path (and therefore the build) stays reproducible. + src = lib.cleanSourceWith { + name = "copy-source"; + src = ../.; + filter = path: _type: + let base = baseNameOf (toString path); + in !( + base == "target" + || base == "result" + || lib.hasPrefix "result-" base + || base == ".git" + ); + }; +in +rustPlatform.buildRustPackage { + pname = "copy"; + version = "0.1.4"; # keep in sync with Cargo.toml + + inherit src; + + cargoLock.lockFile = ../Cargo.lock; # committed lockfile -> no vendor hash + + # Default features need no system libraries. SELinux xattr preservation is + # opt-in; to enable it, uncomment: + # buildFeatures = [ "selinux-support" ]; + # buildInputs = [ libselinux ]; + + meta = { + description = "A modern, fast file copying tool"; + homepage = "https://github.com/UnbreakableMJ/copy"; + license = lib.licenses.mit; + mainProgram = "copy"; + platforms = lib.platforms.linux; + }; +} diff --git a/packaging/aur/.SRCINFO b/packaging/aur/.SRCINFO new file mode 100644 index 0000000..8dba692 --- /dev/null +++ b/packaging/aur/.SRCINFO @@ -0,0 +1,14 @@ +pkgbase = copy + pkgdesc = A modern, fast file copying tool + pkgver = 0.1.4 + pkgrel = 1 + url = https://github.com/UnbreakableMJ/copy + arch = x86_64 + arch = aarch64 + license = MIT + makedepends = cargo + depends = gcc-libs + source = copy-0.1.4.tar.gz::https://github.com/UnbreakableMJ/copy/archive/refs/tags/v0.1.4.tar.gz + sha256sums = SKIP + +pkgname = copy diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD new file mode 100644 index 0000000..2aec20e --- /dev/null +++ b/packaging/aur/PKGBUILD @@ -0,0 +1,41 @@ +# Maintainer: Mohamed Hammad +pkgname=copy +pkgver=0.1.4 +pkgrel=1 +pkgdesc="A modern, fast file copying tool" +arch=('x86_64' 'aarch64') +url="https://github.com/UnbreakableMJ/copy" +license=('MIT') +depends=('gcc-libs') +makedepends=('cargo') +source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz") +# Replace with `updpkgsums` once the v$pkgver tag is published. +sha256sums=('SKIP') + +prepare() { + cd "$pkgname-$pkgver" + export RUSTUP_TOOLCHAIN=stable + cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')" +} + +build() { + cd "$pkgname-$pkgver" + export RUSTUP_TOOLCHAIN=stable + export CARGO_TARGET_DIR=target + # Default feature set (no system libraries). To build with SELinux xattr + # preservation, add `libselinux` to depends/makedepends and append + # `--features selinux-support` below. + cargo build --frozen --release +} + +check() { + cd "$pkgname-$pkgver" + export RUSTUP_TOOLCHAIN=stable + cargo test --frozen --release +} + +package() { + cd "$pkgname-$pkgver" + install -Dm0755 "target/release/$pkgname" "$pkgdir/usr/bin/$pkgname" + install -Dm0644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" +} diff --git a/src/cli/args.rs b/src/cli/args.rs index 0995091..ddb5c9b 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -1,7 +1,7 @@ use crate::config::config_command::ConfigCommand; use crate::config::loader::{load_config, load_config_file}; use crate::config::schema::Config; -use crate::error::{CpxError, CpxResult}; +use crate::error::{CliError, CliResult}; use crate::utility::helper::parse_progress_bar; use crate::utility::progress_bar::ProgressOptions; use crate::utility::{ @@ -9,7 +9,7 @@ use crate::utility::{ helper::{parse_backup_mode, parse_follow_symlink, parse_reflink_mode, parse_symlink_mode}, preserve::PreserveAttr, }; -use clap::{Args, Parser, Subcommand, ValueEnum}; +use clap::{Parser, Subcommand, ValueEnum}; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; @@ -47,9 +47,6 @@ pub enum FollowSymlink { #[derive(Debug, Subcommand)] pub enum Commands { - /// Default (Implicit) - Copy(CopyArgs), - /// Manage configuration Config { #[command(subcommand)] @@ -57,15 +54,27 @@ pub enum Commands { }, } +/// Dedicated parser for the `config` subcommand. +/// +/// Copy is the root action, so its positional `sources` are a greedy variadic +/// that would otherwise swallow `config show` as two source paths. When the +/// first argument is `config` we parse it through this grammar instead, which +/// has no copy positionals to compete with the subcommand. #[derive(Parser, Debug)] -#[command(name = "cpx",version = env!("CARGO_PKG_VERSION"))] -pub struct CLIArgs { +#[command(name = "copy")] +struct ConfigInvocation { #[command(subcommand)] - pub command: Commands, + command: Commands, } -#[derive(Args, Debug, Clone)] -pub struct CopyArgs { +#[derive(Parser, Debug)] +#[command( + name = "copy", + version = env!("CARGO_PKG_VERSION"), + args_conflicts_with_subcommands = true, + subcommand_negates_reqs = true +)] +pub struct CLIArgs { // Input/Output Options #[arg(help = "Source file(s) or directory(ies)", required = true)] pub sources: Vec, @@ -203,6 +212,9 @@ pub struct CopyArgs { #[arg(long, help = "Ignore all config files")] pub no_config: bool, + + #[command(subcommand)] + pub command: Option, } #[derive(Debug, Clone)] @@ -273,70 +285,63 @@ impl CopyOptions { } } -impl From<&CopyArgs> for CopyOptions { - fn from(cli: &CopyArgs) -> Self { - Self { - recursive: cli.recursive, - parallel: cli.parallel, - resume: cli.resume, - force: cli.force, - interactive: cli.interactive, - parents: cli.parents, - preserve: match &cli.preserve { - None => PreserveAttr::none(), - Some(s) => { - PreserveAttr::from_string(s).expect("unable to parse preserve attribute") - } - }, - attributes_only: cli.attributes_only, - remove_destination: cli.remove_destination, - symbolic_link: cli.symbolic_link, - hard_link: cli.hard_link, - follow_symlink: FollowSymlink::NoDereference, - progress_bar: ProgressOptions::default(), - backup: cli.backup, - reflink: cli.reflink, - exclude_rules: None, - abort: Arc::new(AtomicBool::new(false)), - } - } -} - impl CLIArgs { - /// Parse arguments with implicit copy command support + /// Parse command-line arguments. + /// + /// Copy is the root action (`copy `); `config` is the only + /// subcommand. Because the copy `sources` positional is a greedy variadic, + /// it would consume `config show` as two source paths, so we special-case a + /// leading `config` and route it through [`ConfigInvocation`] instead. The + /// `command` field is still declared on `CLIArgs` so `--help` lists `config`. pub fn parse() -> Self { - let mut args: Vec = std::env::args().collect(); + let argv: Vec = std::env::args().collect(); + if argv.get(1).map(String::as_str) == Some("config") { + let ConfigInvocation { command } = ::parse_from(argv); + return Self::with_config(command); + } + ::parse_from(argv) + } - if args.len() > 1 { - let first_arg = &args[1]; - let is_subcommand = matches!( - first_arg.as_str(), - "config" | "copy" | "-h" | "--help" | "-V" | "--version" - ); - if !is_subcommand { - args.insert(1, "copy".to_string()); - return ::parse_from(args); - } + /// Build a config-mode invocation: copy fields take their defaults and the + /// parsed `config` subcommand is carried in `command` for `validate` to run. + fn with_config(command: Commands) -> Self { + Self { + sources: Vec::new(), + destination: PathBuf::new(), + target_directory: None, + exclude: Vec::new(), + recursive: false, + parallel: 4, + resume: false, + force: false, + interactive: false, + parents: false, + attributes_only: false, + remove_destination: false, + symbolic_link: None, + hard_link: false, + no_dereference: false, + dereference: false, + dereference_command_line: false, + preserve: None, + backup: None, + reflink: None, + config: None, + no_config: false, + command: Some(command), } - ::parse() } - pub fn validate(self) -> CpxResult<(Vec, PathBuf, CopyOptions)> { - // Handle config command - if let Commands::Config { command } = &self.command { + pub fn validate(self) -> CliResult<(Vec, PathBuf, CopyOptions)> { + // Handle the config subcommand (exits the process when present). + if let Some(Commands::Config { command }) = &self.command { command.execute().map_err(|e| { - CpxError::Validation(format!("Failed to execute config command: {}", e)) + CliError::Validation(format!("Failed to execute config command: {}", e)) })?; std::process::exit(0); } - // Get copy args from the Copy subcommand - let copy_args = match self.command { - Commands::Copy(args) => args, - _ => unreachable!(), - }; - - let config = load_config_if_needed(©_args).map_err(CpxError::Config)?; + let config = load_config_if_needed(&self).map_err(CliError::Config)?; // Start with config or defaults let mut options = if let Some(ref cfg) = config { @@ -346,46 +351,46 @@ impl CLIArgs { }; // CLI args override config - apply_cli_overrides(&mut options, ©_args).map_err(CpxError::Validation)?; + apply_cli_overrides(&mut options, &self).map_err(CliError::Validation)?; // Build exclude rules let all_patterns = - build_all_exclude_patterns(©_args, config.as_ref()).map_err(CpxError::Exclude)?; - options.exclude_rules = build_exclude_rules(all_patterns).map_err(CpxError::Exclude)?; + build_all_exclude_patterns(&self, config.as_ref()).map_err(CliError::Exclude)?; + options.exclude_rules = build_exclude_rules(all_patterns).map_err(CliError::Exclude)?; // Validate conflicts - validate_conflicts(&options).map_err(CpxError::Validation)?; + validate_conflicts(&options).map_err(CliError::Validation)?; // Handle attributes_only special case if options.attributes_only { options.preserve = PreserveAttr::all(); } - let (sources, destination) = if let Some(target) = copy_args.target_directory { - let mut sources = copy_args.sources; - sources.push(copy_args.destination); + let (sources, destination) = if let Some(target) = self.target_directory { + let mut sources = self.sources; + sources.push(self.destination); (sources, target) } else { - (copy_args.sources, copy_args.destination) + (self.sources, self.destination) }; Ok((sources, destination, options)) } } -fn load_config_if_needed(copy_args: &CopyArgs) -> crate::error::ConfigResult> { - if copy_args.no_config { +fn load_config_if_needed(cli: &CLIArgs) -> crate::error::ConfigResult> { + if cli.no_config { return Ok(None); } - if let Some(custom_path) = ©_args.config { + if let Some(custom_path) = &cli.config { return Ok(Some(load_config_file(custom_path)?)); } Ok(Some(load_config())) } -fn apply_cli_overrides(options: &mut CopyOptions, copy_args: &CopyArgs) -> Result<(), String> { +fn apply_cli_overrides(options: &mut CopyOptions, copy_args: &CLIArgs) -> Result<(), String> { // Boolean flags - when present, they override if copy_args.recursive { options.recursive = true; @@ -435,7 +440,7 @@ fn apply_cli_overrides(options: &mut CopyOptions, copy_args: &CopyArgs) -> Resul } fn build_all_exclude_patterns( - copy_args: &CopyArgs, + copy_args: &CLIArgs, config: Option<&Config>, ) -> crate::error::ExcludeResult> { let mut all_patterns = Vec::new(); @@ -486,7 +491,7 @@ fn validate_conflicts(options: &CopyOptions) -> Result<(), String> { Ok(()) } -impl CopyArgs { +impl CLIArgs { pub fn follow_symlink_mode(&self) -> Result { match ( self.no_dereference, @@ -519,30 +524,29 @@ mod tests { #[test] fn test_validate_symlink_and_hardlink_conflict() { let args = CLIArgs { - command: Commands::Copy(CopyArgs { - sources: vec![PathBuf::from("source.txt")], - destination: PathBuf::from("dest.txt"), - target_directory: None, - recursive: false, - parallel: 4, - resume: false, - force: false, - interactive: false, - parents: false, - preserve: None, - attributes_only: false, - remove_destination: false, - symbolic_link: Some(SymlinkMode::Auto), - hard_link: true, - dereference: true, - no_dereference: false, - dereference_command_line: false, - backup: None, - reflink: None, - exclude: Vec::new(), - no_config: false, - config: None, - }), + sources: vec![PathBuf::from("source.txt")], + destination: PathBuf::from("dest.txt"), + target_directory: None, + recursive: false, + parallel: 4, + resume: false, + force: false, + interactive: false, + parents: false, + preserve: None, + attributes_only: false, + remove_destination: false, + symbolic_link: Some(SymlinkMode::Auto), + hard_link: true, + dereference: true, + no_dereference: false, + dereference_command_line: false, + backup: None, + reflink: None, + exclude: Vec::new(), + no_config: false, + config: None, + command: None, }; let result = args.validate(); @@ -553,30 +557,29 @@ mod tests { #[test] fn test_validate_symlink_and_resume_conflict() { let args = CLIArgs { - command: Commands::Copy(CopyArgs { - sources: vec![PathBuf::from("source.txt")], - destination: PathBuf::from("dest.txt"), - target_directory: None, - recursive: false, - parallel: 4, - resume: true, - force: false, - interactive: false, - parents: false, - preserve: None, - attributes_only: false, - remove_destination: false, - symbolic_link: Some(SymlinkMode::Auto), - hard_link: false, - dereference: true, - no_dereference: false, - dereference_command_line: false, - backup: None, - reflink: None, - exclude: Vec::new(), - no_config: false, - config: None, - }), + sources: vec![PathBuf::from("source.txt")], + destination: PathBuf::from("dest.txt"), + target_directory: None, + recursive: false, + parallel: 4, + resume: true, + force: false, + interactive: false, + parents: false, + preserve: None, + attributes_only: false, + remove_destination: false, + symbolic_link: Some(SymlinkMode::Auto), + hard_link: false, + dereference: true, + no_dereference: false, + dereference_command_line: false, + backup: None, + reflink: None, + exclude: Vec::new(), + no_config: false, + config: None, + command: None, }; let result = args.validate(); @@ -587,30 +590,29 @@ mod tests { #[test] fn test_validate_hardlink_and_resume_conflict() { let args = CLIArgs { - command: Commands::Copy(CopyArgs { - sources: vec![PathBuf::from("source.txt")], - destination: PathBuf::from("dest.txt"), - target_directory: None, - recursive: false, - parallel: 4, - resume: true, - force: false, - interactive: false, - parents: false, - preserve: None, - attributes_only: false, - remove_destination: false, - symbolic_link: None, - hard_link: true, - dereference: true, - no_dereference: false, - dereference_command_line: false, - backup: None, - reflink: None, - exclude: Vec::new(), - no_config: false, - config: None, - }), + sources: vec![PathBuf::from("source.txt")], + destination: PathBuf::from("dest.txt"), + target_directory: None, + recursive: false, + parallel: 4, + resume: true, + force: false, + interactive: false, + parents: false, + preserve: None, + attributes_only: false, + remove_destination: false, + symbolic_link: None, + hard_link: true, + dereference: true, + no_dereference: false, + dereference_command_line: false, + backup: None, + reflink: None, + exclude: Vec::new(), + no_config: false, + config: None, + command: None, }; let result = args.validate(); @@ -621,30 +623,29 @@ mod tests { #[test] fn test_validate_success() { let args = CLIArgs { - command: Commands::Copy(CopyArgs { - sources: vec![PathBuf::from("source.txt")], - destination: PathBuf::from("dest.txt"), - target_directory: None, - recursive: false, - parallel: 4, - resume: false, - force: false, - interactive: false, - parents: false, - preserve: None, - attributes_only: false, - remove_destination: false, - symbolic_link: None, - hard_link: false, - dereference: true, - no_dereference: false, - dereference_command_line: false, - backup: None, - reflink: None, - exclude: Vec::new(), - no_config: false, - config: None, - }), + sources: vec![PathBuf::from("source.txt")], + destination: PathBuf::from("dest.txt"), + target_directory: None, + recursive: false, + parallel: 4, + resume: false, + force: false, + interactive: false, + parents: false, + preserve: None, + attributes_only: false, + remove_destination: false, + symbolic_link: None, + hard_link: false, + dereference: true, + no_dereference: false, + dereference_command_line: false, + backup: None, + reflink: None, + exclude: Vec::new(), + no_config: false, + config: None, + command: None, }; let result = args.validate(); diff --git a/src/config/config_command.rs b/src/config/config_command.rs index 52638a5..b831fcb 100644 --- a/src/config/config_command.rs +++ b/src/config/config_command.rs @@ -37,9 +37,9 @@ fn init_config(force: bool) -> std::io::Result<()> { "Could not determine config directory", ) })? - .join("cpx"); + .join("copy"); - let config_path = config_dir.join("cpxconfig.toml"); + let config_path = config_dir.join("copyconfig.toml"); // Check if config already exists if config_path.exists() && !force { @@ -83,7 +83,7 @@ fn show_config() -> std::io::Result<()> { if config_files.is_empty() { println!("{} No config files found", "Info:".yellow().bold()); - println!("\nCreate one with: {}", "cpx config init".green()); + println!("\nCreate one with: {}", "copy config init".green()); return Ok(()); } @@ -128,7 +128,7 @@ fn show_paths() -> std::io::Result<()> { let mut effective: Option = None; //Project config - let project = PathBuf::from("./cpxconfig.toml"); + let project = PathBuf::from("./copyconfig.toml"); if project.exists() { effective = Some(project); } @@ -137,7 +137,7 @@ fn show_paths() -> std::io::Result<()> { if effective.is_none() && let Some(config_dir) = dirs::config_dir() { - let user = config_dir.join("cpx").join("cpxconfig.toml"); + let user = config_dir.join("copy").join("copyconfig.toml"); if user.exists() { effective = Some(user); } @@ -146,7 +146,7 @@ fn show_paths() -> std::io::Result<()> { //System config (Unix) #[cfg(unix)] if effective.is_none() { - let system = PathBuf::from("/etc/cpx/config.toml"); + let system = PathBuf::from("/etc/copy/copyconfig.toml"); if system.exists() { effective = Some(system); } @@ -169,8 +169,8 @@ fn show_paths() -> std::io::Result<()> { } fn add_comments_to_config(toml: &str) -> String { - let header = r#"# cpx configuration file -# For more information, see: https://github.com/11happy/cpx/docs/configuration.md + let header = r#"# copy configuration file +# For more information, see: https://github.com/UnbreakableMJ/copy/blob/main/docs/configuration.md "#; diff --git a/src/config/loader.rs b/src/config/loader.rs index 860e4e2..e160599 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs @@ -5,19 +5,19 @@ use std::path::{Path, PathBuf}; pub fn find_config_files() -> Vec { let mut paths = Vec::new(); - let project_config = PathBuf::from("./cpxconfig.toml"); + let project_config = PathBuf::from("./copyconfig.toml"); if project_config.exists() { paths.push(project_config); } if let Some(config_dir) = dirs::config_dir() { - let user_config = config_dir.join("cpx").join("cpxconfig.toml"); + let user_config = config_dir.join("copy").join("copyconfig.toml"); if user_config.exists() { paths.push(user_config); } } #[cfg(unix)] { - let system_config = PathBuf::from("/etc/cpx/cpxconfig.toml"); + let system_config = PathBuf::from("/etc/copy/copyconfig.toml"); if system_config.exists() { paths.push(system_config); } @@ -33,7 +33,7 @@ pub fn load_config_file(path: &Path) -> ConfigResult { /// Load and merge all config files (reverse priority: system < user < project) pub fn load_config() -> Config { - let project = PathBuf::from("./cpxconfig.toml"); + let project = PathBuf::from("./copyconfig.toml"); if project.exists() && let Ok(config) = load_config_file(&project) { @@ -41,7 +41,7 @@ pub fn load_config() -> Config { } if let Some(config_dir) = dirs::config_dir() { - let user = config_dir.join("cpx").join("cpxconfig.toml"); + let user = config_dir.join("copy").join("copyconfig.toml"); if user.exists() && let Ok(config) = load_config_file(&user) { @@ -51,7 +51,7 @@ pub fn load_config() -> Config { #[cfg(unix)] { - let system = PathBuf::from("/etc/cpx/cpxconfig.toml"); + let system = PathBuf::from("/etc/copy/copyconfig.toml"); if system.exists() && let Ok(config) = load_config_file(&system) { diff --git a/src/error.rs b/src/error.rs index 9feb4c4..cd87973 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,7 +3,7 @@ use std::io; use std::path::PathBuf; #[derive(Debug)] -pub enum CpxError { +pub enum CliError { Io(io::Error), Config(ConfigError), Copy(CopyError), @@ -61,17 +61,17 @@ pub enum PreserveError { FailedToPreserve { path: PathBuf, attribute: String }, } -impl fmt::Display for CpxError { +impl fmt::Display for CliError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - CpxError::Io(e) => write!(f, "IO error: {}", e), - CpxError::Config(e) => write!(f, "Configuration error: {}", e), - CpxError::Copy(e) => write!(f, "Copy error: {}", e), - CpxError::Exclude(e) => write!(f, "Exclude pattern error: {}", e), - CpxError::Preserve(e) => write!(f, "Preserve attribute error: {}", e), - CpxError::Validation(msg) => write!(f, "Validation error: {}", msg), - CpxError::OperationCancelled => write!(f, "Operation cancelled"), - CpxError::InvalidPath(path) => write!(f, "Invalid path: {}", path.display()), + CliError::Io(e) => write!(f, "IO error: {}", e), + CliError::Config(e) => write!(f, "Configuration error: {}", e), + CliError::Copy(e) => write!(f, "Copy error: {}", e), + CliError::Exclude(e) => write!(f, "Exclude pattern error: {}", e), + CliError::Preserve(e) => write!(f, "Preserve attribute error: {}", e), + CliError::Validation(msg) => write!(f, "Validation error: {}", msg), + CliError::OperationCancelled => write!(f, "Operation cancelled"), + CliError::InvalidPath(path) => write!(f, "Invalid path: {}", path.display()), } } } @@ -177,14 +177,14 @@ impl fmt::Display for PreserveError { } } -impl std::error::Error for CpxError { +impl std::error::Error for CliError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { - CpxError::Io(e) => Some(e), - CpxError::Config(e) => Some(e), - CpxError::Copy(e) => Some(e), - CpxError::Exclude(e) => Some(e), - CpxError::Preserve(e) => Some(e), + CliError::Io(e) => Some(e), + CliError::Config(e) => Some(e), + CliError::Copy(e) => Some(e), + CliError::Exclude(e) => Some(e), + CliError::Preserve(e) => Some(e), _ => None, } } @@ -228,33 +228,33 @@ impl std::error::Error for PreserveError { } // Conversion traits -impl From for CpxError { +impl From for CliError { fn from(e: io::Error) -> Self { - CpxError::Io(e) + CliError::Io(e) } } -impl From for CpxError { +impl From for CliError { fn from(e: ConfigError) -> Self { - CpxError::Config(e) + CliError::Config(e) } } -impl From for CpxError { +impl From for CliError { fn from(e: CopyError) -> Self { - CpxError::Copy(e) + CliError::Copy(e) } } -impl From for CpxError { +impl From for CliError { fn from(e: ExcludeError) -> Self { - CpxError::Exclude(e) + CliError::Exclude(e) } } -impl From for CpxError { +impl From for CliError { fn from(e: PreserveError) -> Self { - CpxError::Preserve(e) + CliError::Preserve(e) } } @@ -312,7 +312,7 @@ impl From for PreserveError { } // Result type alias -pub type CpxResult = Result; +pub type CliResult = Result; pub type ConfigResult = Result; pub type CopyResult = Result; pub type ExcludeResult = Result; diff --git a/src/main.rs b/src/main.rs index 725299a..a3849fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ -use cpx::cli::args::CLIArgs; -use cpx::core::copy::{copy, multiple_copy}; -use cpx::error::CpxError; +use copy::cli::args::CLIArgs; +use copy::core::copy::{copy, multiple_copy}; +use copy::error::CliError; use signal_hook::consts::signal::*; use signal_hook::iterator::Signals; use std::process; @@ -24,7 +24,7 @@ fn main() { options.abort = abort.clone(); let mut signals = Signals::new([SIGINT, SIGTERM]) - .map_err(CpxError::Io) + .map_err(CliError::Io) .unwrap_or_else(|e| { eprintln!("Failed to setup signal handler: {}", e); process::exit(1); @@ -58,7 +58,7 @@ fn main() { // interrupt check if abort.load(Ordering::Relaxed) { eprintln!("\nOperation interrupted"); - eprintln!("Resume with: cpx --resume [original command]"); + eprintln!("Resume with: copy --resume [original command]"); eprintln!("Completed files will be skipped automatically"); process::exit(130); // SIGINT } else { diff --git a/src/utility/preprocess.rs b/src/utility/preprocess.rs index 674147f..b5f1446 100644 --- a/src/utility/preprocess.rs +++ b/src/utility/preprocess.rs @@ -144,7 +144,7 @@ impl CopyPlan { } pub fn sort_files_descending(&mut self) { - self.files.sort_by(|a, b| b.size.cmp(&a.size)); + self.files.sort_by_key(|b| std::cmp::Reverse(b.size)); } pub fn merge(&mut self, other: CopyPlan) { diff --git a/tests/gnu/abuse.sh b/tests/gnu/abuse.sh index d21fa1c..94dec87 100755 --- a/tests/gnu/abuse.sh +++ b/tests/gnu/abuse.sh @@ -1,7 +1,7 @@ -# Ensure CPX does not write through a just-created symlink during recursive copy +# Ensure Copy does not write through a just-created symlink during recursive copy # # Inspired by GNU coreutils test: tests/cp/abuse.sh -# Independent reimplementation for CPX. +# Independent reimplementation for Copy. set -eu @@ -16,7 +16,7 @@ echo payload > b/1 # Case 1: dangling destination rm -f t -if cpx --no-dereference --preserve=links -r a/1 b/1 c 2>/dev/null; then +if copy --no-dereference --preserve=links -r a/1 b/1 c 2>/dev/null; then echo "ERROR: unexpected success with dangling destination" fail=1 fi @@ -25,7 +25,7 @@ test ! -f t || fail=1 # Case 2: existing destination echo i > t -if cpx --no-dereference --preserve=links -r a/1 b/1 c 2>/dev/null; then +if copy --no-dereference --preserve=links -r a/1 b/1 c 2>/dev/null; then echo "ERROR: unexpected success with existing destination" fail=1 fi diff --git a/tests/gnu/acl.sh b/tests/gnu/acl.sh index 03c1c99..f6e8067 100755 --- a/tests/gnu/acl.sh +++ b/tests/gnu/acl.sh @@ -2,13 +2,13 @@ # and make sure acls are preserved appropriately # # Inspired by GNU coreutils test: tests/cp/acl.sh -# Independent reimplementation for CPX. +# Independent reimplementation for Copy. set -eu fail=0 -command -v cpx >/dev/null 2>&1 || exit 77 +command -v copy >/dev/null 2>&1 || exit 77 command -v getfacl >/dev/null 2>&1 || exit 77 command -v setfacl >/dev/null 2>&1 || exit 77 @@ -26,7 +26,7 @@ setfacl -m user:bin:rw- a/file 2> /dev/null || skip=yes test $skip = yes && exit 77 # copy a file without preserving permissions -cpx a/file b/ || fail=1 +copy a/file b/ || fail=1 acl2=$(cd b && getfacl file) || fail=1 test "$acl1" = "$acl2" || fail=1 @@ -34,14 +34,14 @@ test "$acl1" = "$acl2" || fail=1 acl1=$(cd a && getfacl file) || fail=1 # copy a file, preserving permissions -cpx --preserve=mode,ownership,timestamps a/file b/ || fail=1 +copy --preserve=mode,ownership,timestamps a/file b/ || fail=1 acl2=$(cd b && getfacl file) || fail=1 test "$acl1" = "$acl2" || fail=1 # copy a file, preserving permissions, with --attributes-only echo > a/file || exit 1 test -s a/file || exit 1 -cpx --preserve=mode,ownership,timestamps --attributes-only a/file b/ || fail=1 +copy --preserve=mode,ownership,timestamps --attributes-only a/file b/ || fail=1 cmp /dev/null b/file || fail=1 acl2=$(cd b && getfacl file) || fail=1 test "$acl1" = "$acl2" || fail=1 diff --git a/tests/gnu/capability.sh b/tests/gnu/capability.sh old mode 100755 new mode 100644 index e87f675..4cf1d18 --- a/tests/gnu/capability.sh +++ b/tests/gnu/capability.sh @@ -1,17 +1,17 @@ -# Ensure cpx --preserves copies capabilities +# Ensure copy --preserves copies capabilities # # Inspired by GNU coreutils test: tests/cp/capability.sh -# Independent reimplementation for CPX. +# Independent reimplementation for Copy. set -eu fail=0 -CPX="${CPX_PATH:-$(command -v cpx 2>/dev/null || echo "")}" -if [ -z "$CPX" ]; then - for candidate in "$HOME/.local/bin/cpx" "$HOME/.cargo/bin/cpx" "/usr/local/bin/cpx" "/usr/bin/cpx"; do - [ -x "$candidate" ] && CPX="$candidate" && break +COPY="${COPY_PATH:-$(command -v copy 2>/dev/null || echo "")}" +if [ -z "$COPY" ]; then + for candidate in "$HOME/.local/bin/copy" "$HOME/.cargo/bin/copy" "/usr/local/bin/copy" "/usr/bin/copy"; do + [ -x "$candidate" ] && COPY="$candidate" && break done fi -[ -x "$CPX" ] || { echo "SKIP: cpx not found"; exit 0; } +[ -x "$COPY" ] || { echo "SKIP: copy not found"; exit 77; } [ "$(id -u)" -eq 0 ] || { echo "SKIP: must run as root"; exit 0; } command -v setcap >/dev/null 2>&1 || { echo "SKIP: setcap not found"; exit 0; } @@ -25,10 +25,10 @@ chown $NON_ROOT_USERNAME file || { echo "FAIL: chown failed"; exit 1; } setcap 'cap_net_bind_service=ep' file || { echo "SKIP: setcap doesn't work"; exit 0; } getcap file | grep -q cap_net_bind_service || { echo "SKIP: getcap doesn't work"; exit 0; } -$CPX --preserve=xattr file copy1 || fail=1 +$COPY --preserve=xattr file copy1 || fail=1 # Before coreutils 8.5 the capabilities would not be preserved, # as the owner was set _after_ copying xattrs, thus clearing any capabilities. -$CPX --preserve=all file copy2 || fail=1 +$COPY --preserve=all file copy2 || fail=1 for file in copy1 copy2; do getcap $file | grep -q cap_net_bind_service || fail=1 diff --git a/tests/gnu/cpx-HL.sh b/tests/gnu/copy-HL.sh similarity index 81% rename from tests/gnu/cpx-HL.sh rename to tests/gnu/copy-HL.sh index 8d7d3d9..bd33d84 100755 --- a/tests/gnu/cpx-HL.sh +++ b/tests/gnu/copy-HL.sh @@ -1,13 +1,13 @@ -# test cpx's -H and -L options +# test copy's -H and -L options # Test that -H dereferences command-line symlinks but preserves others # # Inspired by GNU coreutils test: tests/cp/cp-HL.sh -# Independent reimplementation for CPX. +# Independent reimplementation for Copy. set -eu fail=0 -command -v cpx >/dev/null 2>&1 || exit 77 +command -v copy >/dev/null 2>&1 || exit 77 tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT @@ -19,7 +19,7 @@ ln -s f slink || exit 1 ln -s no-such-file src-dir/slink || exit 1 # Copy with -H: dereference command-line symlinks only -cpx -H -r slink src-dir dest-dir || fail=1 +copy -H -r slink src-dir dest-dir || fail=1 test -d src-dir || fail=1 test -d dest-dir/src-dir || fail=1 diff --git a/tests/gnu/cpx-deref.sh b/tests/gnu/copy-deref.sh similarity index 73% rename from tests/gnu/cpx-deref.sh rename to tests/gnu/copy-deref.sh index d591e5a..1a8504e 100755 --- a/tests/gnu/cpx-deref.sh +++ b/tests/gnu/copy-deref.sh @@ -1,15 +1,15 @@ -# cpx -L -r dir1 dir2' must handle the case in which each of dir1 and dir2 +# copy -L -r dir1 dir2' must handle the case in which each of dir1 and dir2 # contain a symlink pointing to some third directory. # Test that dereferencing symlinks works correctly when multiple sources # have symlinks to the same directory. # # Inspired by GNU coreutils test: tests/cp/cp-deref.sh -# Independent reimplementation for CPX. +# Independent reimplementation for Copy. set -eu fail=0 -command -v cpx >/dev/null 2>&1 || exit 77 +command -v copy >/dev/null 2>&1 || exit 77 tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT @@ -21,7 +21,7 @@ ln -s ../c b || exit 1 # Copy with -L: dereference all symlinks # This should not fail with "will not create hard link" error -cpx -L -r a b d || fail=1 +copy -L -r a b d || fail=1 test -d a/c || fail=1 test -d b/c || fail=1 diff --git a/tests/gnu/cpx-i.sh b/tests/gnu/copy-i.sh similarity index 71% rename from tests/gnu/cpx-i.sh rename to tests/gnu/copy-i.sh index 33467c2..65dec95 100755 --- a/tests/gnu/cpx-i.sh +++ b/tests/gnu/copy-i.sh @@ -1,14 +1,14 @@ #!/bin/sh -# Test whether cpx -i prompts in the right place. +# Test whether copy -i prompts in the right place. # Test interactive mode prompting behavior with various flag combinations. # # Inspired by GNU coreutils test: tests/cp/cp-i.sh -# Independent reimplementation for CPX. +# Independent reimplementation for Copy. set -eu fail=0 -command -v cpx >/dev/null 2>&1 || exit 77 +command -v copy >/dev/null 2>&1 || exit 77 tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT @@ -17,9 +17,9 @@ cd "$tmp" mkdir -p a b/a/c || exit 1 touch a/c || exit 1 -# cpx should prompt when overwriting in interactive mode +# copy should prompt when overwriting in interactive mode # Answer 'n' should result in non-zero exit and file should not be overwritten -echo n | cpx -i -r a b 2>/dev/null && fail=1 +echo n | copy -i -r a b 2>/dev/null && fail=1 # Verify the original file was not overwritten test -e b/a/c || fail=1 @@ -32,18 +32,18 @@ echo "original" > d || exit 1 echo "new content" > c || exit 1 # ask for overwrite, answer no - file should remain unchanged -echo n | cpx -i c d 2>/dev/null && fail=1 +echo n | copy -i c d 2>/dev/null && fail=1 test "$(cat d)" = "original" || fail=1 # ask for overwrite, answer yes - file should be overwritten -echo y | cpx -i c d 2>/dev/null || fail=1 +echo y | copy -i c d 2>/dev/null || fail=1 test "$(cat d)" = "new content" || fail=1 # Reset for next test echo "original" > d || exit 1 # -f with -i: should still prompt (last option wins or -i takes precedence) -echo y | cpx -f -i c d 2>/dev/null || fail=1 +echo y | copy -f -i c d 2>/dev/null || fail=1 test "$(cat d)" = "new content" || fail=1 exit $fail diff --git a/tests/intergration.rs b/tests/intergration.rs index 688dc28..d098a0b 100644 --- a/tests/intergration.rs +++ b/tests/intergration.rs @@ -16,7 +16,7 @@ fn test_copy_single_file() { source.write_str("Hello, World!").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source.path()) .arg(dest.path()) .assert() @@ -34,7 +34,7 @@ fn test_copy_single_file_to_directory() { source.write_str("Test content").unwrap(); dest_dir.create_dir_all().unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source.path()) .arg(dest_dir.path()) .assert() @@ -54,7 +54,7 @@ fn test_copy_multiple_files_traditional() { file2.write_str("Content 2").unwrap(); dest_dir.create_dir_all().unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(file1.path()) .arg(file2.path()) .arg(dest_dir.path()) @@ -76,7 +76,7 @@ fn test_copy_with_target_directory_flag() { file2.write_str("Content 2").unwrap(); dest_dir.create_dir_all().unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-t") .arg(dest_dir.path()) .arg(file1.path()) @@ -97,7 +97,7 @@ fn test_copy_directory_without_recursive_flag() { source_dir.create_dir_all().unwrap(); source_dir.child("file.txt").write_str("content").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source_dir.path()) .arg(dest_dir.path()) .assert() @@ -120,7 +120,7 @@ fn test_copy_directory_recursive() { subdir.create_dir_all().unwrap(); subdir.child("file3.txt").write_str("content3").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-r") .arg(source_dir.path()) .arg(dest_dir.path()) @@ -147,7 +147,7 @@ fn test_copy_with_resume_flag() { dest.write_str("Same content").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("--resume") .arg(source.path()) .arg(dest_dir.path()) @@ -176,7 +176,7 @@ fn test_copy_with_force_flag() { fs::set_permissions(dest.path(), perms).unwrap(); } - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-f") .arg(source.path()) .arg(dest.path()) @@ -199,7 +199,7 @@ fn test_copy_with_parallel() { files.push(file); } - let mut cmd = Command::new(cargo::cargo_bin!("cpx")); + let mut cmd = Command::new(cargo::cargo_bin!("copy")); cmd.arg("-j").arg("2").arg("-t").arg(dest_dir.path()); for file in &files { @@ -220,7 +220,7 @@ fn test_invalid_source() { let temp = assert_fs::TempDir::new().unwrap(); let dest = temp.child("dest.txt"); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("/nonexistent/file.txt") .arg(dest.path()) .assert() @@ -233,7 +233,7 @@ fn test_missing_destination() { let source = temp.child("source.txt"); source.write_str("content").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source.path()) .assert() .failure() @@ -246,7 +246,7 @@ fn test_target_directory_must_exist() { let source = temp.child("source.txt"); source.write_str("content").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-t") .arg("/nonexistent/directory") .arg(source.path()) @@ -263,7 +263,7 @@ fn test_copy_preserves_content_integrity() { let binary_data: Vec = (0..=255).cycle().take(10240).collect(); fs::write(source.path(), &binary_data).unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source.path()) .arg(dest.path()) .assert() @@ -282,7 +282,7 @@ fn test_copy_large_file() { let large_content = "x".repeat(5 * 1024 * 1024); fs::write(source.path(), &large_content).unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source.path()) .arg(dest.path()) .assert() @@ -305,7 +305,7 @@ fn test_symlink_mode_auto_relative() { let original_dir = std::env::current_dir().unwrap(); std::env::set_current_dir(temp.path()).unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-s") .arg("auto") .arg("source.txt") @@ -332,7 +332,7 @@ fn test_symlink_mode_absolute() { source.write_str("content").unwrap(); dest_dir.create_dir_all().unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-s") .arg("absolute") .arg(source.path()) @@ -356,7 +356,7 @@ fn test_symlink_directory_recursive() { source_dir.child("file1.txt").write_str("content1").unwrap(); source_dir.child("file2.txt").write_str("content2").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-r") .arg("-s") .arg("relative") @@ -398,7 +398,7 @@ fn test_preserve_existing_symlink() { symlink(actual_file.path(), source_link.path()).unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-P") // no-dereference .arg(source_link.path()) .arg(dest_dir.path()) @@ -422,7 +422,7 @@ fn test_hardlink_single_file() { source.write_str("content").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-l") .arg(source.path()) .arg(dest.path()) @@ -448,7 +448,7 @@ fn test_hardlink_multiple_files() { file2.write_str("content2").unwrap(); dest_dir.create_dir_all().unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-l") .arg(file1.path()) .arg(file2.path()) @@ -472,7 +472,7 @@ fn test_backup_simple() { source.write_str("new content").unwrap(); dest.write_str("old content").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-b") .arg("simple") .arg(source.path()) @@ -493,7 +493,7 @@ fn test_backup_numbered() { source.write_str("version 1").unwrap(); dest.write_str("version 0").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-b") .arg("numbered") .arg(source.path()) @@ -505,7 +505,7 @@ fn test_backup_numbered() { source.write_str("version 2").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-b") .arg("numbered") .arg(source.path()) @@ -526,7 +526,7 @@ fn test_backup_existing_mode() { dest.write_str("old").unwrap(); // First backup with existing mode (no numbered backups exist) - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-b") .arg("existing") .arg(source.path()) @@ -543,7 +543,7 @@ fn test_backup_existing_mode() { dest.write_str("new").unwrap(); // Now it should use numbered - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-b") .arg("existing") .arg(source.path()) @@ -567,7 +567,7 @@ fn test_preserve_mode() { perms.set_mode(0o755); fs::set_permissions(source.path(), perms).unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-p") .arg("mode") .arg(source.path()) @@ -589,7 +589,7 @@ fn test_preserve_timestamps() { std::thread::sleep(std::time::Duration::from_millis(100)); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-p") .arg("timestamps") .arg(source.path()) @@ -618,7 +618,7 @@ fn test_attributes_only() { source.write_str("source content").unwrap(); dest.write_str("dest content").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("--attributes-only") .arg(source.path()) .arg(dest.path()) @@ -644,7 +644,7 @@ fn test_exclude_basename() { dest_dir.create_dir_all().unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-r") .arg("-e") .arg("node_modules") @@ -667,7 +667,7 @@ fn test_exclude_glob_pattern() { source_dir.child("temp.tmp").write_str("exclude").unwrap(); source_dir.child("cache.tmp").write_str("exclude").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-r") .arg("-e") .arg("*.tmp") @@ -693,7 +693,7 @@ fn test_exclude_multiple_patterns() { source_dir.child("file.log").write_str("exclude").unwrap(); source_dir.child(".git").create_dir_all().unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-r") .arg("-e") .arg("*.tmp,*.log,.git") @@ -726,7 +726,7 @@ fn test_exclude_relative_path() { .unwrap(); source_dir.child("other.txt").write_str("keep").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-r") .arg("-e") .arg("subdir/exclude.txt") @@ -754,7 +754,7 @@ fn test_parents_flag() { let original_dir = std::env::current_dir().unwrap(); std::env::set_current_dir(temp.path()).unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("--parents") .arg("a/b/c/file.txt") .arg("dest") @@ -782,7 +782,7 @@ fn test_parents_multiple_files_absolute() { let file2 = file2_dir.child("file2.txt"); file2.write_str("content2").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("--parents") .arg(file1.path()) .arg(file2.path()) @@ -812,7 +812,7 @@ fn test_dereference_command_line() { let dest_dir = temp.child("dest"); dest_dir.create_dir_all().unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-r") .arg("-H") .arg(symlink_dir.path()) @@ -839,7 +839,7 @@ fn test_dereference_always() { let dest_dir = temp.child("dest"); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-r") .arg("-L") .arg(source_dir.path()) @@ -860,7 +860,7 @@ fn test_symlink_hardlink_conflict() { source.write_str("content").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-s") .arg("-l") .arg(source.path()) @@ -878,7 +878,7 @@ fn test_symlink_resume_conflict() { source.write_str("content").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-s") .arg("--resume") .arg(source.path()) @@ -896,7 +896,7 @@ fn test_dereference_flags_conflict() { source.write_str("content").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-P") .arg("-L") .arg(source.path()) @@ -914,7 +914,7 @@ fn test_copy_empty_file() { source.write_str("").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source.path()) .arg(dest.path()) .assert() @@ -932,7 +932,7 @@ fn test_copy_to_existing_directory() { source.write_str("content").unwrap(); dest_dir.create_dir_all().unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source.path()) .arg(dest_dir.path()) .assert() @@ -950,7 +950,7 @@ fn test_copy_directory_to_file_fails() { source_dir.create_dir_all().unwrap(); dest_file.write_str("existing").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-r") .arg(source_dir.path()) .arg(dest_file.path()) @@ -967,7 +967,7 @@ fn test_copy_special_characters_in_filename() { source.write_str("content").unwrap(); dest_dir.create_dir_all().unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source.path()) .arg(dest_dir.path()) .assert() @@ -990,7 +990,7 @@ fn test_copy_nested_directories() { .write_str("deep content") .unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-r") .arg(temp.child("a").path()) .arg(dest_dir.path()) @@ -1009,7 +1009,7 @@ fn test_remove_destination_flag() { source.write_str("new").unwrap(); dest.write_str("old").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("--remove-destination") .arg(source.path()) .arg(dest.path()) @@ -1029,7 +1029,7 @@ fn test_copy_very_long_filename() { source.write_str("content").unwrap(); dest_dir.create_dir_all().unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source.path()) .arg(dest_dir.path()) .assert() @@ -1042,7 +1042,7 @@ fn test_copy_very_long_filename() { fn test_config_init() { let temp = assert_fs::TempDir::new().unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("config") .arg("init") .env("HOME", temp.path()) @@ -1050,7 +1050,7 @@ fn test_config_init() { .assert() .success(); - let config_path = temp.path().join(".config/cpx/cpxconfig.toml"); + let config_path = temp.path().join(".config/copy/copyconfig.toml"); assert!(config_path.exists()); let contents = fs::read_to_string(&config_path).unwrap(); @@ -1062,13 +1062,13 @@ fn test_config_init() { #[test] fn test_config_init_force_overwrite() { let temp = assert_fs::TempDir::new().unwrap(); - let config_dir = temp.path().join(".config/cpx"); + let config_dir = temp.path().join(".config/copy"); fs::create_dir_all(&config_dir).unwrap(); - let config_path = config_dir.join("cpxconfig.toml"); + let config_path = config_dir.join("copyconfig.toml"); fs::write(&config_path, "old config").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("config") .arg("init") .arg("--force") @@ -1083,7 +1083,7 @@ fn test_config_init_force_overwrite() { #[test] fn test_config_show() { - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("config") .arg("show") .assert() @@ -1092,7 +1092,7 @@ fn test_config_show() { #[test] fn test_config_path() { - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("config") .arg("path") .assert() @@ -1102,10 +1102,10 @@ fn test_config_path() { #[test] fn test_no_config_flag() { let temp = assert_fs::TempDir::new().unwrap(); - let config_dir = temp.path().join(".config/cpx"); + let config_dir = temp.path().join(".config/copy"); fs::create_dir_all(&config_dir).unwrap(); - let config_path = config_dir.join("cpxconfig.toml"); + let config_path = config_dir.join("copyconfig.toml"); fs::write( &config_path, r#" @@ -1130,7 +1130,7 @@ force = true } // With --no-config, should fail without force - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("--no-config") .arg(source.path()) .arg(dest.path()) @@ -1167,7 +1167,7 @@ fn test_resume_skips_identical_files() { .write_str("new content") .unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-r") .arg("--resume") .arg(source_dir.path()) @@ -1189,7 +1189,7 @@ fn test_resume_with_size_mismatch() { let dest_file = dest_dir.child("source.txt"); dest_file.write_str("old").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("--resume") .arg(source.path()) .arg(dest_dir.path()) @@ -1208,7 +1208,7 @@ fn test_reflink_auto() { source.write_str("reflink content").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("--reflink") .arg("auto") .arg(source.path()) @@ -1228,7 +1228,7 @@ fn test_reflink_never() { source.write_str("content").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("--reflink") .arg("never") .arg(source.path()) @@ -1255,7 +1255,7 @@ fn test_copy_multiple_large_files() { files.push(file); } - let mut cmd = Command::new(cargo::cargo_bin!("cpx")); + let mut cmd = Command::new(cargo::cargo_bin!("copy")); cmd.arg("-j").arg("2").arg("-t").arg(dest_dir.path()); for file in &files { @@ -1288,7 +1288,7 @@ fn test_copy_file_with_different_buffer_sizes() { let content = vec![42u8; size]; fs::write(source.path(), &content).unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source.path()) .arg(dest.path()) .assert() @@ -1307,7 +1307,7 @@ fn test_implicit_copy_command() { source.write_str("implicit").unwrap(); // Should work without explicit "copy" subcommand - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source.path()) .arg(dest.path()) .assert() @@ -1316,27 +1316,9 @@ fn test_implicit_copy_command() { dest.assert("implicit"); } -#[test] -fn test_explicit_copy_command() { - let temp = assert_fs::TempDir::new().unwrap(); - let source = temp.child("source.txt"); - let dest = temp.child("dest.txt"); - - source.write_str("explicit").unwrap(); - - Command::new(cargo::cargo_bin!("cpx")) - .arg("copy") - .arg(source.path()) - .arg(dest.path()) - .assert() - .success(); - - dest.assert("explicit"); -} - #[test] fn test_help_flag() { - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("--help") .assert() .success() @@ -1345,7 +1327,7 @@ fn test_help_flag() { #[test] fn test_version_flag() { - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("--version") .assert() .success(); @@ -1353,8 +1335,7 @@ fn test_version_flag() { #[test] fn test_copy_help() { - Command::new(cargo::cargo_bin!("cpx")) - .arg("copy") + Command::new(cargo::cargo_bin!("copy")) .arg("--help") .assert() .success() @@ -1376,7 +1357,7 @@ fn test_copy_readonly_source() { perms.set_mode(0o444); fs::set_permissions(source.path(), perms).unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source.path()) .arg(dest.path()) .assert() @@ -1393,7 +1374,7 @@ fn test_destination_parent_not_exist() { source.write_str("content").unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source.path()) .arg(dest.path()) .assert() @@ -1415,7 +1396,7 @@ fn test_copy_with_multiple_flags() { .write_str("content3") .unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-r") .arg("-f") .arg("-p") @@ -1451,7 +1432,7 @@ fn test_copy_dotfiles() { .write_str("config") .unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-r") .arg(source_dir.path()) .arg(dest_dir.path()) @@ -1471,7 +1452,7 @@ fn test_copy_unicode_filenames() { source.write_str("unicode content").unwrap(); dest_dir.create_dir_all().unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg(source.path()) .arg(dest_dir.path()) .assert() @@ -1488,7 +1469,7 @@ fn test_copy_empty_directory() { source_dir.create_dir_all().unwrap(); - Command::new(cargo::cargo_bin!("cpx")) + Command::new(cargo::cargo_bin!("copy")) .arg("-r") .arg(source_dir.path()) .arg(dest_dir.path())