sd-1.0.0/.cargo/config.toml000064400000000000000000000000511046102023000135370ustar 00000000000000[alias] xtask = "run --package xtask --" sd-1.0.0/.cargo_vcs_info.json0000644000000001360000000000100114400ustar { "git": { "sha1": "eb51bdf11e8bde87e9d983730209c7a147e39fb0" }, "path_in_vcs": "" }sd-1.0.0/.editorconfig000064400000000000000000000000341046102023000127020ustar 00000000000000[*.rs] max_line_length = 80 sd-1.0.0/.github/dependabot.yml000064400000000000000000000006711046102023000144240ustar 00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" sd-1.0.0/.github/workflows/publish.yml000064400000000000000000000066571046102023000160340ustar 00000000000000name: Publish on: push: tags: - '*' jobs: publish: name: ${{ matrix.target }} runs-on: ${{ matrix.os }} strategy: matrix: include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu use-cross: false - os: ubuntu-latest target: x86_64-unknown-linux-musl use-cross: false - os: ubuntu-latest target: arm-unknown-linux-gnueabihf use-cross: true - os: windows-latest target: x86_64-pc-windows-gnu use-cross: false - os: windows-latest target: x86_64-pc-windows-msvc use-cross: false - os: macos-latest target: x86_64-apple-darwin use-cross: false - os: macos-latest target: aarch64-apple-darwin use-cross: false - os: ubuntu-latest target: aarch64-unknown-linux-musl use-cross: true - os: ubuntu-latest target: armv7-unknown-linux-gnueabihf use-cross: true steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 - name: Set the version shell: bash if: env.SD_VERSION == '' run: | echo "SD_VERSION=$GITHUB_REF_NAME" >> $GITHUB_ENV echo "version is: ${{ env.SD_VERSION }}" - name: Install Rust uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Setup native compilation if: ${{ matrix.use-cross == false }} shell: bash run: | echo "CARGO=cargo" >> $GITHUB_ENV - name: Setup cross compilation if: ${{ matrix.use-cross == true }} shell: bash run: | dir="$RUNNER_TEMP/cross-download" mkdir "$dir" echo "$dir" >> $GITHUB_PATH cd "$dir" curl -LO "https://github.com/cross-rs/cross/releases/download/v0.2.5/cross-x86_64-unknown-linux-musl.tar.gz" tar xf cross-x86_64-unknown-linux-musl.tar.gz echo "CARGO=cross" >> $GITHUB_ENV echo "RUSTFLAGS=--cfg sd_cross_compile" >> $GITHUB_ENV echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV - name: Build shell: bash run: | $CARGO --version $CARGO build --release --locked --target ${{ matrix.target }} # Handle windows being an oddity if [ "${{ matrix.os }}" = "windows-latest" ]; then echo "BIN_NAME=sd.exe" >> $GITHUB_ENV else echo "BIN_NAME=sd" >> $GITHUB_ENV fi - name: Setup archive shell: bash run: | staging="sd-${{ env.SD_VERSION }}-${{ matrix.target }}" mkdir -p "$staging" cp -r {README.md,LICENSE,CHANGELOG.md,gen/*} "$staging" if [ "${{ matrix.os }}" = "windows-latest" ]; then cp "target/${{ matrix.target }}/release/sd.exe" "$staging/" 7z a "$staging.zip" "$staging" echo "ASSET=$staging.zip" >> $GITHUB_ENV else cp "target/${{ matrix.target }}/release/sd" "$staging/" tar czf "$staging.tar.gz" "$staging" echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV fi - name: Upload binaries to release uses: svenstaro/upload-release-action@2.7.0 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: ${{ env.ASSET }} asset_name: ${{ env.ASSET }} tag: ${{ github.ref }} sd-1.0.0/.github/workflows/test.yml000064400000000000000000000047771046102023000153460ustar 00000000000000name: Test on: pull_request: workflow_dispatch: jobs: test: name: ${{ matrix.target }} runs-on: ${{ matrix.os }} strategy: matrix: include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu use-cross: false - os: ubuntu-latest target: x86_64-unknown-linux-musl use-cross: false - os: ubuntu-latest target: arm-unknown-linux-gnueabihf use-cross: true - os: windows-latest target: x86_64-pc-windows-gnu use-cross: false - os: windows-latest target: x86_64-pc-windows-msvc use-cross: false - os: macos-latest target: x86_64-apple-darwin use-cross: false - os: macos-latest target: aarch64-apple-darwin use-cross: false - os: ubuntu-latest target: aarch64-unknown-linux-musl use-cross: true - os: ubuntu-latest target: armv7-unknown-linux-gnueabihf use-cross: true steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 - name: Install Rust uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Setup native compilation if: ${{ matrix.use-cross == false }} shell: bash run: | echo "CARGO=cargo" >> $GITHUB_ENV - name: Install Cross if: ${{ matrix.use-cross == true }} shell: bash run: | dir="$RUNNER_TEMP/cross-download" mkdir "$dir" echo "$dir" >> $GITHUB_PATH cd "$dir" curl -LO "https://github.com/cross-rs/cross/releases/download/v0.2.5/cross-x86_64-unknown-linux-musl.tar.gz" tar xf cross-x86_64-unknown-linux-musl.tar.gz echo "CARGO=cross" >> $GITHUB_ENV echo "RUSTFLAGS=--cfg sd_cross_compile" >> $GITHUB_ENV echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV - name: Test shell: bash run: | $CARGO --version # For legal reasons, cross doesn't support Apple Silicon. See this: # https://github.com/cross-rs/cross-toolchains#darwin-targets # It builds and runs fine, but there's no easy way to test it in CI if [ "${{ matrix.target }}" = "aarch64-apple-darwin" ]; then $CARGO build --target ${{ matrix.target }} else $CARGO test --target ${{ matrix.target }} fi sd-1.0.0/.gitignore000064400000000000000000000000231046102023000122130ustar 00000000000000/target **/*.rs.bk sd-1.0.0/.rustfmt.toml000064400000000000000000000001001046102023000126760ustar 00000000000000edition = "2018" max_width = 80 use_field_init_shorthand = true sd-1.0.0/CHANGELOG.md000064400000000000000000000171261046102023000120500ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.0.0] - 2023-11-07 A quick note to any packages. The generated shell completions and man page are now in the `gen` directory of the repo. They're also included in the pre-built release artifacts on the releases page. ### Improvements - #115 Do not replace symlink with output file (@SimplyDanny) - Fixes an issue where a symlink would be replaced with a regular file - #124 Fix tests (@Linus789) - Removed displaying the file path when passing the `--preview` flag and fixed how text coloring was handled in tests ### Breaking - #192 Rename `--string-mode` to `--fixed-strings` (@CosmicHorrorDev) - Renamed `-s` `--string-mode` to `-f` `--fixed-strings` to better match similar tools - `-s` and `--string-mode` will still continue to work for backwards compatibility, but are no longer documented - #258 Error on `$` capture replacement names (@CosmicHorrorDev) - Previously when you tried to use a numbered capture group right before some letters in the replacement text (e.g. `$1foo`) then it would be considered the impossible-to-use `1foo` capture. The correct way to pass the numbered capture group in this case would be to surround the number with curly braces like so `${1}foo`. The error just detects this case and informs the user of the issue ### Docs - #93 Add note about in-place file modification to --help output (@jchook) - #148 Doc: nitpick `--` has no special meaning to shells (@hexagonrecursion) - #181 Fix man page -f flag help text (@ulope) - Fixed copy-pasted text in the man page's `-f` flag's help text - #186 Improve error message for failed replacements (@CosmicHorrorDev) - #187 Freshen up README (@CosmicHorrorDev) - Added a repology badge to document different installation methods - Improved the formatting of the benchmarks - #207 Documenting `$` escape (@yahkbar) - Adds a section in the README that covers that `$$` is a literal `$` in the replacement text - #227 Improve README readability (@vassudanagunta) - Various formatting improvements - #231 Use `clap_mangen` and `roff` to generate manpage (@nc7s) - This change ensures the man page contents stay in sync with the CLI automatically, and fixes some broken rendering of the existing manpage - #243 Exclude unsupported packages from the repology badge (@CosmicHorrorDev) ### Pre-built Releases - (11295fb) Add ARM target (@chmln) - Added the `arm-unknown-linux-gnueabihf` target to CI and releases - #114 Adding `aarch64-apple-darwin` target (@yahkbar) - #143 Fix paths to release binary in "publish" action (@skrattaren) - #179 Build Adjustments (@yahkbar) - `strip`ed release binaries and added the `aarch64-ubuntu-linux-musl` target - #204 Adding `armv7-unknown-linux-gnueabihf` target (@yahkbar) - Added the `armv7-unknown-linux-gnueabihf` target to the list of targets to build in CI and for each release - #205 Resolving broken `aarch64-apple-darwin` tests (@yahkbar) - Switched `aarch64-apple-darwin` to only try building the executable without running the tests since there seems to be no easy way to test for ARM Apple targets - #206 Adding Windows builds back (@yahkbar) - Added the `x86_64-pc-windows-gnu` and `x86_64-windows-musl` targets back to the list of targets to build in CI and for each release ### Internal - #118 Fix master (@SimplyDanny) - Fixes several cross-compilation issues that effected different targets in CI - #182 `cargo update` (@CosmicHorrorDev) - Bumps dependencies to their latest compatible versions - #183 Switch `memmap` -> `memmap2` (@CosmicHorrorDev) - Switches away from an unmaintained crate - #184 Add editor config file matching rustfmt config (@CosmicHorrorDev) - Adds an `.editorconfig` file matching the settings listed in the `.rustfmt.toml` file - #185 Fix warnings and clippy lints (@CosmicHorrorDev) - #188 Switch `atty` for `is-terminal` (@CosmicHorrorDev) - Switches away from an unmaintained crate - #189 Replace structopt with clap v4 (@CosmicHorrorDev) - Switches away from a defacto deprecated crate - #190 Change how all shell variants are expressed (@CosmicHorrorDev) - Tiny tidying up PR - #196 Move generating static assets to a `cargo-xtask` task (@CosmicHorrorDev) - Moves the generation of the man page and shell completions from a build script to a [`cargo-xtask`](https://github.com/matklad/cargo-xtask) task - #197 Add a release checklist (@CosmicHorrorDev) - #209 Dependency updates (@yahkbar) - #235 Update generated assets (@CosmicHorrorDev) - #236 Tone down dependabot (@CosmicHorrorDev) - #245 Update sd to 2021 edition (@CosmicHorrorDev) - Updates `sd` to the Rust 2021 edition - #248 Misc Cargo.toml tweaks (@CosmicHorrorDev) - Switches to use workspace edition and dependencies where appropriate - #249 Resolve CI warnings (@CosmicHorrorDev) - Switched from `actions-rs` actions to `dtolnay@rust-toolchain` - Switched from using `::set-output` to `$GITHUB_ENV` - #251 Update dependencies (@CosmicHorrorDev) - A lot of sad CI tweaking: - #252 Fix build target usage in CI (@CosmicHorrorDev) - #253 Improve publishing CI job (@CosmicHorrorDev) - #256 More CI tweaks (@CosmicHorrorDev) - #257 Fix publish action (@CosmicHorrorDev) - #267 Rework the replacements flag (@CosmicHorrorDev) - #269 Make modified text blue instead of green (@CosmicHorrorDev) - #271 Fix release checklist indentation (@CosmicHorrorDev) - #272 Remove outdated release checklist step (@CosmicHorrorDev) - #274 Prepare 1.0.0-beta.0 release (@CosmicHorrorDev) - #275 Update `sd` version in lockfile (@CosmicHorrorDev) ## (History listed in here is missing from v0.6.3 - v0.7.6) ## [0.6.2] - Fixed pre-allocated memmap buffer size - Fixed failing tests ## [0.6.0] - 2019-06-15 ### Improvements - `sd` now uses memory-mapped files, allowing replacement on files of any size - `-p`/`--preview` flag is now added to preview changes - as of right now, results are simply emitted to stdout - in a future version, the output will be changed to contain only relevant information - a `w` regex flag is added to match full words only, e.g. `sd -f w foo bar file.txt` ### Deprecations - `--in-place` is now deprecated and assumed whenever a list of files is given ## [0.5.0] - 2019-02-22 ### Improvements - Windows support (thanks to @ErichDonGubler) ## [0.4.2] - 2019-01-02 ### Improvements - Support for unicode and special characters (like `\n`) in replacement expressions - Only in regex mode - Fixed edge-cases when replacing content containing unescaped characters ## [0.4.1] - 2019-01-01 ### Improvements - Significant performance improvements (see benchmarks in README) ## [0.4.0] - 2018-12-30 ### Added - Option to set regex flags via `-f` or `--flags`: - `m` (multi-line) - `c` (case-sensitive) - `i` (case-insensitive) - Smart case-sensitivity is used by default with regular expressions ### Improvements - You may now pass multiple files to `sd` - this is now valid: `sd -i "\n" "," *.txt` ## [0.3.0] - 2018-12-29 **Breaking Change**: the `-i`/`--input` is revamped per [#1](https://github.com/chmln/sd/issues/1). The file path now comes at the end, instead of `-i`. Transforming the file in-place: - Before: `sd -s 'str' '' -i file.txt'` - Now: `sd -i -s 'str' '' file.txt` - Future: `sd -i -s 'str' '' *.txt` To reflect this change, `--input` is also renamed to `--in-place`. This is the first and most likely the last breaking change in this project. ### Improvements - Files are now written to [atomically](https://github.com/chmln/sd/issues/3) sd-1.0.0/Cargo.lock0000644000000622040000000000100074170ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] name = "ansi-to-html" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7bd918cc0ff933f0e6cf48a8f74584818ea43e07d1fba1f9251bb3df2a37ca2" dependencies = [ "regex", "thiserror", ] [[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ "winapi", ] [[package]] name = "anstream" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" 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 = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", "windows-sys 0.48.0", ] [[package]] name = "anyhow" version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "assert_cmd" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6" dependencies = [ "anstyle", "bstr", "doc-comment", "predicates", "predicates-core", "predicates-tree", "wait-timeout", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bit-set" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "bstr" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019" dependencies = [ "memchr", "regex-automata", "serde", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size", ] [[package]] name = "clap_derive" version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" [[package]] name = "clap_mangen" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b44f35c514163027542f7147797ff930523eea288e03642727348ef1a9666f6b" dependencies = [ "clap", "roff", ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", "windows-sys 0.45.0", ] [[package]] name = "crossbeam-deque" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", "scopeguard", ] [[package]] name = "crossbeam-utils" version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encode_unicode" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "errno" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ "libc", "windows-sys 0.48.0", ] [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "getrandom" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", "wasi", ] [[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.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "insta" version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" dependencies = [ "console", "lazy_static", "linked-hash-map", "similar", "yaml-rust", ] [[package]] name = "is-terminal" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", "rustix", "windows-sys 0.48.0", ] [[package]] name = "itertools" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memmap2" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deaba38d7abf1d4cca21cc89e932e542ba2b9258664d2a9ef0e61512039c9375" dependencies = [ "libc", ] [[package]] name = "memoffset" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] [[package]] name = "num-traits" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", "libm", ] [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "predicates" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" dependencies = [ "anstyle", "difflib", "itertools", "predicates-core", ] [[package]] name = "predicates-core" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" [[package]] name = "predicates-tree" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" dependencies = [ "predicates-core", "termtree", ] [[package]] name = "proc-macro2" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e" dependencies = [ "bit-set", "bit-vec", "bitflags 2.4.1", "lazy_static", "num-traits", "rand", "rand_chacha", "rand_xorshift", "regex-syntax 0.7.5", "rusty-fork", "tempfile", "unarray", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rand_xorshift" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ "rand_core", ] [[package]] name = "rayon" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "regex" version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax 0.8.2", ] [[package]] name = "regex-automata" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", "regex-syntax 0.8.2", ] [[package]] name = "regex-syntax" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "roff" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" [[package]] name = "rustix" version = "0.38.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", "windows-sys 0.48.0", ] [[package]] name = "rusty-fork" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", "quick-error", "tempfile", "wait-timeout", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sd" version = "1.0.0" dependencies = [ "ansi-to-html", "ansi_term", "anyhow", "assert_cmd", "clap", "clap_mangen", "console", "insta", "is-terminal", "memmap2", "proptest", "rayon", "regex", "regex-automata", "tempfile", "thiserror", "unescape", ] [[package]] name = "serde" version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "similar" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" [[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.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", "windows-sys 0.48.0", ] [[package]] name = "terminal_size" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ "rustix", "windows-sys 0.48.0", ] [[package]] name = "termtree" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "unarray" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unescape" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccb97dac3243214f8d8507998906ca3e2e0b900bf9bf4870477f125b82e68f6e" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "wait-timeout" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" dependencies = [ "libc", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "yaml-rust" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] sd-1.0.0/Cargo.toml0000644000000034370000000000100074450ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.70.0" name = "sd" version = "1.0.0" authors = ["Gregory "] description = "An intuitive find & replace CLI" homepage = "https://github.com/chmln/sd" readme = "README.md" keywords = [ "sed", "find", "replace", "regex", ] categories = [ "command-line-utilities", "text-processing", "development-tools", ] license = "MIT" repository = "https://github.com/chmln/sd.git" [profile.release] opt-level = 3 lto = true strip = true [dependencies.ansi_term] version = "0.12.1" [dependencies.clap] version = "4.4.6" features = [ "derive", "deprecated", "wrap_help", ] [dependencies.is-terminal] version = "0.4.9" [dependencies.memmap2] version = "0.9.0" [dependencies.rayon] version = "1.8.0" [dependencies.regex] version = "1.10.2" [dependencies.tempfile] version = "3.8.0" [dependencies.thiserror] version = "1.0.50" [dependencies.unescape] version = "0.1.0" [dev-dependencies.ansi-to-html] version = "0.1.3" [dev-dependencies.anyhow] version = "1.0.75" [dev-dependencies.assert_cmd] version = "2.0.12" [dev-dependencies.clap_mangen] version = "0.2.14" [dev-dependencies.console] version = "0.15.7" [dev-dependencies.insta] version = "1.34.0" [dev-dependencies.proptest] version = "1.3.1" [dev-dependencies.regex-automata] version = "0.4.3" sd-1.0.0/Cargo.toml.orig000064400000000000000000000020671046102023000131240ustar 00000000000000[workspace] members = [ ".", "xtask", ] [workspace.dependencies.clap] version = "4.4.6" features = ["derive", "deprecated", "wrap_help"] [workspace.package] edition = "2021" version = "1.0.0" [package] name = "sd" version.workspace = true edition.workspace = true authors = ["Gregory "] description = "An intuitive find & replace CLI" readme = "README.md" keywords = ["sed", "find", "replace", "regex"] license = "MIT" homepage = "https://github.com/chmln/sd" repository = "https://github.com/chmln/sd.git" categories = ["command-line-utilities", "text-processing", "development-tools"] rust-version = "1.70.0" [dependencies] regex = "1.10.2" rayon = "1.8.0" unescape = "0.1.0" memmap2 = "0.9.0" tempfile = "3.8.0" thiserror = "1.0.50" ansi_term = "0.12.1" is-terminal = "0.4.9" clap.workspace = true [dev-dependencies] assert_cmd = "2.0.12" anyhow = "1.0.75" clap_mangen = "0.2.14" proptest = "1.3.1" console = "0.15.7" insta = "1.34.0" ansi-to-html = "0.1.3" regex-automata = "0.4.3" [profile.release] opt-level = 3 lto = true strip = true sd-1.0.0/LICENSE000064400000000000000000000020501046102023000112320ustar 00000000000000MIT License Copyright (c) 2018 Gregory Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sd-1.0.0/README.md000064400000000000000000000127301046102023000115120ustar 00000000000000# sd - `s`earch & `d`isplace `sd` is an intuitive find & replace CLI. ## The Pitch Why use it over any existing tools? *Painless regular expressions.*   `sd` uses regex syntax that you already know from JavaScript and Python. Forget about dealing with quirks of `sed` or `awk` - get productive immediately. *String-literal mode.*   Non-regex find & replace. No more backslashes or remembering which characters are special and need to be escaped. *Easy to read, easy to write.*   Find & replace expressions are split up, which makes them easy to read and write. No more messing with unclosed and escaped slashes. *Smart, common-sense defaults.*   Defaults follow common sense and are tailored for typical daily use. ## Comparison to sed While sed does a whole lot more, sd focuses on doing just one thing and doing it well. Here are some cherry-picked examples where sd shines. Simpler syntax for replacing all occurrences: - sd: `sd before after` - sed: `sed s/before/after/g` Replace newlines with commas: - sd: `sd '\n' ','` - sed: `sed ':a;N;$!ba;s/\n/,/g'` Extracting stuff out of strings containing slashes: - sd: `echo "sample with /path/" | sd '.*(/.*/)' '$1'` - sed: `echo "sample with /path/" | sed -E 's/.*(\\/.*\\/)/\1/g'` With sed, you can make it better with a different delimiter, but it is still messy: `echo "sample with /path/" | sed -E 's|.*(/.*/)|\1|g'` In place modification of files: - sd: `sd before after file.txt` - sed: `sed -i -e 's/before/after/g' file.txt` With sed, you need to remember to use `-e` or else some platforms will consider the next argument to be a backup suffix. ## Benchmarks **Simple replacement on ~1.5 gigabytes of JSON** ```sh hyperfine --warmup 3 --export-markdown out.md \ 'sed -E "s/\"/'"'"'/g" *.json > /dev/null' \ 'sed "s/\"/'"'"'/g" *.json > /dev/null' \ 'sd "\"" "'"'"'" *.json > /dev/null' ``` | Command | Mean [s] | Min…Max [s] | |:---|---:|---:| | `sed -E "s/\"/'/g" *.json > /dev/null` | 2.338 ± 0.008 | 2.332…2.358 | | `sed "s/\"/'/g" *.json > /dev/null` | 2.365 ± 0.009 | 2.351…2.378 | | `sd "\"" "'" *.json > /dev/null` | **0.997 ± 0.006** | 0.987…1.007 | Result: ~2.35 times faster **Regex replacement on a ~55M json file**: ```sh hyperfine --warmup 3 --export-markdown out.md \ 'sed -E "s:(\w+):\1\1:g" dump.json > /dev/null' \ 'sed "s:\(\w\+\):\1\1:g" dump.json > /dev/null' \ 'sd "(\w+)" "$1$1" dump.json > /dev/null' ``` | Command | Mean [s] | Min…Max [s] | |:---|---:|---:| | `sed -E "s:(\w+):\1\1:g" dump.json > /dev/null` | 11.315 ± 0.215 | 11.102…11.725 | | `sed "s:\(\w\+\):\1\1:g" dump.json > /dev/null` | 11.239 ± 0.208 | 11.057…11.762 | | `sd "(\w+)" "$1$1" dump.json > /dev/null` | **0.942 ± 0.004** | 0.936…0.951 | Result: ~11.93 times faster ## Installation Install through [`cargo`](https://doc.rust-lang.org/cargo/getting-started/installation.html) with `cargo install sd`, or through various package managers [![Packaging status](https://repology.org/badge/vertical-allrepos/sd-find-replace.svg?exclude_unsupported=1)](https://repology.org/project/sd-find-replace/versions) ## Quick Guide 1. **String-literal mode**. By default, expressions are treated as regex. Use `-F` or `--fixed-strings` to disable regex. ```sh > echo 'lots((([]))) of special chars' | sd -s '((([])))' '' lots of special chars ``` 2. **Basic regex use** - let's trim some trailing whitespace ```sh > echo 'lorem ipsum 23 ' | sd '\s+$' '' lorem ipsum 23 ``` 3. **Capture groups** Indexed capture groups: ```sh > echo 'cargo +nightly watch' | sd '(\w+)\s+\+(\w+)\s+(\w+)' 'cmd: $1, channel: $2, subcmd: $3' cmd: cargo, channel: nightly, subcmd: watch ``` Named capture groups: ```sh > echo "123.45" | sd '(?P\d+)\.(?P\d+)' '$dollars dollars and $cents cents' 123 dollars and 45 cents ``` In the unlikely case you stumble upon ambiguities, resolve them by using `${var}` instead of `$var`. Here's an example: ```sh > echo '123.45' | sd '(?P\d+)\.(?P\d+)' '$dollars_dollars and $cents_cents' and > echo '123.45' | sd '(?P\d+)\.(?P\d+)' '${dollars}_dollars and ${cents}_cents' 123_dollars and 45_cents ``` 4. **Find & replace in a file** ```sh > sd 'window.fetch' 'fetch' http.js ``` That's it. The file is modified in-place. To preview changes: ```sh > sd -p 'window.fetch' 'fetch' http.js ``` 5. **Find & replace across project** This example uses [fd](https://github.com/sharkdp/fd). Good ol' unix philosophy to the rescue. ```sh fd --type file --exec sd 'from "react"' 'from "preact"' ``` Same, but with backups (consider version control). ```bash fd --type file --exec cp {} {}.bk \; --exec sd 'from "react"' 'from "preact"' ``` ### Edge cases sd will interpret every argument starting with `-` as a (potentially unknown) flag. The common convention of using `--` to signal the end of flags is respected: ```bash $ echo "./hello foo" | sd "foo" "-w" error: Found argument '-w' which wasn't expected, or isn't valid in this context USAGE: sd [OPTIONS] [files]... For more information try --help $ echo "./hello foo" | sd "foo" -- "-w" ./hello -w $ echo "./hello --foo" | sd -- "--foo" "-w" ./hello -w ``` ### Escaping special characters To escape the `$` character, use `$$`: ```bash ❯ echo "foo" | sd 'foo' '$$bar' $bar ``` sd-1.0.0/RELEASE.md000064400000000000000000000015551046102023000116400ustar 00000000000000# Release checklist 1. [ ] Create a new _"Release v{VERSION}"_ issue with this checklist - `$ cat RELEASE.md | sd '\{VERSION\}' '{NEW_VERSION}' | xclip -sel clip` - Create the issue in GitHub 1. [ ] Regenerate static assets - `$ cargo xtask gen` 1. [ ] Update `rust-version` in `Cargo.toml` - `$ cargo msrv --min 1.60 -- cargo check` 1. [ ] Bump `version` in `Cargo.toml` 1. [ ] Run `cargo check` to propogate the change to `Cargo.lock` 1. [ ] Update the `CHANGELOG.md` 1. [ ] Merge changes through a PR to make sure that CI passes 1. [ ] Publish on [crates.io](crates.io) - `$ cargo publish` 1. [ ] Publish on GitHub by pushing a version tag - Make sure the branch you're on is fully up to date - `$ git tag v{VERSION}` - `$ git push upstream/origin v{VERSION}` 1. [ ] Make a release announcement on GitHub after the release workflow finishes sd-1.0.0/gen/completions/_sd000064400000000000000000000033231046102023000140260ustar 00000000000000#compdef sd autoload -U is-at-least _sd() { typeset -A opt_args typeset -a _arguments_options local ret=1 if is-at-least 5.2; then _arguments_options=(-s -S -C) else _arguments_options=(-s -C) fi local context curcontext="$curcontext" state line _arguments "${_arguments_options[@]}" \ '-n+[Limit the number of replacements that can occur per file. 0 indicates unlimited replacements]:LIMIT: ' \ '--max-replacements=[Limit the number of replacements that can occur per file. 0 indicates unlimited replacements]:LIMIT: ' \ '-f+[Regex flags. May be combined (like \`-f mc\`).]:FLAGS: ' \ '--flags=[Regex flags. May be combined (like \`-f mc\`).]:FLAGS: ' \ '-p[Display changes in a human reviewable format (the specifics of the format are likely to change in the future)]' \ '--preview[Display changes in a human reviewable format (the specifics of the format are likely to change in the future)]' \ '-F[Treat FIND and REPLACE_WITH args as literal strings]' \ '--fixed-strings[Treat FIND and REPLACE_WITH args as literal strings]' \ '-h[Print help (see more with '\''--help'\'')]' \ '--help[Print help (see more with '\''--help'\'')]' \ '-V[Print version]' \ '--version[Print version]' \ ':find -- The regexp or string (if using `-F`) to search for:' \ ':replace_with -- What to replace each match with. Unless in string mode, you may use captured values like $1, $2, etc:' \ '*::files -- The path to file(s). This is optional - sd can also read from STDIN:_files' \ && ret=0 } (( $+functions[_sd_commands] )) || _sd_commands() { local commands; commands=() _describe -t commands 'sd commands' commands "$@" } if [ "$funcstack[1]" = "_sd" ]; then _sd "$@" else compdef _sd sd fi sd-1.0.0/gen/completions/_sd.ps1000064400000000000000000000054041046102023000145320ustar 00000000000000 using namespace System.Management.Automation using namespace System.Management.Automation.Language Register-ArgumentCompleter -Native -CommandName 'sd' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) $commandElements = $commandAst.CommandElements $command = @( 'sd' for ($i = 1; $i -lt $commandElements.Count; $i++) { $element = $commandElements[$i] if ($element -isnot [StringConstantExpressionAst] -or $element.StringConstantType -ne [StringConstantType]::BareWord -or $element.Value.StartsWith('-') -or $element.Value -eq $wordToComplete) { break } $element.Value }) -join ';' $completions = @(switch ($command) { 'sd' { [CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements') [CompletionResult]::new('--max-replacements', 'max-replacements', [CompletionResultType]::ParameterName, 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements') [CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Regex flags. May be combined (like `-f mc`).') [CompletionResult]::new('--flags', 'flags', [CompletionResultType]::ParameterName, 'Regex flags. May be combined (like `-f mc`).') [CompletionResult]::new('-p', 'p', [CompletionResultType]::ParameterName, 'Display changes in a human reviewable format (the specifics of the format are likely to change in the future)') [CompletionResult]::new('--preview', 'preview', [CompletionResultType]::ParameterName, 'Display changes in a human reviewable format (the specifics of the format are likely to change in the future)') [CompletionResult]::new('-F', 'F ', [CompletionResultType]::ParameterName, 'Treat FIND and REPLACE_WITH args as literal strings') [CompletionResult]::new('--fixed-strings', 'fixed-strings', [CompletionResultType]::ParameterName, 'Treat FIND and REPLACE_WITH args as literal strings') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version') break } }) $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | Sort-Object -Property ListItemText } sd-1.0.0/gen/completions/sd.bash000064400000000000000000000027341046102023000146100ustar 00000000000000_sd() { local i cur prev opts cmd COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" cmd="" opts="" for i in ${COMP_WORDS[@]} do case "${cmd},${i}" in ",$1") cmd="sd" ;; *) ;; esac done case "${cmd}" in sd) opts="-p -F -n -f -h -V --preview --fixed-strings --max-replacements --flags --help --version [FILES]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 fi case "${prev}" in --max-replacements) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -n) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --flags) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; -f) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; *) COMPREPLY=() ;; esac COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; esac } complete -F _sd -o nosort -o bashdefault -o default sd sd-1.0.0/gen/completions/sd.elv000064400000000000000000000030011046102023000144450ustar 00000000000000 use builtin; use str; set edit:completion:arg-completer[sd] = {|@words| fn spaces {|n| builtin:repeat $n ' ' | str:join '' } fn cand {|text desc| edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc } var command = 'sd' for word $words[1..-1] { if (str:has-prefix $word '-') { break } set command = $command';'$word } var completions = [ &'sd'= { cand -n 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements' cand --max-replacements 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements' cand -f 'Regex flags. May be combined (like `-f mc`).' cand --flags 'Regex flags. May be combined (like `-f mc`).' cand -p 'Display changes in a human reviewable format (the specifics of the format are likely to change in the future)' cand --preview 'Display changes in a human reviewable format (the specifics of the format are likely to change in the future)' cand -F 'Treat FIND and REPLACE_WITH args as literal strings' cand --fixed-strings 'Treat FIND and REPLACE_WITH args as literal strings' cand -h 'Print help (see more with ''--help'')' cand --help 'Print help (see more with ''--help'')' cand -V 'Print version' cand --version 'Print version' } ] $completions[$command] } sd-1.0.0/gen/completions/sd.fish000064400000000000000000000011101046102023000146070ustar 00000000000000complete -c sd -s n -l max-replacements -d 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements' -r complete -c sd -s f -l flags -d 'Regex flags. May be combined (like `-f mc`).' -r complete -c sd -s p -l preview -d 'Display changes in a human reviewable format (the specifics of the format are likely to change in the future)' complete -c sd -s F -l fixed-strings -d 'Treat FIND and REPLACE_WITH args as literal strings' complete -c sd -s h -l help -d 'Print help (see more with \'--help\')' complete -c sd -s V -l version -d 'Print version' sd-1.0.0/gen/sd.1000064400000000000000000000047171046102023000115020ustar 00000000000000.ie \n(.g .ds Aq \(aq .el .ds Aq ' .TH sd 1 "sd 1.0.0" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .SH NAME sd .ie \n(.g .ds Aq \(aq .el .ds Aq ' .SH SYNOPSIS \fBsd\fR [\fB\-p\fR|\fB\-\-preview\fR] [\fB\-F\fR|\fB\-\-fixed\-strings\fR] [\fB\-n\fR|\fB\-\-max\-replacements\fR] [\fB\-f\fR|\fB\-\-flags\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] <\fIFIND\fR> <\fIREPLACE_WITH\fR> [\fIFILES\fR] .ie \n(.g .ds Aq \(aq .el .ds Aq ' .SH DESCRIPTION .ie \n(.g .ds Aq \(aq .el .ds Aq ' .SH OPTIONS .TP \fB\-p\fR, \fB\-\-preview\fR Display changes in a human reviewable format (the specifics of the format are likely to change in the future) .TP \fB\-F\fR, \fB\-\-fixed\-strings\fR Treat FIND and REPLACE_WITH args as literal strings .TP \fB\-n\fR, \fB\-\-max\-replacements\fR=\fILIMIT\fR [default: 0] Limit the number of replacements that can occur per file. 0 indicates unlimited replacements .TP \fB\-f\fR, \fB\-\-flags\fR=\fIFLAGS\fR Regex flags. May be combined (like `\-f mc`). c \- case\-sensitive e \- disable multi\-line matching i \- case\-insensitive m \- multi\-line matching s \- make `.` match newlines w \- match full words only .TP \fB\-h\fR, \fB\-\-help\fR Print help (see a summary with \*(Aq\-h\*(Aq) .TP \fB\-V\fR, \fB\-\-version\fR Print version .TP <\fIFIND\fR> The regexp or string (if using `\-F`) to search for .TP <\fIREPLACE_WITH\fR> What to replace each match with. Unless in string mode, you may use captured values like $1, $2, etc .TP [\fIFILES\fR] The path to file(s). This is optional \- sd can also read from STDIN. Note: sd modifies files in\-place by default. See documentation for examples. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .SH "EXIT STATUS" .IP 0 Successful program execution. .IP 1 Unsuccessful program execution. .IP 101 The program panicked. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .SH EXAMPLES .TP String\-literal mode \fB$ echo \*(Aqlots((([]))) of special chars\*(Aq | sd \-s \*(Aq((([])))\*(Aq\fR .br lots of special chars .TP Regex use. Let\*(Aqs trim some trailing whitespace \fB$ echo \*(Aqlorem ipsum 23 \*(Aq | sd \*(Aq\\s+$\*(Aq \*(Aq\*(Aq\fR .br lorem ipsum 23 .TP Indexed capture groups \fB$ echo \*(Aqcargo +nightly watch\*(Aq | sd \*(Aq(\\w+)\\s+\\+(\\w+)\\s+(\\w+)\*(Aq \*(Aqcmd: $1, channel: $2, subcmd: $3\*(Aq\fR .br 123 dollars and 45 cents .TP Find & replace in file \fB$ sd \*(Aqwindow.fetch\*(Aq \*(Aqfetch\*(Aq http.js\fR .br .TP Find & replace from STDIN an emit to STDOUT \fB$ sd \*(Aqwindow.fetch\*(Aq \*(Aqfetch\*(Aq < http.js\fR .br sd-1.0.0/proptest-regressions/replacer/tests.txt000064400000000000000000000007341046102023000201550ustar 00000000000000# Seeds for failure cases proptest has generated in the past. It is # automatically read and these particular cases re-run before any # novel cases are generated. # # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. cc 3a23ade8355ca034558ea8635e4ea2ee96ecb38b7b1cb9a854509d7633d45795 # shrinks to s = "" cc 8c8d1e7497465f26416bddb7607df0de1fce48d098653eeabac0ad2aeba1fa0a # shrinks to s = "$0$0a" sd-1.0.0/proptest-regressions/replacer/validate.txt000064400000000000000000000012161046102023000206000ustar 00000000000000# Seeds for failure cases proptest has generated in the past. It is # automatically read and these particular cases re-run before any # novel cases are generated. # # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. cc cfacd65058c8dae0ac7b91c56b8096c36ef68cb35d67262debebac005ea9c677 # shrinks to s = "" cc 61e5dc6ce0314cde48b5cbc839fbf46a49fcf8d0ba02cfeecdcbff52fca8c786 # shrinks to s = "$a" cc 8e5fd9dbb58ae762a751349749320664715056ef63aad58215397e87ee42c722 # shrinks to s = "$$" cc 37c2e41ceeddbecbc4e574f82b58a4007923027ad1a6756bf2f547aa3f748d13 # shrinks to s = "$$0" sd-1.0.0/release.toml000064400000000000000000000000261046102023000125430ustar 00000000000000no-dev-version = true sd-1.0.0/src/cli.rs000064400000000000000000000035471046102023000121450ustar 00000000000000use clap::Parser; #[derive(Parser, Debug)] #[command( name = "sd", author, version, about, max_term_width = 100, help_template = "\ {before-help}{name} v{version} {about-with-newline} {usage-heading} {usage} {all-args}{after-help}" )] pub struct Options { #[arg(short, long)] /// Display changes in a human reviewable format (the specifics of the /// format are likely to change in the future). pub preview: bool, #[arg( short = 'F', long = "fixed-strings", short_alias = 's', alias = "string-mode" )] /// Treat FIND and REPLACE_WITH args as literal strings pub literal_mode: bool, #[arg( short = 'n', long = "max-replacements", value_name = "LIMIT", default_value_t )] /// Limit the number of replacements that can occur per file. 0 indicates /// unlimited replacements. pub replacements: usize, #[arg(short, long, verbatim_doc_comment)] #[rustfmt::skip] /** Regex flags. May be combined (like `-f mc`). c - case-sensitive e - disable multi-line matching i - case-insensitive m - multi-line matching s - make `.` match newlines w - match full words only */ pub flags: Option, /// The regexp or string (if using `-F`) to search for. pub find: String, /// What to replace each match with. Unless in string mode, you may /// use captured values like $1, $2, etc. pub replace_with: String, /// The path to file(s). This is optional - sd can also read from STDIN. /// /// Note: sd modifies files in-place by default. See documentation for /// examples. pub files: Vec, } #[cfg(test)] mod tests { use super::*; use clap::CommandFactory; #[test] fn debug_assert() { let cmd = Options::command(); cmd.debug_assert(); } } sd-1.0.0/src/error.rs000064400000000000000000000024171046102023000125220ustar 00000000000000use std::{ fmt::{self, Write}, path::PathBuf, }; use crate::replacer::InvalidReplaceCapture; #[derive(thiserror::Error)] pub enum Error { #[error("invalid regex {0}")] Regex(#[from] regex::Error), #[error(transparent)] File(#[from] std::io::Error), #[error("failed to move file: {0}")] TempfilePersist(#[from] tempfile::PersistError), #[error("file doesn't have parent path: {0}")] InvalidPath(PathBuf), #[error("failed processing files:\n{0}")] FailedProcessing(FailedJobs), #[error("{0}")] InvalidReplaceCapture(#[from] InvalidReplaceCapture), } pub struct FailedJobs(Vec<(PathBuf, Error)>); impl From> for FailedJobs { fn from(vec: Vec<(PathBuf, Error)>) -> Self { Self(vec) } } impl fmt::Display for FailedJobs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\tFailedJobs(\n")?; for (path, err) in &self.0 { f.write_str(&format!("\t{:?}: {}\n", path, err))?; } f.write_char(')') } } // pretty-print the error impl std::fmt::Debug for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self) } } pub type Result = std::result::Result; sd-1.0.0/src/input.rs000064400000000000000000000057261046102023000125360ustar 00000000000000use std::{fs::File, io::prelude::*, path::PathBuf}; use crate::{Error, Replacer, Result}; use is_terminal::IsTerminal; #[derive(Debug)] pub(crate) enum Source { Stdin, Files(Vec), } pub(crate) struct App { replacer: Replacer, source: Source, } impl App { fn stdin_replace(&self, is_tty: bool) -> Result<()> { let mut buffer = Vec::with_capacity(256); let stdin = std::io::stdin(); let mut handle = stdin.lock(); handle.read_to_end(&mut buffer)?; let stdout = std::io::stdout(); let mut handle = stdout.lock(); handle.write_all(&if is_tty { self.replacer.replace_preview(&buffer) } else { self.replacer.replace(&buffer) })?; Ok(()) } pub(crate) fn new(source: Source, replacer: Replacer) -> Self { Self { source, replacer } } pub(crate) fn run(&self, preview: bool) -> Result<()> { let is_tty = std::io::stdout().is_terminal(); match (&self.source, preview) { (Source::Stdin, true) => self.stdin_replace(is_tty), (Source::Stdin, false) => self.stdin_replace(is_tty), (Source::Files(paths), false) => { use rayon::prelude::*; let failed_jobs: Vec<_> = paths .par_iter() .filter_map(|p| { if let Err(e) = self.replacer.replace_file(p) { Some((p.to_owned(), e)) } else { None } }) .collect(); if failed_jobs.is_empty() { Ok(()) } else { let failed_jobs = crate::error::FailedJobs::from(failed_jobs); Err(Error::FailedProcessing(failed_jobs)) } } (Source::Files(paths), true) => { let stdout = std::io::stdout(); let mut handle = stdout.lock(); let print_path = paths.len() > 1; paths.iter().try_for_each(|path| { if Replacer::check_not_empty(File::open(path)?).is_err() { return Ok(()); } let file = unsafe { memmap2::Mmap::map(&File::open(path)?)? }; if self.replacer.has_matches(&file) { if print_path { writeln!( handle, "----- FILE {} -----", path.display() )?; } handle .write_all(&self.replacer.replace_preview(&file))?; writeln!(handle)?; } Ok(()) }) } } } } sd-1.0.0/src/main.rs000064400000000000000000000015621046102023000123150ustar 00000000000000mod cli; mod error; mod input; pub(crate) mod replacer; pub(crate) mod utils; use std::process; pub(crate) use self::input::{App, Source}; use ansi_term::{Color, Style}; pub(crate) use error::{Error, Result}; use replacer::Replacer; use clap::Parser; fn main() { if let Err(e) = try_main() { eprintln!("{}: {}", Style::from(Color::Red).bold().paint("error"), e); process::exit(1); } } fn try_main() -> Result<()> { let options = cli::Options::parse(); let source = if !options.files.is_empty() { Source::Files(options.files) } else { Source::Stdin }; App::new( source, Replacer::new( options.find, options.replace_with, options.literal_mode, options.flags, options.replacements, )?, ) .run(options.preview)?; Ok(()) } sd-1.0.0/src/replacer/mod.rs000064400000000000000000000140761046102023000137510ustar 00000000000000use std::{borrow::Cow, fs, fs::File, io::prelude::*, path::Path}; use crate::{utils, Error, Result}; use regex::bytes::Regex; #[cfg(test)] mod tests; mod validate; pub use validate::{validate_replace, InvalidReplaceCapture}; pub(crate) struct Replacer { regex: Regex, replace_with: Vec, is_literal: bool, replacements: usize, } impl Replacer { pub(crate) fn new( look_for: String, replace_with: String, is_literal: bool, flags: Option, replacements: usize, ) -> Result { let (look_for, replace_with) = if is_literal { (regex::escape(&look_for), replace_with.into_bytes()) } else { validate_replace(&replace_with)?; ( look_for, utils::unescape(&replace_with) .unwrap_or(replace_with) .into_bytes(), ) }; let mut regex = regex::bytes::RegexBuilder::new(&look_for); regex.multi_line(true); if let Some(flags) = flags { flags.chars().for_each(|c| { #[rustfmt::skip] match c { 'c' => { regex.case_insensitive(false); }, 'i' => { regex.case_insensitive(true); }, 'm' => {}, 'e' => { regex.multi_line(false); }, 's' => { if !flags.contains('m') { regex.multi_line(false); } regex.dot_matches_new_line(true); }, 'w' => { regex = regex::bytes::RegexBuilder::new(&format!( "\\b{}\\b", look_for )); }, _ => {}, }; }); }; Ok(Self { regex: regex.build()?, replace_with, is_literal, replacements, }) } pub(crate) fn has_matches(&self, content: &[u8]) -> bool { self.regex.is_match(content) } pub(crate) fn check_not_empty(mut file: File) -> Result<()> { let mut buf: [u8; 1] = Default::default(); file.read_exact(&mut buf)?; Ok(()) } pub(crate) fn replace<'a>( &'a self, content: &'a [u8], ) -> std::borrow::Cow<'a, [u8]> { let regex = &self.regex; let limit = self.replacements; let use_color = false; if self.is_literal { Self::replacen( regex, limit, content, use_color, regex::bytes::NoExpand(&self.replace_with), ) } else { Self::replacen( regex, limit, content, use_color, &*self.replace_with, ) } } /// A modified form of [`regex::bytes::Regex::replacen`] that supports /// coloring replacements pub(crate) fn replacen<'haystack, R: regex::bytes::Replacer>( regex: ®ex::bytes::Regex, limit: usize, haystack: &'haystack [u8], use_color: bool, mut rep: R, ) -> Cow<'haystack, [u8]> { let mut it = regex.captures_iter(haystack).enumerate().peekable(); if it.peek().is_none() { return Cow::Borrowed(haystack); } let mut new = Vec::with_capacity(haystack.len()); let mut last_match = 0; for (i, cap) in it { // unwrap on 0 is OK because captures only reports matches let m = cap.get(0).unwrap(); new.extend_from_slice(&haystack[last_match..m.start()]); if use_color { new.extend_from_slice( ansi_term::Color::Blue.prefix().to_string().as_bytes(), ); } rep.replace_append(&cap, &mut new); if use_color { new.extend_from_slice( ansi_term::Color::Blue.suffix().to_string().as_bytes(), ); } last_match = m.end(); if limit > 0 && i >= limit - 1 { break; } } new.extend_from_slice(&haystack[last_match..]); Cow::Owned(new) } pub(crate) fn replace_preview<'a>( &self, content: &'a [u8], ) -> std::borrow::Cow<'a, [u8]> { let regex = &self.regex; let limit = self.replacements; // TODO: refine this condition more let use_color = true; if self.is_literal { Self::replacen( regex, limit, content, use_color, regex::bytes::NoExpand(&self.replace_with), ) } else { Self::replacen( regex, limit, content, use_color, &*self.replace_with, ) } } pub(crate) fn replace_file(&self, path: &Path) -> Result<()> { use memmap2::{Mmap, MmapMut}; use std::ops::DerefMut; if Self::check_not_empty(File::open(path)?).is_err() { return Ok(()); } let source = File::open(path)?; let meta = fs::metadata(path)?; let mmap_source = unsafe { Mmap::map(&source)? }; let replaced = self.replace(&mmap_source); let target = tempfile::NamedTempFile::new_in( path.parent() .ok_or_else(|| Error::InvalidPath(path.to_path_buf()))?, )?; let file = target.as_file(); file.set_len(replaced.len() as u64)?; file.set_permissions(meta.permissions())?; if !replaced.is_empty() { let mut mmap_target = unsafe { MmapMut::map_mut(file)? }; mmap_target.deref_mut().write_all(&replaced)?; mmap_target.flush_async()?; } drop(mmap_source); drop(source); target.persist(fs::canonicalize(path)?)?; Ok(()) } } sd-1.0.0/src/replacer/tests.rs000064400000000000000000000035571046102023000143360ustar 00000000000000use super::*; use proptest::prelude::*; proptest! { #[test] fn validate_doesnt_panic(s in r"(\PC*\$?){0,5}") { let _ = validate::validate_replace(&s); } // $ followed by a digit and a non-ident char or an ident char #[test] fn validate_ok(s in r"([^\$]*(\$([0-9][^a-zA-Z_0-9\$]|a-zA-Z_))?){0,5}") { validate::validate_replace(&s).unwrap(); } // Force at least one $ followed by a digit and an ident char #[test] fn validate_err(s in r"[^\$]*?\$[0-9][a-zA-Z_]\PC*") { validate::validate_replace(&s).unwrap_err(); } } fn replace( look_for: impl Into, replace_with: impl Into, literal: bool, flags: Option<&'static str>, src: &'static str, target: &'static str, ) { const UNLIMITED_REPLACEMENTS: usize = 0; let replacer = Replacer::new( look_for.into(), replace_with.into(), literal, flags.map(ToOwned::to_owned), UNLIMITED_REPLACEMENTS, ) .unwrap(); assert_eq!( std::str::from_utf8(&replacer.replace(src.as_bytes())), Ok(target) ); } #[test] fn default_global() { replace("a", "b", false, None, "aaa", "bbb"); } #[test] fn escaped_char_preservation() { replace("a", "b", false, None, "a\\n", "b\\n"); } #[test] fn case_sensitive_default() { replace("abc", "x", false, None, "abcABC", "xABC"); replace("abc", "x", true, None, "abcABC", "xABC"); } #[test] fn sanity_check_literal_replacements() { replace("((special[]))", "x", true, None, "((special[]))y", "xy"); } #[test] fn unescape_regex_replacements() { replace("test", r"\n", false, None, "testtest", "\n\n"); } #[test] fn no_unescape_literal_replacements() { replace("test", r"\n", true, None, "testtest", r"\n\n"); } #[test] fn full_word_replace() { replace("abc", "def", false, Some("w"), "abcd abc", "abcd def"); } sd-1.0.0/src/replacer/validate.rs000064400000000000000000000275451046102023000147700ustar 00000000000000use std::{error::Error, fmt, str::CharIndices}; use ansi_term::{Color, Style}; #[derive(Debug)] pub struct InvalidReplaceCapture { original_replace: String, invalid_ident: Span, num_leading_digits: usize, } impl Error for InvalidReplaceCapture {} // NOTE: This code is much more allocation heavy than it needs to be, but it's // only displayed as a hard error to the user, so it's not a big deal impl fmt::Display for InvalidReplaceCapture { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { #[derive(Clone, Copy)] enum SpecialChar { Newline, CarriageReturn, Tab, } impl SpecialChar { fn new(c: char) -> Option { match c { '\n' => Some(Self::Newline), '\r' => Some(Self::CarriageReturn), '\t' => Some(Self::Tab), _ => None, } } /// Renders as the character from the "Control Pictures" block /// /// https://en.wikipedia.org/wiki/Control_Pictures fn render(self) -> char { match self { Self::Newline => '␊', Self::CarriageReturn => '␍', Self::Tab => '␉', } } } let Self { original_replace, invalid_ident, num_leading_digits, } = self; // Build up the error to show the user let mut formatted = String::new(); let mut arrows_start = Span::start_at(0); let special = Style::new().bold(); let error = Style::from(Color::Red).bold(); for (byte_index, c) in original_replace.char_indices() { let (prefix, suffix, text) = match SpecialChar::new(c) { Some(c) => { (Some(special.prefix()), Some(special.suffix()), c.render()) } None => { let (prefix, suffix) = if byte_index == invalid_ident.start { (Some(error.prefix()), None) } else if byte_index == invalid_ident.end.checked_sub(1).unwrap() { (None, Some(error.suffix())) } else { (None, None) }; (prefix, suffix, c) } }; if let Some(prefix) = prefix { formatted.push_str(&prefix.to_string()); } formatted.push(text); if let Some(suffix) = suffix { formatted.push_str(&suffix.to_string()); } if byte_index < invalid_ident.start { // Assumes that characters have a base display width of 1. While // that's not technically true, it's near impossible to do right // since the specifics on text rendering is up to the user's // terminal/font. This _does_ rely on variable-width characters // like \n, \r, and \t getting converting to single character // representations above arrows_start.start += 1; } } // This relies on all non-curly-braced capture chars being 1 byte let arrows_span = arrows_start.end_offset(invalid_ident.len()); let mut arrows = " ".repeat(arrows_span.start); arrows.push_str(&format!( "{}", Style::new().bold().paint("^".repeat(arrows_span.len())) )); let ident = invalid_ident.slice(original_replace); let (number, the_rest) = ident.split_at(*num_leading_digits); let disambiguous = format!("${{{number}}}{the_rest}"); let error_message = format!( "The numbered capture group `{}` in the replacement text is ambiguous.", Style::new().bold().paint(format!("${}", number).to_string()) ); let hint_message = format!( "{}: Use curly braces to disambiguate it `{}`.", Style::from(Color::Blue).bold().paint("hint"), Style::new().bold().paint(disambiguous) ); writeln!(f, "{}", error_message)?; writeln!(f, "{}", hint_message)?; writeln!(f, "{}", formatted)?; write!(f, "{}", arrows) } } pub fn validate_replace(s: &str) -> Result<(), InvalidReplaceCapture> { for ident in ReplaceCaptureIter::new(s) { let mut char_it = ident.name.char_indices(); let (_, c) = char_it.next().unwrap(); if c.is_ascii_digit() { for (i, c) in char_it { if !c.is_ascii_digit() { return Err(InvalidReplaceCapture { original_replace: s.to_owned(), invalid_ident: ident.span, num_leading_digits: i, }); } } } } Ok(()) } #[derive(Clone, Copy, Debug)] struct Span { start: usize, end: usize, } impl Span { fn start_at(start: usize) -> SpanOpen { SpanOpen { start } } fn new(start: usize, end: usize) -> Self { // `<` instead of `<=` because `Span` is exclusive on the upper bound assert!(start < end); Self { start, end } } fn slice(self, s: &str) -> &str { &s[self.start..self.end] } fn len(self) -> usize { self.end - self.start } } #[derive(Clone, Copy)] struct SpanOpen { start: usize, } impl SpanOpen { fn end_at(self, end: usize) -> Span { let Self { start } = self; Span::new(start, end) } fn end_offset(self, offset: usize) -> Span { assert_ne!(offset, 0); let Self { start } = self; self.end_at(start + offset) } } #[derive(Debug)] struct Capture<'rep> { name: &'rep str, span: Span, } impl<'rep> Capture<'rep> { fn new(name: &'rep str, span: Span) -> Self { Self { name, span } } } /// An iterator over the capture idents in an interpolated replacement string /// /// This code is adapted from the `regex` crate /// /// (hence the high quality doc comments). struct ReplaceCaptureIter<'rep>(CharIndices<'rep>); impl<'rep> ReplaceCaptureIter<'rep> { fn new(s: &'rep str) -> Self { Self(s.char_indices()) } } impl<'rep> Iterator for ReplaceCaptureIter<'rep> { type Item = Capture<'rep>; fn next(&mut self) -> Option { // Continually seek to `$` until we find one that has a capture group loop { let (start, _) = self.0.find(|(_, c)| *c == '$')?; let replacement = self.0.as_str(); let rep = replacement.as_bytes(); let open_span = Span::start_at(start + 1); let maybe_cap = match rep.first()? { // Handle escaping of '$'. b'$' => { self.0.next().unwrap(); None } b'{' => find_cap_ref_braced(rep, open_span), _ => find_cap_ref(rep, open_span), }; if let Some(cap) = maybe_cap { // Advance the inner iterator to consume the capture let mut remaining_bytes = cap.name.len(); while remaining_bytes > 0 { let (_, c) = self.0.next().unwrap(); remaining_bytes = remaining_bytes.checked_sub(c.len_utf8()).unwrap(); } return Some(cap); } } } } /// Parses a possible reference to a capture group name in the given text, /// starting at the beginning of `replacement`. /// /// If no such valid reference could be found, None is returned. fn find_cap_ref(rep: &[u8], open_span: SpanOpen) -> Option> { if rep.is_empty() { return None; } let mut cap_end = 0; while rep.get(cap_end).copied().map_or(false, is_valid_cap_letter) { cap_end += 1; } if cap_end == 0 { return None; } // We just verified that the range 0..cap_end is valid ASCII, so it must // therefore be valid UTF-8. If we really cared, we could avoid this UTF-8 // check via an unchecked conversion or by parsing the number straight from // &[u8]. let name = core::str::from_utf8(&rep[..cap_end]) .expect("valid UTF-8 capture name"); Some(Capture::new(name, open_span.end_offset(name.len()))) } /// Looks for a braced reference, e.g., `${foo1}`. This then looks for a /// closing brace and returns the capture reference within the brace. fn find_cap_ref_braced(rep: &[u8], open_span: SpanOpen) -> Option> { assert_eq!(b'{', rep[0]); let mut cap_end = 1; while rep.get(cap_end).map_or(false, |&b| b != b'}') { cap_end += 1; } if !rep.get(cap_end).map_or(false, |&b| b == b'}') { return None; } // When looking at braced names, we don't put any restrictions on the name, // so it's possible it could be invalid UTF-8. But a capture group name // can never be invalid UTF-8, so if we have invalid UTF-8, then we can // safely return None. let name = core::str::from_utf8(&rep[..cap_end + 1]).ok()?; Some(Capture::new(name, open_span.end_offset(name.len()))) } fn is_valid_cap_letter(b: u8) -> bool { matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_') } #[cfg(test)] mod tests { use super::*; use proptest::prelude::*; #[test] fn literal_dollar_sign() { let replace = "$$0"; let mut cap_iter = ReplaceCaptureIter::new(replace); assert!(cap_iter.next().is_none()); } #[test] fn wacky_captures() { let replace = "$foo $1 $1invalid ${1}valid ${valid} $__${__weird__}${${__}"; let cap_iter = ReplaceCaptureIter::new(replace); let expecteds = &[ "foo", "1", "1invalid", "{1}", "{valid}", "__", "{__weird__}", "{${__}", ]; for (&expected, cap) in expecteds.iter().zip(cap_iter) { assert_eq!(expected, cap.name, "name didn't match"); assert_eq!(expected, cap.span.slice(replace), "span didn't match"); } } const INTERPOLATED_CAPTURE: &str = ""; fn upstream_interpolate(s: &str) -> String { let mut dst = String::new(); regex_automata::util::interpolate::string( s, |_, dst| dst.push_str(INTERPOLATED_CAPTURE), |_| Some(0), &mut dst, ); dst } fn our_interpolate(s: &str) -> String { let mut after_last_write = 0; let mut dst = String::new(); for cap in ReplaceCaptureIter::new(s) { // This only iterates over the capture groups, so copy any text // before the capture // -1 here to exclude the `$` that starts a capture dst.push_str( &s[after_last_write..cap.span.start.checked_sub(1).unwrap()], ); // Interpolate our capture dst.push_str(INTERPOLATED_CAPTURE); after_last_write = cap.span.end; } if after_last_write < s.len() { // And now any text that was after the last capture dst.push_str(&s[after_last_write..]); } // Handle escaping literal `$`s dst.replace("$$", "$") } proptest! { // `regex-automata` doesn't expose a way to iterate over replacement // captures, but we can use our iterator to mimic interpolation, so that // we can pit the two against each other #[test] fn interpolation_matches_upstream(s in r"\PC*(\$\PC*){0,5}") { assert_eq!(our_interpolate(&s), upstream_interpolate(&s)); } } } sd-1.0.0/src/utils.rs000064400000000000000000000001201046102023000125160ustar 00000000000000pub(crate) fn unescape(s: &str) -> Option { unescape::unescape(s) } sd-1.0.0/tests/cli.rs000064400000000000000000000167031046102023000125160ustar 00000000000000#[cfg(test)] #[cfg(not(sd_cross_compile))] // Cross-compilation does not allow to spawn threads but `command.assert()` would do. mod cli { use anyhow::Result; use assert_cmd::Command; use std::io::prelude::*; fn sd() -> Command { Command::cargo_bin(env!("CARGO_PKG_NAME")).expect("Error invoking sd") } fn assert_file(path: &std::path::Path, content: &str) { assert_eq!(content, std::fs::read_to_string(path).unwrap()); } fn create_soft_link>( src: &P, dst: &P, ) -> Result<()> { #[cfg(target_family = "unix")] std::os::unix::fs::symlink(src, dst)?; #[cfg(target_family = "windows")] std::os::windows::fs::symlink_file(src, dst)?; Ok(()) } #[test] fn in_place() -> Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.write_all(b"abc123def")?; let path = file.into_temp_path(); sd().args(["abc\\d+", "", path.to_str().unwrap()]) .assert() .success(); assert_file(&path, "def"); Ok(()) } #[test] fn in_place_with_empty_result_file() -> Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.write_all(b"a7c")?; let path = file.into_temp_path(); sd().args(["a\\dc", "", path.to_str().unwrap()]) .assert() .success(); assert_file(&path, ""); Ok(()) } #[test] fn in_place_following_symlink() -> Result<()> { let dir = tempfile::tempdir()?; let path = dir.path(); let file = path.join("file"); let link = path.join("link"); create_soft_link(&file, &link)?; std::fs::write(&file, "abc123def")?; sd().args(["abc\\d+", "", link.to_str().unwrap()]) .assert() .success(); assert_file(&file, "def"); assert!(std::fs::symlink_metadata(link)?.file_type().is_symlink()); Ok(()) } #[test] fn replace_into_stdout() -> Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.write_all(b"abc123def")?; sd().args(["-p", "abc\\d+", "", file.path().to_str().unwrap()]) .assert() .success() .stdout(format!( "{}{}def\n", ansi_term::Color::Blue.prefix(), ansi_term::Color::Blue.suffix() )); assert_file(file.path(), "abc123def"); Ok(()) } #[test] fn stdin() -> Result<()> { sd().args(["abc\\d+", ""]) .write_stdin("abc123def") .assert() .success() .stdout("def"); Ok(()) } fn bad_replace_helper_styled(replace: &str) -> String { let err = sd() .args(["find", replace]) .write_stdin("stdin") .unwrap_err(); String::from_utf8(err.as_output().unwrap().stderr.clone()).unwrap() } fn bad_replace_helper_plain(replace: &str) -> String { let stderr = bad_replace_helper_styled(replace); // TODO: no easy way to toggle off styling yet. Add a `--color ` // flag, and respect things like `$NO_COLOR`. `ansi_term` is // unmaintained, so we should migrate off of it anyways console::AnsiCodeIterator::new(&stderr) .filter_map(|(s, is_ansi)| (!is_ansi).then_some(s)) .collect() } #[test] fn fixed_strings_ambiguous_replace_is_fine() { sd().args([ "--fixed-strings", "foo", "inner_before $1fine inner_after", ]) .write_stdin("outer_before foo outer_after") .assert() .success() .stdout("outer_before inner_before $1fine inner_after outer_after"); } #[test] fn ambiguous_replace_basic() { let plain_stderr = bad_replace_helper_plain("before $1bad after"); insta::assert_snapshot!(plain_stderr, @r###" error: The numbered capture group `$1` in the replacement text is ambiguous. hint: Use curly braces to disambiguate it `${1}bad`. before $1bad after ^^^^ "###); } #[test] fn ambiguous_replace_variable_width() { let plain_stderr = bad_replace_helper_plain("\r\n\t$1bad\r"); insta::assert_snapshot!(plain_stderr, @r###" error: The numbered capture group `$1` in the replacement text is ambiguous. hint: Use curly braces to disambiguate it `${1}bad`. ␍␊␉$1bad␍ ^^^^ "###); } #[test] fn ambiguous_replace_multibyte_char() { let plain_stderr = bad_replace_helper_plain("😈$1bad😇"); insta::assert_snapshot!(plain_stderr, @r###" error: The numbered capture group `$1` in the replacement text is ambiguous. hint: Use curly braces to disambiguate it `${1}bad`. 😈$1bad😇 ^^^^ "###); } #[test] fn ambiguous_replace_issue_44() { let plain_stderr = bad_replace_helper_plain("$1Call $2($5, GetFM20ReturnKey(), $6)"); insta::assert_snapshot!(plain_stderr, @r###" error: The numbered capture group `$1` in the replacement text is ambiguous. hint: Use curly braces to disambiguate it `${1}Call`. $1Call $2($5, GetFM20ReturnKey(), $6) ^^^^^ "###); } // NOTE: styled terminal output is platform dependent, so convert to a // common format, in this case HTML, to check #[test] fn ambiguous_replace_ensure_styling() { let styled_stderr = bad_replace_helper_styled("\t$1bad after"); let html_stderr = ansi_to_html::convert(&styled_stderr, true, true).unwrap(); insta::assert_snapshot!(html_stderr, @r###" error: The numbered capture group `$1` in the replacement text is ambiguous. hint: Use curly braces to disambiguate it `${1}bad`. $1bad after ^^^^ "###); } #[test] fn limit_replacements_file() -> Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.write_all(b"foo\nfoo\nfoo")?; let path = file.into_temp_path(); sd().args(["-n", "1", "foo", "bar", path.to_str().unwrap()]) .assert() .success(); assert_file(&path, "bar\nfoo\nfoo"); Ok(()) } #[test] fn limit_replacements_file_preview() -> Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.write_all(b"foo\nfoo\nfoo")?; let path = file.into_temp_path(); sd().args([ "--preview", "-n", "1", "foo", "bar", path.to_str().unwrap(), ]) .assert() .success() .stdout(format!( "{}\nfoo\nfoo\n", ansi_term::Color::Blue.paint("bar") )); Ok(()) } #[test] fn limit_replacements_stdin() { sd().args(["-n", "1", "foo", "bar"]) .write_stdin("foo\nfoo\nfoo") .assert() .success() .stdout("bar\nfoo\nfoo"); } #[test] fn limit_replacements_stdin_preview() { sd().args(["--preview", "-n", "1", "foo", "bar"]) .write_stdin("foo\nfoo\nfoo") .assert() .success() .stdout("bar\nfoo\nfoo"); } }