diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b3b5a9..f4d1135 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,60 @@ on: branches: - master jobs: + rustfmt: + name: rustfmt + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + profile: minimal + components: rustfmt + - name: Check formatting + run: | + cargo fmt --all -- --check + + readme: + name: readme + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + # cache + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ~/.cargo/.crates.toml + ~/.cargo/.crates2.json + target/ + key: ${{ runner.os }}-cargo-readme + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + profile: minimal + components: rustfmt + - name: Check readme + run: | + ci/install-packages-ubuntu.sh + cargo install just + just setup + just readme + rustup install nightly + just fmt + just check-dirty + test: + needs: ['rustfmt', 'readme'] name: test env: # For some builds, we use cross to test on 32-bit and big-endian @@ -30,12 +83,10 @@ jobs: - beta # Our release builds are generated by a nightly compiler to take # advantage of the latest optimizations/compile time improvements. So - # we test all of them here. (We don't do mips releases, but test on - # mips for big-endian coverage.) + # we test all of them here. - nightly - nightly-musl - nightly-32 - - nightly-mips - nightly-arm - macos - win-msvc @@ -43,8 +94,8 @@ jobs: include: - build: pinned os: ubuntu-latest - # NOTE: ratatui requires 1.65.0 - rust: 1.65.0 + # NOTE: ripgrep requires 1.72.0 + rust: 1.72.0 - build: stable os: ubuntu-latest rust: stable @@ -56,22 +107,18 @@ jobs: # NOTE: we pin to a specific version since sometimes things fail to compile on the latest nightly builds - build: nightly os: ubuntu-latest - rust: nightly-2023-04-16 + rust: nightly-2023-12-04 - build: nightly-musl os: ubuntu-latest - rust: nightly-2023-04-16 + rust: nightly-2023-12-04 target: x86_64-unknown-linux-musl - build: nightly-32 os: ubuntu-latest - rust: nightly-2023-04-16 + rust: nightly-2023-12-04 target: i686-unknown-linux-gnu - - build: nightly-mips - os: ubuntu-latest - rust: nightly-2023-04-16 - target: mips64-unknown-linux-gnuabi64 - build: nightly-arm os: ubuntu-latest - rust: nightly-2023-04-16 + rust: nightly-2023-12-04 # For stripping release binaries: # docker run --rm -v $PWD/target:/target:Z \ # rustembedded/cross:arm-unknown-linux-gnueabihf \ @@ -80,19 +127,32 @@ jobs: target: arm-unknown-linux-gnueabihf - build: macos os: macos-latest - rust: nightly-2023-04-16 + rust: nightly-2023-12-04 - build: win-msvc os: windows-2019 - rust: nightly-2023-04-16 + rust: nightly-2023-12-04 - build: win-gnu os: windows-2019 - rust: nightly-2023-04-16-x86_64-gnu + rust: nightly-2023-12-04-x86_64-gnu steps: - name: Checkout repository uses: actions/checkout@v3 with: submodules: recursive + # cache + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ~/.cargo/.crates.toml + ~/.cargo/.crates2.json + target/ + key: ${{ runner.os }}-cargo-${{ matrix.build }} + - name: Install packages (Ubuntu) if: matrix.os == 'ubuntu-latest' run: | @@ -147,60 +207,3 @@ jobs: - name: Run tests (with cross) if: matrix.target != '' run: ${{ env.CARGO }} test --verbose --all ${{ env.TARGET_FLAGS }} - - - name: Test for existence of build artifacts (Windows) - if: matrix.os == 'windows-2019' - shell: bash - run: | - outdir="$(ci/cargo-out-dir.sh "${{ env.TARGET_DIR }}")" - ls "$outdir/_rgr.ps1" && file "$outdir/_rgr.ps1" - - - name: Test for existence of build artifacts (Unix) - if: matrix.os != 'windows-2019' - shell: bash - run: | - outdir="$(ci/cargo-out-dir.sh "${{ env.TARGET_DIR }}")" - for f in rgr.1 _rgr rgr.bash rgr.fish; do - # We could use file -E here, but it isn't supported on macOS. - ls "$outdir/$f" && file "$outdir/$f" - done - - rustfmt: - name: rustfmt - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - profile: minimal - components: rustfmt - - name: Check formatting - run: | - cargo fmt --all -- --check - - readme: - name: readme - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - override: true - profile: minimal - components: rustfmt - - name: Check readme - run: | - ci/install-packages-ubuntu.sh - cargo install just - just setup - just readme - rustup install nightly - just fmt - just check-dirty diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 568b1bc..ebdfd3d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,31 +73,31 @@ jobs: # NOTE: we pin to a specific version since sometimes things fail to compile on the latest nightly builds - build: linux-gnu os: ubuntu-latest - rust: nightly-2023-04-16 + rust: nightly-2023-12-04 target: x86_64-unknown-linux-gnu - build: linux-musl os: ubuntu-latest - rust: nightly-2023-04-16 + rust: nightly-2023-12-04 target: x86_64-unknown-linux-musl - build: linux-arm-gnueabihf os: ubuntu-latest - rust: nightly-2023-04-16 + rust: nightly-2023-12-04 target: arm-unknown-linux-gnueabihf - build: macos os: macos-latest - rust: nightly-2023-04-16 + rust: nightly-2023-12-04 target: x86_64-apple-darwin - build: win-msvc os: windows-2019 - rust: nightly-2023-04-16 + rust: nightly-2023-12-04 target: x86_64-pc-windows-msvc - build: win-gnu os: windows-2019 - rust: nightly-2023-04-16-x86_64-gnu + rust: nightly-2023-12-04-x86_64-gnu target: x86_64-pc-windows-gnu - build: win32-msvc os: windows-2019 - rust: nightly-2023-04-16 + rust: nightly-2023-12-04 target: i686-pc-windows-msvc steps: @@ -125,6 +125,25 @@ jobs: override: true target: ${{ matrix.target }} + # cache + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ~/.cargo/.crates.toml + ~/.cargo/.crates2.json + target/ + key: ${{ runner.os }}-cargo-${{ matrix.target }} + + # We install ripgrep via cargo to get the latest version (we need at least 14) + # since ubuntu is still on 13 (at least it is while this is being written...) + # We need 14 in order to generate completion files. + - name: Install ripgrep + run: cargo install ripgrep + - name: Use Cross run: | cargo install cross @@ -184,6 +203,11 @@ jobs: staging="repgrep-${{ env.RELEASE_VERSION }}-${{ matrix.target }}" mkdir -p "$staging"/{complete,doc} + echo 'complete -F _rg rgr' > "${outdir}/rgr.bash" + echo 'compdef $_comps[rg] rgr' > "${outdir}/_rgr" + rg --generate complete-fish | sed 's/-c rg/-c rgr/' > "${outdir}/rgr.fish" + rg --generate complete-powershell | sed "s/'rg'/'rgr'/" > "${outdir}/_rgr.ps1" + cp {LICENSE-*,CHANGELOG.md,README.md} "$staging/doc/" cp "$outdir"/{_rgr,rgr.bash,rgr.fish,_rgr.ps1} "$staging/complete/" diff --git a/.hidden b/.hidden new file mode 100644 index 0000000..d61df21 --- /dev/null +++ b/.hidden @@ -0,0 +1 @@ +is this hidden? diff --git a/CHANGELOG.md b/CHANGELOG.md index 796437c..5748fbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# 0.15.0 + +- fae13fc don't treat patterns as regexes if --fixed-strings is passed +- fae1e2b refactor argument parsing and remove (broken) stdin things +- fae1696 fix an issue with capturing not working as expected +- fae11c0 ensure we capture -h, -v and --version +- fae1ed5 update how completions are generated since we no longer use clap +- fae1341 support reading RIPGREP_CONFIG_FILE for arguments +- fae1567 update README.md +- fae1d8f update DEVELOPMENT_NOTES.md +- fae1b324 update ci to run fmt and readme checks before tests +- 946b8f5 Merge pull request #95 from herbygillot/docs-macports-install +- fae1b328 update docs with macports instructions +- 1741e0d docs: add MacPorts install instructions + # 0.14.3 - 2b2e896 Merge pull request #93 from a-kenji/chore/move-to-ratatui diff --git a/Cargo.lock b/Cargo.lock index 02fcb41..a2e47a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,55 +32,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" -[[package]] -name = "anstream" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is-terminal", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" - -[[package]] -name = "anstyle-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys 0.48.0", -] - -[[package]] -name = "anstyle-wincon" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" -dependencies = [ - "anstyle", - "windows-sys 0.48.0", -] - [[package]] name = "anyhow" version = "1.0.72" @@ -208,56 +159,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "bitflags 1.3.2", - "clap_lex 0.2.4", + "clap_lex", "indexmap", "textwrap", ] -[[package]] -name = "clap" -version = "4.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" -dependencies = [ - "clap_builder", - "clap_derive", - "once_cell", -] - -[[package]] -name = "clap_builder" -version = "4.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" -dependencies = [ - "anstream", - "anstyle", - "clap_lex 0.5.0", - "once_cell", - "strsim", -] - -[[package]] -name = "clap_complete" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" -dependencies = [ - "clap 4.3.19", -] - -[[package]] -name = "clap_derive" -version = "4.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "clap_lex" version = "0.2.4" @@ -267,18 +173,6 @@ dependencies = [ "os_str_bytes", ] -[[package]] -name = "clap_lex" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" - -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - [[package]] name = "console" version = "0.15.7" @@ -327,7 +221,7 @@ dependencies = [ "atty", "cast", "ciborium", - "clap 3.2.25", + "clap", "criterion-plot", "itertools", "lazy_static", @@ -574,12 +468,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "hermit-abi" version = "0.1.19" @@ -694,6 +582,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lexopt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baff4b617f7df3d896f97fe922b64817f6cd9a756bb81d40f8883f2f66dcb401" + [[package]] name = "libc" version = "0.2.147" @@ -979,13 +873,11 @@ checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "repgrep" -version = "0.14.3" +version = "0.15.0" dependencies = [ "anyhow", "base64-simd", "chardet", - "clap 4.3.19", - "clap_complete", "const_format", "criterion", "crossbeam-queue", @@ -995,6 +887,7 @@ dependencies = [ "flexi_logger", "hex", "insta", + "lexopt", "log", "memmap", "num_cpus", @@ -1124,12 +1017,6 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "syn" version = "2.0.27" @@ -1214,12 +1101,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - [[package]] name = "vsimd" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 15caaf3..8b69a36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "repgrep" -version = "0.14.3" +version = "0.15.0" description = "An interactive command line replacer for `ripgrep`." homepage = "https://github.com/acheronfail/repgrep" repository = "https://github.com/acheronfail/repgrep" @@ -23,12 +23,12 @@ path = "src/main.rs" anyhow = "1.0.37" base64-simd = { version = "0.8.0", features = ["detect"] } chardet = "0.2.4" -clap = { version = "4.1.9", features = ["cargo", "derive"] } const_format = "0.2.11" crossterm = "0.26.1" either = "1.6.1" encoding = "0.2.33" flexi_logger = "0.25.3" +lexopt = "0.3.0" log = "0.4.11" regex = "1.8.4" safe-transmute = "0.11.0" @@ -39,10 +39,6 @@ tempfile = "3.1.0" ratatui = { version = "0.22.0", default-features = false, features = ["crossterm"] } unicode-width = "0.1.8" -[build-dependencies] -clap = { version = "4.1.9", features = ["cargo", "derive"] } -clap_complete = "4.1.5" - [dev-dependencies] criterion = { version = "0.4.0", features = ["html_reports"] } crossbeam-queue = "0.3.8" diff --git a/DEVELOPMENT_NOTES.md b/DEVELOPMENT_NOTES.md index 7a45f35..db8e62e 100644 --- a/DEVELOPMENT_NOTES.md +++ b/DEVELOPMENT_NOTES.md @@ -33,6 +33,7 @@ Once the doc comments have been updated, run `just readme` to apply the changes Some guidelines: * All development is done on the `next` branch, so please target that for any Pull Request. + * (Exceptions can be made for PRs that only update the README or don't change any code) * Make sure to run `just fmt` on each commit so formatting is consistent * Make sure to also use `just test` to ensure each commit passes the tests diff --git a/README.md b/README.md index 03ffc43..7861b0e 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ See the [releases] page for pre-compiled binaries. #### Via Cargo -**NOTE**: The minimum Rust version required is `1.46.0`. +**NOTE**: The minimum Rust version required is `1.72.0`. ```bash cargo install repgrep @@ -68,8 +68,6 @@ cargo install repgrep #### Via Pacman (Arch Linux) -Maintained by [orhun](https://github.com/orhun). - [`repgrep`](https://archlinux.org/packages/extra/x86_64/repgrep/) can be installed from the official repositories using [Pacman](https://wiki.archlinux.org/title/Pacman). @@ -87,9 +85,9 @@ sudo port install repgrep More info [here](https://ports.macports.org/port/repgrep/). -#### From Source (via Cargo) +#### From Source -**NOTE**: The minimum Rust version required is `1.65.0`. +**NOTE**: The minimum Rust version required is `1.72.0`. ```bash git clone https://github.com/acheronfail/repgrep/ diff --git a/build.rs b/build.rs index 4a89c2f..d50aa25 100644 --- a/build.rs +++ b/build.rs @@ -1,14 +1,7 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; use std::{env, fs, io}; -use clap::CommandFactory; -use clap_complete::{generate_to, shells}; - -#[allow(dead_code)] -#[path = "src/cli/args.rs"] -mod cli; - fn generate_manpage>(outdir: P) -> io::Result<()> { // If asciidoctor isn't installed, don't do anything. // This is for platforms where it's unsupported. @@ -43,31 +36,12 @@ fn generate_manpage>(outdir: P) -> io::Result<()> { fn main() { // https://doc.rust-lang.org/cargo/reference/build-scripts.html#outputs-of-the-build-script let outdir = env::var_os("OUT_DIR").expect("failed to find OUT_DIR"); + let outdir = PathBuf::from(outdir); fs::create_dir_all(&outdir).expect("failed to create dirs for OUT_DIR"); // Create a stamp file. (This is used by CI to help find the OUT_DIR.) fs::write(Path::new(&outdir).join("repgrep-stamp"), "").unwrap(); - // Generate completions. - let mut app = cli::Args::command(); - macro_rules! gen { - ($shell:expr) => {{ - let path = generate_to( - $shell, &mut app, // We need to specify what generator to use - "rgr", // We need to specify the bin name manually - &outdir, // We need to specify where to write to - ) - .expect("failed to generate completion"); - - println!("cargo:warning=completion file generated: {:?}", path); - }}; - } - - gen!(shells::Bash); - gen!(shells::Elvish); - gen!(shells::Fish); - gen!(shells::PowerShell); - gen!(shells::Zsh); // Generate manpage. generate_manpage(&outdir).unwrap(); } diff --git a/ci/install-packages-macos.sh b/ci/install-packages-macos.sh index bdce7cc..dc0955c 100755 --- a/ci/install-packages-macos.sh +++ b/ci/install-packages-macos.sh @@ -1,3 +1,3 @@ #!/bin/sh -brew install asciidoctor ripgrep +brew install asciidoctor diff --git a/ci/install-packages-ubuntu.sh b/ci/install-packages-ubuntu.sh index 8419d2b..4f65d63 100755 --- a/ci/install-packages-ubuntu.sh +++ b/ci/install-packages-ubuntu.sh @@ -1,3 +1,3 @@ #!/bin/sh -sudo apt-get install -y --no-install-recommends asciidoctor ripgrep +sudo apt-get install -y --no-install-recommends asciidoctor diff --git a/config b/config new file mode 100644 index 0000000..91e6a90 --- /dev/null +++ b/config @@ -0,0 +1,2 @@ +--hidden +--sort=path diff --git a/src/cli/args.rs b/src/cli/args.rs deleted file mode 100644 index ae9be61..0000000 --- a/src/cli/args.rs +++ /dev/null @@ -1,397 +0,0 @@ -/// Note that while we duplicate a log of ripgrep's command line options here, we don't appear to -/// use many of them at all. This is because the Parser arguments defined here act as a whitelist of -/// supported ripgrep options: if this fails to pass, then the user has passed some options which we -/// don't yet support. -/// -/// We do use some of this information, for instance the `encoding` is sniffed from the argument -/// parsing we do here. -use std::env; -use std::ffi::OsString; -use std::path::PathBuf; - -use clap::{crate_authors, crate_version, ArgAction, Parser}; - -// TODO: options to support in the future -// -P/--pcre2 -// -F/--fixed-strings -// -f/--file - -/// See `rg --help` for more detailed information on each of the flags passed. -/// -/// Providing no arguments will make repgrep read JSON input from STDIN. -#[derive(Parser, Debug)] -#[clap( - version = crate_version!(), - author = crate_authors!(), -)] -pub struct Args { - // - // RIPGREP ARGUMENTS - // - - // POSITIONAL - /// The pattern to search. Required unless patterns are passed via -e/--regexp. - #[clap(name = "PATTERN")] - pub pattern: Option, - /// The paths in which to search. - #[clap(name = "PATH")] - pub paths: Vec, - /// Used to provide multiple patterns. - #[clap( - short = 'e', - long = "regexp", - num_args = 1.., - number_of_values = 1 - )] - pub patterns: Vec, - - // FLAGS - /// How many lines of context should be shown after each match. - #[clap(short = 'A', long = "after-context")] - pub after_context: Option, - /// How many lines of context should be shown before each match. - #[clap(short = 'B', long = "before-context")] - pub before_context: Option, - /// How many lines of context should be shown before and after each match. - #[clap(short = 'C', long = "context")] - pub context: Option, - /// Treat CRLF ('\r\n') as a line terminator. - #[clap(long = "crlf")] - pub crlf: bool, - /// Provide the encoding to use when searching files. - #[clap(short = 'E', long = "encoding")] - pub encoding: Option, - /// Follow symlinks. - #[clap(short = 'L', long = "follow")] - pub follow_symlinks: bool, - /// Ignore case when searching. - #[clap(short = 'i', long = "ignore-case")] - pub ignore_case: bool, - /// Invert the matches on each line. - #[clap(short = 'v', long = "invert-match")] - pub invert_match: bool, - /// Print both matching and non-matching lines. - #[clap(long = "passthru")] - pub passthru: bool, - /// Use smart case matching. - #[clap(short = 'S', long = "smart-case")] - pub smart_case: bool, - /// Use case sensitive matching. - #[clap(short = 's', long = "case-sensitive")] - pub case_sensitive: bool, - /// Sort the results (ascending). - #[clap(long = "sort")] - pub sort: Option, - /// Sort the results (descending). - #[clap(long = "sortr")] - pub sortr: Option, - /// How many threads to use. - #[clap(short = 'j', long = "threads")] - pub threads: Option, - /// Trim leading/trailing whitespace. - #[clap(long = "trim")] - pub trim: bool, - /// Search only a specific type of file. - #[clap( - short = 't', - long = "type", - num_args = 1.., - number_of_values = 1 - )] - pub r#type: Vec, - /// Inverse of --type. - #[clap( - short = 'T', - long = "type-not", - num_args = 1.., - number_of_values = 1 - )] - pub type_not: Vec, - /// Set the "unrestricted" searching options for ripgrep. - /// Note that this is currently limited to only two occurrences `-uu` since - /// binary searching is not supported in repgrep. - #[clap(short = 'u', long = "unrestricted", action = ArgAction::Count)] - pub unrestricted: u8, - /// Allow matches to span multiple lines. - #[clap(short = 'U', long = "multiline")] - pub multiline: bool, - /// Allow the "." character to span across lines with multiline searches. - #[clap(long = "multiline-dotall")] - pub multiline_dotall: bool, - /// When matching, use a word boundary search. - #[clap(short = 'w', long = "word-regexp")] - pub word_regexp: bool, - - // FILES & IGNORES - /// A list of globs to match files. - #[clap( - short = 'g', - long = "glob", - num_args = 1.., - number_of_values = 1 - )] - pub glob: Vec, - /// A list of case insensitive globs to match files. - #[clap(long = "iglob", num_args = 1.., number_of_values = 1)] - pub iglob: Vec, - /// Search hidden files. - #[clap(short = '.', long = "hidden")] - pub hidden: bool, - /// Use the given ignore file when searching. - #[clap(long = "ignore-file")] - pub ignore_file: Option, - /// When given an --ignore-file, read its rules case insensitively. - #[clap(long = "ignore-file-case-insensitive")] - pub ignore_file_case_insensitive: bool, - /// Don't traverse filesystems for each path specified. - #[clap(long = "one-file-system")] - pub one_file_system: bool, -} - -impl Args { - /// Provides the command line arguments to pass down to ripgrep. - /// At the moment this just proxies down _all_ command line arguments (excluding the program name) - /// directly to ripgrep. We assume that the arguments contain a supported set of flags and options - /// since we'll have used Parser to parse this struct and validate our program's arguments. - pub fn rg_args(&self) -> impl Iterator { - // Skip the first argument, which _should_ be the binary name. - env::args_os().skip(1) - } -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use clap::{CommandFactory, Parser}; - - use super::Args; - - #[test] - fn verify_cli() { - Args::command().debug_assert() - } - - #[test] - fn verify_pattern() { - let args = Args::parse_from(&["rgr", "foobar"]); - assert_eq!(args.pattern, Some(String::from("foobar"))); - } - - #[test] - fn verify_paths() { - let args = Args::parse_from(&["rgr", "foobar", "/tmp", "/dev"]); - assert_eq!( - args.paths, - vec![PathBuf::from("/tmp"), PathBuf::from("/dev")] - ); - } - - #[test] - fn verify_patterns() { - let args = Args::parse_from(&["rgr", "-e=foobar", "-e", "pattern"]); - assert_eq!( - args.patterns, - vec![String::from("foobar"), String::from("pattern")] - ); - } - - #[test] - fn verify_after_context() { - let args = Args::parse_from(&["rgr", ".", "-A5"]); - assert_eq!(args.after_context, Some(5)); - } - - #[test] - fn verify_before_context() { - let args = Args::parse_from(&["rgr", ".", "-B=10"]); - assert_eq!(args.before_context, Some(10)); - } - - #[test] - fn verify_context() { - let args = Args::parse_from(&["rgr", ".", "-C", "42"]); - assert_eq!(args.context, Some(42)); - } - - #[test] - fn verify_crlf() { - let args = Args::parse_from(&["rgr", ".", "--crlf"]); - assert_eq!(args.crlf, true); - } - - #[test] - fn verify_encoding() { - let args = Args::parse_from(&["rgr", ".", "-Eutf-8"]); - assert_eq!(args.encoding, Some(String::from("utf-8"))); - let args = Args::parse_from(&["rgr", ".", "--encoding", "utf-16"]); - assert_eq!(args.encoding, Some(String::from("utf-16"))); - } - - #[test] - fn verify_follow_symlinks() { - let args = Args::parse_from(&["rgr", ".", "-L"]); - assert_eq!(args.follow_symlinks, true); - let args = Args::parse_from(&["rgr", ".", "--follow"]); - assert_eq!(args.follow_symlinks, true); - } - - #[test] - fn verify_ignore_case() { - let args = Args::parse_from(&["rgr", ".", "-i"]); - assert_eq!(args.ignore_case, true); - let args = Args::parse_from(&["rgr", ".", "--ignore-case"]); - assert_eq!(args.ignore_case, true); - } - - #[test] - fn verify_invert_match() { - let args = Args::parse_from(&["rgr", ".", "-v"]); - assert_eq!(args.invert_match, true); - let args = Args::parse_from(&["rgr", ".", "--invert-match"]); - assert_eq!(args.invert_match, true); - } - - #[test] - fn verify_passthru() { - let args = Args::parse_from(&["rgr", ".", "--passthru"]); - assert_eq!(args.passthru, true); - } - - #[test] - fn verify_smart_case() { - let args = Args::parse_from(&["rgr", ".", "-S"]); - assert_eq!(args.smart_case, true); - let args = Args::parse_from(&["rgr", ".", "--smart-case"]); - assert_eq!(args.smart_case, true); - } - - #[test] - fn verify_case_sensitive() { - let args = Args::parse_from(&["rgr", ".", "-s"]); - assert_eq!(args.case_sensitive, true); - let args = Args::parse_from(&["rgr", ".", "--case-sensitive"]); - assert_eq!(args.case_sensitive, true); - } - - #[test] - fn verify_sort() { - let args = Args::parse_from(&["rgr", ".", "--sort=path"]); - assert_eq!(args.sort, Some(String::from("path"))); - } - - #[test] - fn verify_sortr() { - let args = Args::parse_from(&["rgr", ".", "--sortr=created"]); - assert_eq!(args.sortr, Some(String::from("created"))); - } - - #[test] - fn verify_threads() { - let args = Args::parse_from(&["rgr", ".", "-j12"]); - assert_eq!(args.threads, Some(12)); - let args = Args::parse_from(&["rgr", ".", "--threads=4"]); - assert_eq!(args.threads, Some(4)); - } - - #[test] - fn verify_trim() { - let args = Args::parse_from(&["rgr", ".", "--trim"]); - assert_eq!(args.trim, true); - } - - #[test] - fn verify_type() { - let args = Args::parse_from(&["rgr", ".", "-tcss"]); - assert_eq!(args.r#type, vec![String::from("css")]); - let args = Args::parse_from(&["rgr", ".", "-tcss", "--type=html"]); - assert_eq!(args.r#type, vec![String::from("css"), String::from("html")]); - } - - #[test] - fn verify_type_not() { - let args = Args::parse_from(&["rgr", ".", "-Tcss"]); - assert_eq!(args.type_not, vec![String::from("css")]); - let args = Args::parse_from(&["rgr", ".", "-Tcss", "--type-not=html"]); - assert_eq!( - args.type_not, - vec![String::from("css"), String::from("html")] - ); - } - - #[test] - fn verify_unrestricted() { - let args = Args::parse_from(&["rgr", ".", "-u"]); - assert_eq!(args.unrestricted, 1); - let args = Args::parse_from(&["rgr", ".", "-uu"]); - assert_eq!(args.unrestricted, 2); - let args = Args::parse_from(&["rgr", ".", "--unrestricted"]); - assert_eq!(args.unrestricted, 1); - } - - #[test] - fn verify_multiline() { - let args = Args::parse_from(&["rgr", ".", "--multiline"]); - assert_eq!(args.multiline, true); - } - - #[test] - fn verify_multiline_dotall() { - let args = Args::parse_from(&["rgr", ".", "--multiline-dotall"]); - assert_eq!(args.multiline_dotall, true); - } - - #[test] - fn verify_word_regexp() { - let args = Args::parse_from(&["rgr", ".", "-w"]); - assert_eq!(args.word_regexp, true); - let args = Args::parse_from(&["rgr", ".", "--word-regexp"]); - assert_eq!(args.word_regexp, true); - } - - #[test] - fn verify_glob() { - let args = Args::parse_from(&["rgr", ".", "-g=asdf"]); - assert_eq!(args.glob, vec![String::from("asdf")]); - let args = Args::parse_from(&["rgr", ".", "-g=asdf", "--glob", "qwerty"]); - assert_eq!( - args.glob, - vec![String::from("asdf"), String::from("qwerty")] - ); - } - - #[test] - fn verify_iglob() { - let args = Args::parse_from(&["rgr", ".", "--iglob=zxcv"]); - assert_eq!(args.iglob, vec![String::from("zxcv")]); - } - - #[test] - fn verify_hidden() { - let args = Args::parse_from(&["rgr", ".", "-."]); - assert_eq!(args.hidden, true); - let args = Args::parse_from(&["rgr", ".", "--hidden"]); - assert_eq!(args.hidden, true); - } - - #[test] - fn verify_ignore_file() { - let args = Args::parse_from(&["rgr", ".", "--ignore-file=my/path/to/.gitignore"]); - assert_eq!( - args.ignore_file, - Some(PathBuf::from("my/path/to/.gitignore")) - ); - } - - #[test] - fn verify_ignore_file_case_insensitive() { - let args = Args::parse_from(&["rgr", ".", "--ignore-file-case-insensitive"]); - assert_eq!(args.ignore_file_case_insensitive, true); - } - - #[test] - fn verify_one_file_system() { - let args = Args::parse_from(&["rgr", ".", "--one-file-system"]); - assert_eq!(args.one_file_system, true); - } -} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 07319d2..cfad7e8 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,127 +1,449 @@ -mod args; +use std::{fs, process}; -use std::env; -use std::path::PathBuf; - -use anyhow::{anyhow, Result}; -use args::Args; -use clap::{CommandFactory, Parser}; +use anyhow::{bail, Result}; +use lexopt::Parser; pub const ENV_JSON_FILE: &str = "RGR_JSON_FILE"; -/// This is where we perform our validation of the arguments. -fn validate_arguments(mut args: Args) -> Result { - // Check we have a pattern. - if args.pattern.is_none() && args.patterns.is_empty() && env::var(ENV_JSON_FILE).is_err() { - return Err(anyhow!("No pattern was provided!")); - } +pub fn print_help() { + println!( + "{}", + format!( + r#" +{crate_name} {crate_version} +{crate_authors} - // If a positional pattern was passed _and_ patterns via flags were passed, then - // assume that the positional pattern is a path. - if args.pattern.is_some() && !args.patterns.is_empty() { - args.paths.push(PathBuf::from(args.pattern.take().unwrap())); - } +{crate_name} ({bin}) is an interactive replacer for ripgrep that makes it easy to find +and replace across files on the command line. - // We don't support binary searches. - if args.unrestricted > 2 { - log::warn!("Binary file searching is not supported. Changing -uuu to -uu"); - args.unrestricted = 2; - } +Project home page: {crate_homepage} + +USAGE: + {bin} ... + {env_file}=path/to/rg.json rgr [REGEX] + +EXAMPLES: + There are different ways to invoke {bin}: + + 1: {bin} ... + In this way, {bin} is a thin wrapper for rg and you may pass any rg arguments + you wish. {bin} will take care of forwarding them to rg and spawn it for you. + + {bin} "foo" + Find and replace all occurrences of "foo". + + {bin} "(f)oo" + Find and replace all occurrences of "foo", but now "$1" will be set to "f". + This uses regular expression capturing groups, for more info, see `rg --help`. + + 2: {env_file}=path/to/rg.json rgr [REGEX] + Alternatively, you may store all the JSON results from rg into a file, and have {bin} read + that file for results when running. When running it this way, only a single optional argument + is used, a regular expression. This is to provide capture group support. + This is mainly used to cache results for expensive or long-running searches. - Ok(args) + rg --json "foo" > rg.json && {env_file}=rg.json {bin} + When run this way, no capturing groups are used (as {bin} is not aware of any pattern). + But all the matches rg returned are displayed, and can be replaced as per usual. + + rg --json "foo" > rg.json && {env_file}=rg.json {bin} "(fo)" + The pattern provided this way will be run on each match, and can be used to provide + capturing group powered replacements. In the above example, providing the replacement + text `$1$1` would result in occurrences of "foo" being replaced with "fofo". +"#, + env_file = ENV_JSON_FILE, + bin = env!("CARGO_BIN_NAME"), + crate_name = env!("CARGO_PKG_NAME"), + crate_version = env!("CARGO_PKG_VERSION"), + crate_homepage = env!("CARGO_PKG_HOMEPAGE"), + crate_authors = env!("CARGO_PKG_AUTHORS") + .split(':') + .collect::>() + .join("\n"), + ) + .trim() + ); } -/// Prints the help generated by clap. -pub fn print_help() { - Args::command().print_help().unwrap(); +#[derive(Debug, PartialEq, Eq)] +enum ExecStyle { + Normal, + Json, } -// Parses arguments from the environment (argv, etc). -pub fn parse_arguments() -> Result { - validate_arguments(Args::parse()) +pub struct RgArgs { + /// All the regular expressions that were passed. We need these since we perform matching + /// ourselves in certain situations when rendering the TUI. + pub patterns: Vec, + /// Any encoding that was passed - we want to force the same encoding that ripgrep uses when + /// we perform any replacements ourselves. + pub encoding: Option, + /// Whether fixed strings was enabled - means we only need to substring search rather than + /// regular expression searching. + /// TODO: this is currently unused, we need to update `replace.rs` to use it + pub fixed_strings: bool, + /// All other args that were passed will be forwarded to ripgrep. + pub other_args: Vec, + + exec_style: ExecStyle, } -impl Args { - /// Returns the patterns used by `rg` in the search. - pub fn rg_patterns(&self) -> Vec<&str> { - if let Some(pattern) = &self.pattern { - vec![pattern] - } else { - self.patterns.iter().map(|p| p.as_ref()).collect() +impl RgArgs { + pub fn rg_cmdline(&self) -> String { + match self.exec_style { + ExecStyle::Normal => self.rg_args().join(" "), + ExecStyle::Json => "JSON".into(), + } + } + + pub fn rg_args(&self) -> Vec { + let mut args = self.other_args.clone(); + if self.fixed_strings { + args.push("--fixed-strings".into()); + } + if let Some(encoding) = &self.encoding { + args.push(format!("--encoding={}", encoding)); + } + for pattern in &self.patterns { + args.push(format!("--regexp={}", pattern)); + } + + args + } + + pub fn parse_pattern() -> Result { + RgArgs::parse_pattern_impl(Parser::from_env()) + } + + fn parse_pattern_impl(mut parser: Parser) -> Result { + use lexopt::prelude::*; + + let mut patterns = vec![]; + + while let Some(arg) = parser.next()? { + match arg { + Value(pat) if patterns.is_empty() => patterns.push(pat.string()?), + _ => { + bail!("{}\nSee --help for usage", arg.unexpected()) + } + } + } + + Ok(RgArgs { + patterns, + encoding: None, + fixed_strings: false, + other_args: vec![], + exec_style: ExecStyle::Json, + }) + } + + pub fn parse_rg_args() -> Result { + RgArgs::parse_rg_args_impl(Parser::from_env()) + } + + // TODO: this implementation assumes UTF-8 (via `String`) for all arguments, but in reality it + // should use `OsString` instead to remove the UTF-8 requirement. + fn parse_rg_args_impl(mut parser: Parser) -> Result { + use lexopt::prelude::*; + + // ripgrep's arguments that we want to know + let mut pattern_positional: Option = None; + let mut patterns: Vec = vec![]; + let mut encoding: Option = None; + let mut fixed_strings = false; + let mut other_args: Vec = vec![]; + + // as per ripgrep's documentation: + // > When -f/--file or -e/--regexp is used, then ripgrep treats all positional arguments as + // > files or directories to search. + let mut positional_disabled = false; + + while let Some(arg) = parser.next()? { + match arg { + // ripgrep: pattern related arguments + Value(pattern) if pattern_positional.is_none() => { + pattern_positional = Some(pattern.string()?); + } + Short('e') | Long("regexp") => { + positional_disabled = true; + patterns.push(parser.value()?.string()?); + } + Short('f') | Long("file") => { + positional_disabled = true; + let path = parser.value()?; + if path == "-" { + bail!("reading stdin for --file arguments is not yet supported in rgr") + } + + let text = fs::read_to_string(path)?; + for pattern in text.lines() { + patterns.push(pattern.into()); + } + } + + // ripgrep: flags + Short('E') | Long("encoding") => { + encoding = Some(parser.value()?.string()?); + } + Short('F') | Long("fixed-strings") => { + fixed_strings = true; + } + Long("no-fixed-strings") => { + fixed_strings = false; + } + + // capture help to display our help + // also important to capture these since they make `rg` not output JSON! + Short('h') | Long("help") => { + print_help(); + process::exit(0); + } + Short('v') | Long("version") => { + println!( + "{crate_name} {crate_version}", + crate_name = env!("CARGO_PKG_NAME"), + crate_version = env!("CARGO_PKG_VERSION") + ); + process::exit(0); + } + + // ripgrep: all other arguments and flags + Short(ch) => other_args.push(format!("-{}", ch)), + Long(name) => { + // at this point we don't know if the argument we're passing is a `--flag` or an + // `--option=something`. So, peek at the next argument (if any) and see if it + // starts with `-`. + let name = name.to_string(); + let next_is_flag = parser + .try_raw_args() + .map(|raw_args| { + raw_args + .peek() + .and_then(|next| next.to_str()) + // if there's no next value, this must be a flag + // if there is a next value, see if it looks like a flag + .map_or(true, |s| s.starts_with('-')) + }) + // if `try_raw_args` failed, then we're passing something with an optional + // value, so that's not a flag + .unwrap_or(false); + + if next_is_flag { + other_args.push(format!("--{}", name)); + } else { + other_args.push(format!("--{}={}", name, parser.value()?.string()?)); + } + } + Value(other) => other_args.push(other.string()?), + } + } + + if let Some(pattern) = pattern_positional { + if positional_disabled { + other_args.push(pattern); + } else { + patterns.push(pattern); + } } + + Ok(RgArgs { + patterns, + fixed_strings, + encoding, + other_args, + exec_style: ExecStyle::Normal, + }) } } #[cfg(test)] mod tests { - use std::ffi::OsString; - use std::path::PathBuf; + use super::*; + use crate::temp_file; - use anyhow::Result; - use clap::Parser; - use pretty_assertions::assert_eq; + macro_rules! parse_pattern { + [$($arg:expr$(,)?)*] => { + RgArgs::parse_pattern_impl(Parser::from_iter(["rgr".to_string(), $($arg.into(),)*])).unwrap() + }; + } - use super::{validate_arguments, Args}; + #[test] + fn pattern_empty() { + let args = parse_pattern![]; + assert!(args.patterns.is_empty()); + assert!(!args.fixed_strings); + assert!(args.other_args.is_empty()); + assert_eq!(args.encoding, None); + assert_eq!(args.exec_style, ExecStyle::Json); + } - /// Parses arguments from a list. - fn parse_arguments_from(itr: I) -> Result - where - I: IntoIterator, - T: Into + Clone, - { - validate_arguments(Args::parse_from(itr)) + #[test] + fn pattern_one() { + let args = parse_pattern!["pattern"]; + assert_eq!(args.patterns, ["pattern"]); } #[test] - fn checks_if_no_pattern_was_passed() { - let args = parse_arguments_from(&["rgr", "-E", "utf8", "-A1", "-B", "10"]); - assert!(args.is_err()); - assert_eq!( - format!("{}", args.unwrap_err()), - String::from("No pattern was provided!") - ); + #[should_panic = "unexpected argument \"unexpected\""] + fn pattern_many() { + parse_pattern!["pattern", "unexpected"]; } #[test] - fn reads_pattern_as_path_if_pattern_flag_given() { - let args = parse_arguments_from(&["rgr", "-e", "pattern-flag", "pattern-pos", "path-pos"]) - .unwrap(); + #[should_panic = "invalid option '--flag'"] + fn pattern_flag() { + parse_pattern!["pattern", "--flag"]; + } - assert_eq!(args.pattern, None); - assert_eq!(args.patterns, vec!["pattern-flag".to_owned()]); - assert_eq!( - args.paths, - vec![PathBuf::from("path-pos"), PathBuf::from("pattern-pos")] - ) + macro_rules! parse_rg { + [$($arg:expr$(,)?)*] => { + RgArgs::parse_rg_args_impl(Parser::from_iter(["rgr".to_string(), $($arg.into(),)*])).unwrap() + }; } #[test] - fn does_not_allow_unrestricted_above_two() { - let args = parse_arguments_from(&["rgr", "-uuu", "pattern-pos"]).unwrap(); - assert_eq!(args.unrestricted, 2); + fn rg_empty() { + let args = parse_rg![]; + assert!(args.patterns.is_empty()); + assert!(!args.fixed_strings); + assert!(args.other_args.is_empty()); + assert_eq!(args.encoding, None); + assert_eq!(args.exec_style, ExecStyle::Normal); } #[test] - fn returns_rg_patterns_flags() { - let args = parse_arguments_from(&[ - "rgr", + fn rg_patterns() { + // only positional + let args = parse_rg!["positional"]; + assert_eq!(args.patterns, ["positional"]); + assert!(args.other_args.is_empty()); + + // positional and --regexp + let args = parse_rg!["positional", "--regexp=e"]; + assert_eq!(args.patterns, ["e"]); + assert_eq!(args.other_args, ["positional"]); + + // positional and multiple --regexp flags + let args = parse_rg![ "-e", - "pattern-flag", - "--regexp", - "pattern-flag-long", - "path-pos", - ]) - .unwrap(); + "e1", + "positional", + "--regexp=e2", + "-e=e3", + "another_positional" + ]; + assert_eq!(args.patterns, ["e1", "e2", "e3"]); + assert_eq!(args.other_args, ["another_positional", "positional"]); + } + + #[test] + fn rg_pattern_files() { + let p = temp_file!("foo\nbar"); + + // just --file + let args = parse_rg![format!("--file={}", p.display())]; + assert_eq!(args.patterns, ["foo", "bar"]); + assert!(args.other_args.is_empty()); + + // with positional + let args = parse_rg![format!("--file={}", p.display()), "positional"]; + assert_eq!(args.patterns, ["foo", "bar"]); + assert_eq!(args.other_args, ["positional"]); + + // with positional and --regexp + let args = parse_rg![ + "positional", + "-e=baz", + format!("--file={}", p.display()), + "another_positional" + ]; + assert_eq!(args.patterns, ["baz", "foo", "bar"]); + assert_eq!(args.other_args, ["another_positional", "positional"]); + } + + #[test] + fn rg_fixed_strings() { + let args = parse_rg!["-F"]; + assert!(args.fixed_strings); + let args = parse_rg!["--fixed-strings"]; + assert!(args.fixed_strings); + + let args = parse_rg!["--fixed-strings", "--no-fixed-strings"]; + assert!(!args.fixed_strings); + } + + #[test] + fn rg_encoding() { + let args = parse_rg![]; + assert_eq!(args.encoding, None); + + let args = parse_rg!["--encoding=utf-16be"]; + assert_eq!(args.encoding.as_deref(), Some("utf-16be")); + + let args = parse_rg!["--encoding", "utf-16le"]; + assert_eq!(args.encoding.as_deref(), Some("utf-16le")); + + let args = parse_rg!["-E", "utf-8"]; + assert_eq!(args.encoding.as_deref(), Some("utf-8")); + + let args = parse_rg!["-Eascii"]; + assert_eq!(args.encoding.as_deref(), Some("ascii")); + } + + #[test] + fn rg_other_args() { + let args = parse_rg![ + "pos1", + "pos2", + "--bool", + "--flag1=val1", + "--flag2", + "val2", + "-a", + "-1" + ]; + assert_eq!(args.patterns, ["pos1"]); assert_eq!( - args.rg_patterns(), - vec!["pattern-flag", "pattern-flag-long"] + args.other_args, + ["pos2", "--bool", "--flag1=val1", "--flag2=val2", "-a", "-1"] + ); + assert!(!args.fixed_strings); + assert!(args.encoding.is_none()); + + assert_eq!( + args.rg_args(), + [ + "pos2", + "--bool", + "--flag1=val1", + "--flag2=val2", + "-a", + "-1", + "--regexp=pos1" + ] + ); + } + + #[test] + fn rg_case1() { + let args = parse_rg!["--sort", "path", "--sort=modified", "foo"]; + assert_eq!( + args.rg_args(), + ["--sort=path", "--sort=modified", "--regexp=foo"] ); } #[test] - fn returns_rg_patterns_positional() { - let args = parse_arguments_from(&["rgr", "positional"]).unwrap(); - assert_eq!(args.rg_patterns(), vec!["positional"]); + fn rg_case2() { + let args = parse_rg!["--flag"]; + assert_eq!(args.rg_args(), ["--flag"]); + + let args = parse_rg!["--flag", "val"]; + assert_eq!(args.rg_args(), ["--flag=val"]); + + let args = parse_rg!["--flag=val"]; + assert_eq!(args.rg_args(), ["--flag=val"]); } } diff --git a/src/main.rs b/src/main.rs index 43610aa..533cc9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,7 +53,7 @@ //! //! ### Via Cargo //! -//! **NOTE**: The minimum Rust version required is `1.46.0`. +//! **NOTE**: The minimum Rust version required is `1.72.0`. //! //! ```bash //! cargo install repgrep @@ -61,8 +61,6 @@ //! //! ### Via Pacman (Arch Linux) //! -//! Maintained by [orhun](https://github.com/orhun). -//! //! [`repgrep`](https://archlinux.org/packages/extra/x86_64/repgrep/) can be installed //! from the official repositories using [Pacman](https://wiki.archlinux.org/title/Pacman). //! @@ -80,9 +78,9 @@ //! //! More info [here](https://ports.macports.org/port/repgrep/). //! -//! ### From Source (via Cargo) +//! ### From Source //! -//! **NOTE**: The minimum Rust version required is `1.65.0`. +//! **NOTE**: The minimum Rust version required is `1.72.0`. //! //! ```bash //! git clone https://github.com/acheronfail/repgrep/ @@ -106,7 +104,6 @@ use std::fs::File; use std::{env, process}; use anyhow::Result; -use clap::crate_name; use flexi_logger::{opt_format, FileSpec, Logger}; use rg::exec::run_ripgrep; use ui::tui::Tui; @@ -114,7 +111,7 @@ use ui::tui::Tui; use crate::rg::read::read_messages; fn init_logging() -> Result<::std::path::PathBuf> { - let log_dir = env::temp_dir().join(format!(".{}", crate_name!())); + let log_dir = env::temp_dir().join(format!(".{}", env!("CARGO_PKG_NAME"))); let log_spec = if cfg!(debug_assertions) { FileSpec::default() .directory(env::current_dir().unwrap()) @@ -154,51 +151,50 @@ fn main() { }; } - let args = match cli::parse_arguments() { - Ok(args) => args, - Err(e) => { - cli::print_help(); - exit_with_error!("\nFailed to parse arguments, error: {}", e); - } - }; - - macro_rules! run_ripgrep { - () => {{ - let display_args = args.rg_args().into_iter().collect::>(); - log::debug!("User args for rg: {:?}", display_args); - run_ripgrep(args.rg_args()) - }}; - } - - let rg_json = match env::var(cli::ENV_JSON_FILE) { - Ok(path) => { - log::debug!( - "Found {}={}, reading messages from file", - cli::ENV_JSON_FILE, - &path - ); - match File::open(path) { - Ok(json_file) => read_messages(json_file), - Err(e) => { - log::warn!("Failed to open file: {}", e); - log::warn!("Falling back to running rg"); - run_ripgrep!() + let (args, rg_json) = { + match env::var_os(cli::ENV_JSON_FILE) { + // check if JSON is being passed as an environment file + Some(path) => { + log::debug!( + "{} set to {}; Reading messages from file", + cli::ENV_JSON_FILE, + path.to_string_lossy() + ); + match File::open(&path) { + Ok(json_file) => { + let args = match cli::RgArgs::parse_pattern() { + Ok(args) => args, + Err(e) => { + exit_with_error!("Failed to parse arguments: {}", e); + } + }; + + (args, read_messages(json_file)) + } + Err(e) => { + exit_with_error!("Failed to open {}: {}", path.to_string_lossy(), e); + } } } + // normal execution, parse rg arguments and call it ourselves + None => { + let args = match cli::RgArgs::parse_rg_args() { + Ok(args) => args, + Err(e) => { + exit_with_error!("Failed to parse arguments: {}", e); + } + }; + + let rg_args = args.rg_args(); + (args, run_ripgrep(rg_args)) + } } - Err(_) => run_ripgrep!(), }; match rg_json { Ok(rg_messages) => { - let rg_cmdline: String = args - .rg_args() - .map(|s| s.to_string_lossy().into_owned()) - .collect::>() - .join(" "); - - let patterns = args.rg_patterns(); - let result = Tui::new().and_then(|tui| tui.start(rg_cmdline, rg_messages, patterns)); + let result = Tui::new() + .and_then(|tui| tui.start(args.rg_cmdline(), rg_messages, &args.patterns)); // Restore terminal. if let Err(err) = Tui::restore_terminal() { @@ -212,11 +208,16 @@ fn main() { // Handle application result. match result { Ok(Some(mut replacement_criteria)) => { - // If we detected an encoding passed to `rg`, then use that. + // use an encoding if one was passed to `rg` if let Some(encoding) = args.encoding { replacement_criteria.set_encoding(encoding); } + // if we're running in fixed strings mode, then we shouldn't treat the patterns as regexes + if args.fixed_strings { + replacement_criteria.capture_pattern = None; + } + match replace::perform_replacements(replacement_criteria) { Ok(_) => {} Err(err) => { diff --git a/src/replace.rs b/src/replace.rs index b25adc5..27a4651 100644 --- a/src/replace.rs +++ b/src/replace.rs @@ -217,13 +217,13 @@ pub fn perform_replacements(criteria: ReplacementCriteria) -> Result<()> { #[cfg(test)] mod tests { use std::fs::{self, OpenOptions}; - use std::io::{Read, Write}; + use std::io::Read; use std::path::PathBuf; + #[cfg(not(any(target_os = "macos", target_os = "windows")))] use base64_simd::STANDARD as base64; use pretty_assertions::assert_eq; use regex::bytes::Regex; - use tempfile::NamedTempFile; use crate::model::*; use crate::replace::perform_replacements; @@ -250,9 +250,11 @@ mod tests { // NOTE: due to permission issues on Windows platforms, we need to first "keep" the temporary files otherwise // we cannot atomically replace them. See https://github.com/Stebalien/tempfile/issues/131 + #[macro_export] macro_rules! temp_file { (bytes, $content:expr) => {{ - let mut file = NamedTempFile::new().unwrap(); + let mut file = tempfile::NamedTempFile::new().unwrap(); + use std::io::Write; file.write_all($content).unwrap(); // NOTE: we *must* drop the file here, otherwise Windows will fail with permissions errors let (_, p) = file.keep().unwrap(); diff --git a/src/rg/exec.rs b/src/rg/exec.rs index 04169fe..5f6582d 100644 --- a/src/rg/exec.rs +++ b/src/rg/exec.rs @@ -17,11 +17,12 @@ where S: AsRef, { let mut child = match Command::new("rg") + .args(args) // We use the JSON output .arg("--json") - // We don't (yet?) support reading `rg`'s config files - .arg("--no-config") - .args(args) + // disable binary output (it could mess up our TUI) + .arg("--no-binary") + .arg("--no-text") .stdout(Stdio::piped()) .spawn() { diff --git a/src/ui/app/app_render.rs b/src/ui/app/app_render.rs index a68fcfd..d63d12e 100644 --- a/src/ui/app/app_render.rs +++ b/src/ui/app/app_render.rs @@ -1,5 +1,4 @@ /// Rendering for `App`. -use clap::crate_name; use const_format::formatcp; use ratatui::backend::Backend; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; @@ -233,7 +232,7 @@ impl App { f.render_widget(help_table, hsplit[1]); - let help_title = Span::styled(format!("{} help", crate_name!()), title_style); + let help_title = Span::styled(format!("{} help", env!("CARGO_PKG_NAME")), title_style); let help_text = self.help_text_state.text(hsplit[0].height as usize); let help_text = Text::from(help_text.as_str()); let help_paragraph = Paragraph::new(help_text) diff --git a/src/ui/tui.rs b/src/ui/tui.rs index 1e304e8..343d591 100644 --- a/src/ui/tui.rs +++ b/src/ui/tui.rs @@ -117,7 +117,7 @@ impl Tui { mut self, rg_cmdline: String, rg_messages: Vec, - patterns: Vec<&str>, + patterns: &[String], ) -> Result> { // Parse patterns into `Regex` structs let patterns = patterns