├── .github └── workflows │ ├── ci.yml │ ├── pages.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── codecov.yml ├── docs ├── CHANGELOG.md ├── LICENSE.md ├── compare.md ├── examples.md ├── extra.css ├── filters │ ├── field.md │ ├── format.md │ ├── generate.md │ ├── path.md │ ├── regex.md │ └── string.md ├── images │ └── diagram.svg ├── index.md ├── input.md ├── install.md ├── output.md ├── pattern.md └── usage.md ├── mkdocs.yml ├── rustfmt.toml ├── src ├── bin │ ├── cpb │ │ ├── cli.rs │ │ └── main.rs │ ├── mvb │ │ ├── cli.rs │ │ └── main.rs │ └── rew │ │ ├── cli.rs │ │ ├── counter.rs │ │ ├── input.rs │ │ ├── main.rs │ │ ├── output.rs │ │ ├── pattern │ │ ├── char.rs │ │ ├── error.rs │ │ ├── escape.rs │ │ ├── eval.rs │ │ ├── explain.rs │ │ ├── field.rs │ │ ├── filter.rs │ │ ├── help.rs │ │ ├── index.rs │ │ ├── integer.rs │ │ ├── lexer.rs │ │ ├── mod.rs │ │ ├── number.rs │ │ ├── padding.rs │ │ ├── parse.rs │ │ ├── parser.rs │ │ ├── path.rs │ │ ├── range.rs │ │ ├── reader.rs │ │ ├── regex.rs │ │ ├── repeat.rs │ │ ├── replace.rs │ │ ├── substr.rs │ │ ├── switch.rs │ │ ├── symbols.rs │ │ ├── utils.rs │ │ └── uuid.rs │ │ └── regex.rs └── common │ ├── color.rs │ ├── help.rs │ ├── input.rs │ ├── lib.rs │ ├── output.rs │ ├── run.rs │ ├── symbols.rs │ ├── testing.rs │ ├── transfer │ ├── fs.rs │ ├── input.rs │ ├── mod.rs │ ├── output.rs │ ├── run.rs │ └── testing.rs │ └── utils.rs └── tests ├── cpb.rs ├── mvb.rs ├── rew.rs ├── rew_cpb.rs ├── rew_mvb.rs └── utils.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches-ignore: [ stable-docs, gh-pages ] 7 | pull_request: 8 | branches-ignore: [ stable-docs, gh-pages ] 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | check: 15 | name: Check 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v2 20 | 21 | - name: Install toolchain 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | toolchain: stable 25 | profile: minimal 26 | override: true 27 | 28 | - name: Run cargo check 29 | uses: actions-rs/cargo@v1 30 | with: 31 | command: check 32 | 33 | lint: 34 | name: Lint 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Checkout sources 38 | uses: actions/checkout@v2 39 | 40 | - name: Install toolchain 41 | uses: actions-rs/toolchain@v1 42 | with: 43 | toolchain: stable 44 | profile: minimal 45 | override: true 46 | components: rustfmt, clippy 47 | 48 | - name: Run cargo fmt 49 | uses: actions-rs/cargo@v1 50 | with: 51 | command: fmt 52 | args: -- --check 53 | 54 | - name: Run cargo clippy 55 | uses: actions-rs/cargo@v1 56 | with: 57 | command: clippy 58 | args: -- --deny warnings 59 | 60 | test: 61 | name: Test 62 | runs-on: ${{ matrix.os }} 63 | strategy: 64 | matrix: 65 | os: 66 | - ubuntu-latest 67 | - windows-latest 68 | - macos-latest 69 | steps: 70 | - name: Checkout sources 71 | uses: actions/checkout@v2 72 | 73 | - name: Install toolchain 74 | uses: actions-rs/toolchain@v1 75 | with: 76 | toolchain: stable 77 | profile: minimal 78 | override: true 79 | 80 | - name: Run cargo test 81 | uses: actions-rs/cargo@v1 82 | with: 83 | command: test 84 | args: --no-fail-fast 85 | 86 | coverage: 87 | name: Coverage 88 | runs-on: ubuntu-latest 89 | steps: 90 | - name: Checkout sources 91 | uses: actions/checkout@v2 92 | 93 | - name: Install toolchain 94 | uses: actions-rs/toolchain@v1 95 | with: 96 | toolchain: nightly 97 | profile: minimal 98 | override: true 99 | 100 | - name: Install grcov 101 | uses: actions-rs/install@v0.1 102 | with: 103 | crate: grcov 104 | use-tool-cache: true 105 | 106 | - name: Install rust-covfix 107 | uses: actions-rs/install@v0.1 108 | with: 109 | crate: rust-covfix 110 | use-tool-cache: true 111 | 112 | - name: Run cargo test 113 | uses: actions-rs/cargo@v1 114 | with: 115 | command: test 116 | args: --no-fail-fast 117 | env: 118 | CARGO_INCREMENTAL: '0' 119 | RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests' 120 | RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests' 121 | 122 | - name: Prepare grcov input 123 | run: zip -0 ccov.zip target/debug/deps/{rew,cpb,mvb}*.{gcda,gcno} 124 | 125 | - name: Run grcov 126 | run: grcov ccov.zip --source-dir . --output-path lcov.info --llvm --branch --ignore-not-existing --ignore "/*" --ignore "tests/*" 127 | 128 | - name: Run rust-covfix 129 | run: rust-covfix --verbose --output lcov_correct.info lcov.info 130 | 131 | - name: Upload coverage 132 | if: ${{ github.event_name == 'push' }} 133 | uses: codecov/codecov-action@v1 134 | with: 135 | file: lcov_correct.info 136 | fail_ci_if_error: true 137 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Pages 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ stable-docs ] 7 | paths: [ 'docs/**', CHANGELOG.md , LICENSE.md, mkdocs.yml ] 8 | 9 | jobs: 10 | deploy: 11 | name: Deploy 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: 3.x 21 | 22 | - name: Install mkdocs material 23 | run: pip install mkdocs-material 24 | 25 | - name: Deploy pages 26 | run: mkdocs gh-deploy --force 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | include: 17 | - os: ubuntu-latest 18 | os-type: linux 19 | architecture: x86_64 20 | target: x86_64-unknown-linux-gnu 21 | binutils-target: '' 22 | use-cross: false 23 | 24 | - os: ubuntu-latest 25 | os-type: linux 26 | architecture: i686 27 | target: i686-unknown-linux-gnu 28 | binutils-target: '' 29 | use-cross: true 30 | 31 | - os: ubuntu-latest 32 | os-type: linux 33 | architecture: arm64 34 | target: aarch64-unknown-linux-gnu 35 | binutils-target: aarch64-linux-gnu 36 | use-cross: true 37 | 38 | - os: windows-latest 39 | os-type: windows 40 | architecture: x86_64 41 | target: x86_64-pc-windows-msvc 42 | binutils-target: '' 43 | use-cross: false 44 | 45 | - os: macos-latest 46 | os-type: macos 47 | architecture: x86_64 48 | target: x86_64-apple-darwin 49 | binutils-target: '' 50 | use-cross: false 51 | 52 | steps: 53 | - name: Install toolchain 54 | uses: actions-rs/toolchain@v1 55 | with: 56 | toolchain: stable 57 | profile: minimal 58 | override: true 59 | 60 | - name: Install strip 61 | if: ${{ matrix.binutils-target != '' }} 62 | shell: bash 63 | run: | 64 | sudo apt update 65 | sudo apt-get install -y binutils-${{ matrix.binutils-target }} 66 | 67 | - name: Checkout sources 68 | uses: actions/checkout@v2 69 | 70 | - name: Run cargo build 71 | uses: actions-rs/cargo@v1 72 | with: 73 | command: build 74 | args: --release --target ${{ matrix.target }} 75 | use-cross: ${{ matrix.use-cross }} 76 | 77 | - name: Package binaries 78 | shell: bash 79 | run: | 80 | VERSION=${GITHUB_REF/refs\/tags\/v/} 81 | REALEASE=rew-$VERSION-${{ matrix.os-type }}-${{ matrix.architecture }} 82 | BINARIES=(rew cpb mvb) 83 | STRIP=strip 84 | 85 | if [[ ${{ runner.os }} == Windows ]]; then 86 | BINARIES=(${BINARIES[@]/%/.exe}) 87 | fi 88 | 89 | if [[ '${{ matrix.binutils-target }}' ]]; then 90 | STRIP=${{ matrix.binutils-target }}-$STRIP 91 | fi 92 | 93 | cd target/${{ matrix.target }}/release 94 | $STRIP "${BINARIES[@]}" 95 | tar czvf $REALEASE.tar.gz "${BINARIES[@]}" 96 | 97 | if [[ ${{ runner.os }} == Windows ]]; then 98 | certutil -hashfile $REALEASE.tar.gz sha256 | grep -E [A-Fa-f0-9]{64} > $REALEASE.sha256 99 | else 100 | shasum -a 256 $REALEASE.tar.gz > $REALEASE.sha256 101 | fi 102 | 103 | - name: Upload release 104 | uses: softprops/action-gh-release@v1 105 | with: 106 | files: | 107 | target/${{ matrix.target }}/release/rew-*.tar.gz 108 | target/${{ matrix.target }}/release/rew-*.sha256 109 | env: 110 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 111 | 112 | publish: 113 | name: Publish 114 | runs-on: ubuntu-latest 115 | needs: release 116 | steps: 117 | - name: Checkout sources 118 | uses: actions/checkout@v2 119 | 120 | - name: Install toolchain 121 | uses: actions-rs/toolchain@v1 122 | with: 123 | toolchain: stable 124 | profile: minimal 125 | override: true 126 | 127 | - name: Run cargo publish 128 | uses: actions-rs/cargo@v1 129 | with: 130 | command: publish 131 | args: --allow-dirty --token ${{ secrets.CARGO_API_KEY }} 132 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /pages 3 | /target 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 📈 Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | - `--explain-filters` flag to print an explanation like `--explain` but only for filters. 13 | - `-j, --json-lines` flag to enable JSON lines output mode. 14 | 15 | ### Changed 16 | 17 | - Regex match filter `=` requires index (or index range) as a first parameter. 18 | - Repetition filter `*` can be used without its *value* parameter to repeat input instead (e.g., `*2` instead of `*2:abc`). 19 | - Column filter `&` is now called *Field filter*. 20 | - Global column separator is now called *Default field separator*. 21 | - Default field separator is `\s+` (regular expression) instead of `\t` (horizontal tab). 22 | - Simplified and more consistent parse error messages. 23 | - Parse error messages contains hint how to resolve the error. 24 | 25 | ## [0.3.0] - 2021-03-29 26 | 27 | ### Added 28 | 29 | - `&` filter which splits value using a separator and outputs N-th column. 30 | - `-q, --quote` flag to automatically wrap output of every pattern expression in quotes. 31 | - `-l, --read-end` flag to require the last input value to be properly terminated. 32 | - `-I, --no-stdin` flag to disable reading values from standard input. 33 | 34 | ### Changed 35 | 36 | - `%` is the default pattern escape character instead of `#`. 37 | - `n` filter (substring) was renamed to `#`. 38 | - `N` filter (substring with backward indexing) was replaced by use of `#` with negative indexing (e.g., `#-2`). 39 | - Parsing of `A+L` range can no longer fail with overflow error. Such range would be now resolved as `A-` (from `A` to end). 40 | - Capture groups of a global regex need to be prefixed with `$` (e.g., `{$1}` instead of `{1}`). 41 | - More lenient number parsing that ignore multiple leading zeros (e.g., `001` is interpreted as `1`). 42 | - Output of `--explain` flag and error output have escaped non-printable and other special characters (newline, tab, etc.). 43 | - Output of `--help-pattern` includes list of escape sequences. 44 | - Output of `--help-filters` flag has more readable layout. 45 | - `-T, --no-trailing-delimiter` flag was renamed to `-L, --no-print-end`. 46 | - `-s, --fail-at-end` flag was renamed to `-F, --fail-at-end`. 47 | - `-b, -diff` flag was renamed to `-d, --diff` flag. 48 | 49 | ### Fixed 50 | 51 | - `A+L` range is correctly evaluated as "from `A` to `A+L`" (not `A+L+1` as previously). 52 | - `-h, --help` flag displays correct position of `--` argument in usage. 53 | 54 | ## [0.2.0] - 2021-02-14 55 | 56 | ### Added 57 | 58 | - `@` filter (regular expression switch). 59 | - Alternative way to write range of substring filters as `start+length`. 60 | 61 | ### Changed 62 | 63 | - `l` filter (to lowercase) was renamed to `v`. 64 | - `L` filter (to uppercase) was renamed to `^`. 65 | - `0` is now a valid filter a no longer considered error. 66 | - Simplified error message for an invalid range. 67 | - Simplified output of `--help-pattern` and `--help-filters` flags. 68 | - Output of `-h, --help` flag is organized into sections. 69 | - Output of `-h, --help` flag uses more colors in descriptions. 70 | - Regular expression `-e. --regex` / `-E. --regex-filename` is now called *global* instead of *external*. 71 | 72 | ### Fixed 73 | 74 | - `--help-filters` flag displays correct name of `i` / `I` filters. 75 | 76 | ## [0.1.0] - 2020-12-13 77 | 78 | Initial release. 79 | 80 | [Unreleased]: https://github.com/jpikl/rew/compare/v0.3.0...HEAD 81 | [0.3.0]: https://github.com/jpikl/rew/compare/v0.2.0...v0.3.0 82 | [0.2.0]: https://github.com/jpikl/rew/compare/v0.1.0...v0.2.0 83 | [0.1.0]: https://github.com/jpikl/rew/releases/tag/v0.1.0 84 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rew" 3 | version = "0.4.0" 4 | description = "A text processing CLI tool that rewrites FS paths according to a pattern." 5 | categories = ["command-line-utilities", "text-processing", "filesystem"] 6 | keywords = ["tool", "pattern", "regex", "rename", "path"] 7 | authors = ["Jan Pikl "] 8 | repository = "https://github.com/jpikl/rew" 9 | documentation = "https://jpikl.github.io/rew" 10 | license = "MIT" 11 | edition = "2021" 12 | 13 | [dependencies] 14 | atty = "0.2.14" 15 | clap = { version = "3.0.0-beta.5", features = ["wrap_help"] } 16 | fs_extra = "1.2.0" 17 | indoc = "1.0" 18 | lazy_static = "1.4.0" 19 | normpath = "0.3" 20 | num-traits = "0.2.14" 21 | pathdiff = "0.2.0" 22 | regex = "1" # When upgrading, change also version in docs URL in help.rs 23 | same-file = "1" 24 | rand = "0.8.0" 25 | termcolor = "1.1.0" 26 | unidecode = "0.3.0" 27 | uuid = { version = "0.8", features = ["v4"] } 28 | 29 | [dev-dependencies] 30 | assert_cmd = "2.0.2" 31 | assert_fs = "1.0.0" 32 | claim = "0.5.0" 33 | naughty-strings = "0.2.3" 34 | ntest = "0.7.2" 35 | predicates = "2.1.0" 36 | test-case = "1.1.0" 37 | 38 | [[bin]] 39 | name = "rew" 40 | path = "src/bin/rew/main.rs" 41 | 42 | [[bin]] 43 | name = "mvb" 44 | path = "src/bin/mvb/main.rs" 45 | 46 | [[bin]] 47 | name = "cpb" 48 | path = "src/bin/cpb/main.rs" 49 | 50 | [lib] 51 | name = "common" 52 | path = "src/common/lib.rs" 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 Jan Pikl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rew 2 | 3 | A text processing CLI tool that rewrites FS paths according to a pattern. 4 | 5 | [![Build](https://img.shields.io/github/actions/workflow/status/jpikl/rew/ci.yml?branch=master)](https://github.com/jpikl/rew/actions/workflows/ci.yml) 6 | [![Coverage](https://img.shields.io/codecov/c/github/jpikl/rew/master?token=9K88E1ZCBU)](https://codecov.io/gh/jpikl/rew) 7 | [![Version](https://img.shields.io/crates/v/rew.svg)](https://crates.io/crates/rew) 8 | [![Dependencies](https://deps.rs/repo/github/jpikl/rew/status.svg)](https://deps.rs/repo/github/jpikl/rew) 9 | [![Downloads](https://img.shields.io/crates/d/rew)](https://crates.io/crates/rew) 10 | [![License](https://img.shields.io/crates/l/rew.svg)](https://github.com/jpikl/rew/blob/master/LICENSE.md) 11 | 12 | ## How rew works 13 | 14 | 1. Reads values from standard [input](https://jpikl.github.io/rew/input.html). 15 | 2. Rewrites them according to a [pattern](https://jpikl.github.io/rew/pattern.html). 16 | 3. Prints results to standard [output](https://jpikl.github.io/rew/output.html). 17 | 18 | ![How rew works](docs/images/diagram.svg) 19 | 20 | Input values are assumed to be FS paths, however, `rew` is able to process any UTF-8 encoded text. 21 | 22 | ```bash 23 | find -iname '*.jpeg' | rew 'img_{C}.{e|l|r:e}' 24 | ``` 25 | 26 | `rew` is also distributed with two accompanying utilities (`mvb` and `cpb`) which move/copy files and directories, based on `rew` output. 27 | 28 | ```bash 29 | find -iname '*.jpeg' | rew 'img_{C}.{e|l|r:e}' -d | mvb 30 | ``` 31 | 32 | ## Documentation 33 | 34 | - [📦 Installation](https://jpikl.github.io/rew/install) 35 | - [🚀 Usage](https://jpikl.github.io/rew/usage) 36 | - [✏️ Pattern](https://jpikl.github.io/rew/pattern) 37 | - [🛤 Path filters](https://jpikl.github.io/rew/filters/path) 38 | - [🆎 Substring filters](https://jpikl.github.io/rew/filters/substr) 39 | - [📊 Field filters](https://jpikl.github.io/rew/filters/field) 40 | - [🔍 Replace filters](https://jpikl.github.io/rew/filters/replace) 41 | - [⭐️ Regex filters](https://jpikl.github.io/rew/filters/regex) 42 | - [🎨 Format filters](https://jpikl.github.io/rew/filters/format) 43 | - [🏭 Generators](https://jpikl.github.io/rew/filters/generate) 44 | - [⌨️ Input](https://jpikl.github.io/rew/input) 45 | - [💬 Output](https://jpikl.github.io/rew/output) 46 | - [🔬 Comparison](https://jpikl.github.io/rew/comparison) 47 | - [🗃 Examples](https://jpikl.github.io/rew/examples) 48 | - [📈 Changelog](https://jpikl.github.io/rew/changelog) 49 | 50 | ## License 51 | 52 | `rew` is licensed under the [MIT license](LICENSE.md). 53 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | parsers: 2 | gcov: 3 | branch_detection: 4 | conditional: yes 5 | loop: yes 6 | method: no 7 | macro: no 8 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /docs/LICENSE.md: -------------------------------------------------------------------------------- 1 | ../LICENSE.md -------------------------------------------------------------------------------- /docs/compare.md: -------------------------------------------------------------------------------- 1 | # 🔬 Comparison 2 | 3 | Let us compare `rew` to a variety of existing tools. 4 | 5 | ## rename 6 | 7 | Both `rename` abd `rew` can be used to rename multiple files. 8 | 9 | `rename` requires all inputs to be passed as arguments. 10 | This means you have to use `xargs` when processing output of `find`. 11 | `rew` can read values directly from standard input. 12 | 13 | Additionally, `rew` is only a text-processing tool and cannot rename files by itself. 14 | You have to use accompanying `mvb` / `cpb` utilities, or you can generate and execute shell code. 15 | 16 | ```bash 17 | find -name '*.jpeg' | xargs rename .jpeg .jpg # Rename *.jpeg files to *.jpg 18 | find -name '*.jpeg' | rew -d '{B}.jpg' | mvb # The same thing using rew + mvb 19 | find -name '*.jpeg' | rew -q 'mv {} {B}.jpg' | sh # The same thing using rew + mv + sh 20 | ``` 21 | 22 | ## dirname 23 | 24 | Both `dirname` and `rew` can remove last component from a path: 25 | 26 | ```bash 27 | dirname 'dir/file.txt' # Will print "dir" 28 | rew '{d}' 'dir/file.txt' # The same thing using rew 29 | ``` 30 | 31 | ## basename 32 | 33 | Both `basename` and `rew` can remove leading directories from a path: 34 | 35 | ```bash 36 | basename 'dir/file.txt' # Will print "file.txt" 37 | rew '{f}' 'dir/file.txt' # The same thing using rew 38 | ``` 39 | 40 | `basename` can additionally remove filename extension, but we have to manually provide it as a suffix. 41 | `rew` is able to remove filename extension automatically: 42 | 43 | ```bash 44 | basename 'dir/file.txt' '.txt' # Will print "file" 45 | rew '{b}' 'dir/file.txt' # The same thing using rew 46 | ``` 47 | 48 | In case the suffix does not represent an extension, `rew` requires an additional filter to remove it: 49 | 50 | ```bash 51 | basename 'dir/file_txt' '_txt' # Will print "file" 52 | rew '{f|s:_txt$}' 'dir/file_txt' # The same thing using rew 53 | ``` 54 | 55 | ## realpath 56 | 57 | Both `realpath` and `rew` can resolve canonical form of a path: 58 | 59 | ```bash 60 | realpath -e '/usr/../home' # Will print "/home" 61 | rew '{P}' '/usr/../home' # The same thing using rew 62 | ``` 63 | 64 | Or they can both compute a relative path: 65 | 66 | ```bash 67 | realpath --relative-to='/home' '/usr' # Will print "../usr" 68 | rew -w '/home' '{A}' '/usr' # The same thing using rew 69 | ``` 70 | 71 | ## pwd 72 | 73 | Both `pwd` and `rew` can print the current working directory: 74 | 75 | ```bash 76 | pwd # pwd is obviously easier to use 77 | rew '{w}' '' # rew requires an additional input 78 | ``` 79 | 80 | ## sed 81 | 82 | Both `sed` and `rew` can replace text matching a regular expression: 83 | 84 | ```bash 85 | echo '12 ab 34' | sed -E 's/([0-9]+)/_\1_/g' # Will print "_12_ ab _34_" 86 | echo '12 ab 34' | rew '{S:(\d+):_$1_}' # The same thing using rew 87 | ``` 88 | 89 | ## cut 90 | 91 | Both `cut` and `rew` can print substring: 92 | 93 | ```bash 94 | echo 'abcde' | cut -c '2-4' # Will print "bcd" 95 | echo 'abcde' | rew '{#2-4}' # The same thing using rew 96 | ``` 97 | 98 | Or they can both print fields: 99 | 100 | ```bash 101 | echo 'ab,cd,ef' | cut -d',' -f2 # Will print "cd" 102 | echo 'ab,cd,ef' | rew -s',' '{&2}' # The same thing using rew 103 | ``` 104 | 105 | ## awk 106 | 107 | `awk` is obviously a more powerful tool than `rew`. 108 | However, there are some use cases where `rew` can replace `awk` using more compact pattern syntax. 109 | 110 | Printing substring: 111 | 112 | ```bash 113 | echo 'abcde' | awk '{print substr($0,2,3)}' # Will print "bcd" 114 | echo 'abcde' | rew '{#2+3}' # The same thing using rew 115 | ``` 116 | 117 | Printing field: 118 | 119 | ```bash 120 | echo 'ab,cd,ef' | awk -F',' '{print $2}' # Will print "cd" 121 | echo 'ab,cd,ef' | rew -s',' '{&2}' # The same thing using rew 122 | ``` 123 | 124 | Printing first match of a regular expression: 125 | 126 | ```bash 127 | echo 'ab 12 cd' | awk 'match($0,/[0-9]+/) {print substr($0,RSTART,RLENGTH)}' # Will print "12" 128 | echo 'ab 12 cd' | rew '{=\d+}' # The same thing using rew 129 | ``` 130 | 131 | ## grep 132 | 133 | Both `grep` and `rew` can print matches of a regular expression: 134 | 135 | ```bash 136 | echo 'ab 12 cd' | grep -Po '\d+' # Will print "12" 137 | echo 'ab 12 cd' | rew '{=\d+}' # The same thing using rew 138 | ``` 139 | 140 | If an input line contains multiple matches, `grep` will print each on a separate line. 141 | `rew` will, however, print only the first match from each line. 142 | This is because `rew` transforms lines in 1-to-1 correspondence. 143 | 144 | In this particular case, we can workaround it, using raw output mode `-R` and regex replace filters `sS`. 145 | 146 | ```bash 147 | echo '12 ab 34' | grep -Po '\d+' # Will print "12" and "34" 148 | echo '12 ab 34' | rew -R '{s:^\D+$|S:\D*(\d+)\D*:$1%n}' # The same thing using rew 149 | ``` 150 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # 🗃 Examples 2 | 3 | > ℹ️ Use `rew --explain ` to print detailed explanation what a certain pattern does. 4 | 5 | ## Path processing 6 | 7 | Print contents of the current working directory as absolute paths. 8 | 9 | ```bash 10 | rew '{a}' * 11 | ``` 12 | 13 | The previous `*` shell expansion would not work for an empty directory. 14 | As a workaround, we can read paths from standard input. 15 | 16 | ```bash 17 | dir | rew '{a}' 18 | ``` 19 | 20 | ## Batch rename 21 | 22 | Rename all `*.jpeg` files to `*.jpg`. 23 | 24 | ```bash 25 | find -name '*.jpeg' | rew -d '{B}.jpg' | mvb -v 26 | ``` 27 | 28 | The same thing but we generate and execute shell code. 29 | 30 | ```bash 31 | find -name '*.jpeg' | rew -q 'mv -v {} {B}.jpg' | sh 32 | ``` 33 | 34 | Normalize base names of files to `file_001`, `file_002`, ... 35 | 36 | ```bash 37 | find -type f | rew -d '{d}/file_{C|<3:0}{E}' | mvb -v 38 | ``` 39 | 40 | Flatten directory structure `./dir/subdir/` to `./dir_subdir/`. 41 | 42 | ```bash 43 | find -mindepth 2 -maxdepth 2 -type d | rew -d '{D}_{F}' | mvb -v 44 | ``` 45 | 46 | ## Batch copy 47 | 48 | Make backup copy of each `*.txt` file with `.txt.bak` extension in the same directory. 49 | 50 | ```bash 51 | find -name '*.txt' | rew -d '{}.bak' | cpb -v 52 | ``` 53 | 54 | Copy `*.txt` files to the `~/Backup` directory. Preserve directory structure. 55 | 56 | ```bash 57 | find -name '*.txt' | rew -d "$HOME/Backup/{p}" | cpb -v 58 | ``` 59 | 60 | The same thing but with collapsed output directory structure. 61 | 62 | ```bash 63 | find -name '*.txt' | rew -d "$HOME/Backup/{f}" | cpb -v 64 | ``` 65 | 66 | The same thing but we also append randomly generated base name suffix to avoid collisions. 67 | 68 | ```bash 69 | find -name '*.txt' | rew -d "$HOME/Backup/{b}_{U}.{e}" | cpb -v 70 | ``` 71 | 72 | ## Text processing 73 | 74 | Normalize line endings in a file to `LF` 75 | 76 | ```bash 77 | rw output.txt # LF is the default output terminator 78 | ``` 79 | 80 | Normalize line endings in a file to `CR+LF`. 81 | 82 | ```bash 83 | rew -T$'\r\n' output.txt 84 | ``` 85 | 86 | Replace tabs with 4 spaces. 87 | 88 | ```bash 89 | rew '{R:%t: }' output.txt 90 | ``` 91 | 92 | That would also normalize line endings. 93 | To prevent such behaviour, we can process the text as a whole. 94 | 95 | ```bash 96 | rew -rR '{R:%t: }' output.txt 97 | ``` 98 | 99 | Print the first word from each line in lowercase and with removed diacritics (accents). 100 | 101 | ```bash 102 | rew '{=\S+|v|i}' output.csv 111 | ``` 112 | 113 | The same thing but we use regex replace filter. 114 | 115 | ```bash 116 | rew '{s/([^,]*),([^,]*),(.*)/$2,$1,$3}' output.csv 117 | ``` 118 | -------------------------------------------------------------------------------- /docs/extra.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --md-text-font: Roboto, "Noto Color Emoji"; 3 | } 4 | -------------------------------------------------------------------------------- /docs/filters/field.md: -------------------------------------------------------------------------------- 1 | # 📊 Field filters 2 | 3 | | Filter | Description | 4 | | ------- | --------------------------------------------------------------- | 5 | | `&N:S` | Split value using separator `S`, output `N`-th field.
*Field indices `N` start from 1.
Use `-N` for backward indexing.
Any other character than `:` can be also used as a delimiter.
Use of `/` as a delimiter has special meaning (see below).* | 6 | | `&N/S` | Split value using regular expression `S`, output `N`-th field. | 7 | | `&N` | Split value using default separator, output `N`-th field. | 8 | 9 | The default field separator is regular expression `\s+`. 10 | 11 | - Use `-s, --separator` option to change it to a string. 12 | - Use `-S, --separator-regex` option to change it to a regular expression. 13 | 14 | ```bash 15 | echo a1-b2 | rew '{&1} {&2}' -s'-' # Will print "a1 b2" 16 | echo a1-b2 | rew '{&1} {&2}' -S'[^a-z]+' # Will print "a b" 17 | ``` 18 | 19 | Examples: 20 | 21 | | Input | Pattern | Output | Input | Pattern | Output | 22 | | -------- | -------------- | --------- | -------- | --------------- | --------- | 23 | | `a1 b2` | `{&1}` | `a1` | `a1 b2` | `{&-1}` | `b2` | 24 | | `a1 b2` | `{&2}` | `b2` | `a1 b2` | `{&-2}` | `a1` | 25 | | `a1--b2` | `{&1:-}` | `a1` | `a1--b2` | `{&-1:-}` | `b2` | 26 | | `a1--b2` | `{&2:-}` | *(empty)* | `a1--b2` | `{&-2:-}` | *(empty)* | 27 | | `a1--b2` | `{&3:-}` | `b2` | `a1--b2` | `{&-3:-}` | `a1` | 28 | | `a1--b2` | `{&1/[^a-z]+}` | `a` | `a1--b2` | `{&-1/[^a-z]+}` | *(empty)* | 29 | | `a1--b2` | `{&2/[^a-z]+}` | `b` | `a1--b2` | `{&-2/[^a-z]+}` | `b` | 30 | | `a1--b2` | `{&3/[^a-z]+}` | *(empty)* | `a1--b2` | `{&-3/[^a-z]+}` | `a` | 31 | -------------------------------------------------------------------------------- /docs/filters/format.md: -------------------------------------------------------------------------------- 1 | # 🎨 Format filters 2 | 3 | | Filter | Description | 4 | | ------ | -------------------------------------- | 5 | | `t` | Trim white-spaces from both sides. | 6 | | `v` | Convert to lowercase. | 7 | | `^` | Convert to uppercase. | 8 | | `i` | Convert non-ASCII characters to ASCII. | 9 | | `I` | Remove non-ASCII characters. | 10 | | `*N` | Repeat `N` times. | 11 | | `<*Any other character than `:` can be also used as a delimiter.* | 13 | | `>>M` | Right pad with mask `M`. | 14 | | `>N:M` | Right pad with `N` times repeated mask `M`.
*Any other character than `:` can be also used as a delimiter.* | 15 | 16 | Examples: 17 | 18 | | Input | Pattern | Output | 19 | | ---------- | ------------ | -------- | 20 | | `..a..b..` | `{t}` | `a..b` *(dots are white-spaces)* | 21 | | `aBčĎ` | `{v}` | `abčď` | 22 | | `aBčĎ` | `{^}` | `ABČĎ` | 23 | | `aBčĎ` | `{i}` | `aBcD` | 24 | | `aBčĎ` | `{I}` | `aB` | 25 | | `abc` | `{*2}` | `abcabc` | 26 | | `abc` | `{<<123456}` | `123abc` | 27 | | `abc` | `{>>123456}` | `abc456` | 28 | | `abc` | `{<3:XY}` | `XYXabc` | 29 | | `abc` | `{>3:XY}` | `abcYXY` | 30 | -------------------------------------------------------------------------------- /docs/filters/generate.md: -------------------------------------------------------------------------------- 1 | # 🏭 Generators 2 | 3 | Unlike other filters, generator output is not produced from its input. 4 | However, it is still possible (although meaningless) to pipe input into a generator. 5 | 6 | | Filter | Description | 7 | | ------ | -------------------------------------- | 8 | | `*N:V` | Repeat `N` times `V`.
*Any other character than `:` can be also used as a delimiter.* | 9 | | `c` | Local counter | 10 | | `C` | Global counter | 11 | | `uA-B` | Random 64-bit number (`A` ≤ `u` ≤ `B`) | 12 | | `uA-` | Random 64-bit number (`A` ≤ `u`) | 13 | | `u` | Random 64-bit number | 14 | | `U` | Random UUID | 15 | 16 | Examples: 17 | 18 | | Pattern | Output | 19 | | --------- | ------------------------------------------------- | 20 | | `{*3:ab}` | `ababab` | 21 | | `{c}` | *(see below)* | 22 | | `{C}` | *(see below)* | 23 | | `{u0-99}` | *(random number between 0 and 99)* | 24 | | `{U}` | `5eefc76d-0ca1-4631-8fd0-62eeb401c432` *(random)* | 25 | 26 | - Global counter `C` is incremented for every input value. 27 | - Local counter `c` is incremented per parent directory (assuming input value is a FS path). 28 | - Both counters start at 1 and are incremented by 1. 29 | 30 | | Input | Global counter | Local counter | 31 | | ----- | -------------- | ------------- | 32 | | `A/1` | 1 | 1 | 33 | | `A/2` | 2 | 2 | 34 | | `B/1` | 3 | 1 | 35 | | `B/2` | 4 | 2 | 36 | 37 | - Use `-c, --local-counter` option to change local counter configuration. 38 | - Use `-C, --global-counter` option to change global counter configuration. 39 | 40 | ```bash 41 | rew -c0 '{c}' # Start from 0, increment by 1 42 | rew -c2:3 '{c}' # Start from 2, increment by 3 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/filters/path.md: -------------------------------------------------------------------------------- 1 | # 🛤 Path filters 2 | 3 | Path filters assume that their input value is a FS path. 4 | 5 | ## Path components 6 | 7 | | Filter | Description | Filter | Description | 8 | | ------ | ---------------- | ------ | ------------------ | 9 | | `d` | Parent directory | `D` | Remove last name | 10 | | `f` | File name | `F` | Last name | 11 | | `b` | Base name | `B` | Remove extension | 12 | | `e` | Extension | `E` | Extension with dot | 13 | 14 | For input value `/home/alice/notes.txt`, filters would evaluate to: 15 | 16 | | Pattern | Output | 17 | | ------------ | ----------------------- | 18 | | `{}` | `/home/alice/notes.txt` | 19 | | `{d}`, `{D}` | `/home/alice` | 20 | | `{f}`, `{F}` | `notes.txt` | 21 | | `{b}` | `notes` | 22 | | `{B}` | `/home/alice/notes` | 23 | | `{e}` | `txt` | 24 | | `{E}` | `.txt` | 25 | 26 | Parent directory `d` might give a different result than `D` which removes last name of a path. 27 | Similarly, file name `f` might not be the same as last name `F` which is a complement of `D`. 28 | 29 | | Input | `{d}` | `{D}` | `{f}` | `{F}` | 30 | | --------- | ------- | --------- | ----------| ----------| 31 | | `/` | `/` | `/` | *(empty)* | *(empty)* | 32 | | `/a` | `/` | `/` | `a` | `a` | 33 | | `a/b` | `a` | `a` | `b` | `b` | 34 | | `a` | `.` | *(empty)* | `a` | `a` | 35 | | `.` | `./..` | *(empty)* | *(empty)* | `.` | 36 | | `..` | `../..` | *(empty)* | *(empty)* | `..` | 37 | | *(empty)* | `..` | *(empty)* | *(empty)* | *(empty)* | 38 | 39 | Extension with dot `E` can be useful when dealing with files with no extension. 40 | 41 | | Input | `new.{e}` | `new{E}` | 42 | | --------- | --------- | --------- | 43 | | `old.txt` | `new.txt` | `new.txt` | 44 | | `old` | `new.` | `new` | 45 | 46 | ## Absolute and relative paths 47 | 48 | | Filter | Description | 49 | | ------ | ----------------- | 50 | | `w` | Working directory | 51 | | `a` | Absolute path | 52 | | `A` | Relative path | 53 | 54 | Absolute path `a` and relative path `A` are both resolved against working directory `w`. 55 | 56 | | `{w}` | Input | `{a}` | `{A}` | 57 | | ------------- | ----------- | ----------- | -------- | 58 | | `/home/alice` | `/home/bob` | `/home/bob` | `../bob` | 59 | | `/home/alice` | `../bob` | `/home/bob` | `../bob` | 60 | 61 | By default, working directory `w` is set to your current working directory. 62 | You can change that using the `-w, --working-directory` option. 63 | `w` filter will always output an absolute path, even if you set a relative one using the `-w` option. 64 | 65 | ```bash 66 | rew -w '/home/alice' '{w}' # Absolute path 67 | rew -w '../alice' '{w}' # Relative to your current working directory 68 | ``` 69 | 70 | ## Path normalization 71 | 72 | | Filter | Description | 73 | | ------ | ---------------- | 74 | | `p` | Normalized path | 75 | | `P` | Canonical path | 76 | 77 | Normalized path `p` is constructed using the following rules: 78 | 79 | - On Windows, all `/` separators are converted to `\`. 80 | - Consecutive directory separators are collapsed into one. 81 | - Non-root trailing directory separator is removed. 82 | - Unnecessary current directory `.` components are removed. 83 | - Parent directory `..` components are resolved where possible. 84 | - Initial `..` components in an absolute path are dropped. 85 | - Initial `..` components in a relative path are kept. 86 | - Empty path is resolved to `.` (current directory). 87 | 88 | | Input | Output | Input | Output | 89 | | --------- |------- | --------- |------- | 90 | | *(empty)* | `.` | `/` | `/` | 91 | | `.` | `.` | `/.` | `/` | 92 | | `..` | `..` | `/..` | `/` | 93 | | `a/` | `a` | `/a/` | `/a` | 94 | | `a//` | `a` | `/a//` | `/a` | 95 | | `a/.` | `a` | `/a/.` | `/a` | 96 | | `a/..` | `.` | `/a/..` | `/` | 97 | | `./a` | `a` | `/./a` | `/a` | 98 | | `../a` | `../a` | `/../a` | `/a` | 99 | | `a//b` | `a/b` | `/a//b` | `/a/b` | 100 | | `a/./b` | `a/b` | `/a/./b` | `/a/b` | 101 | | `a/../b` | `b` | `/a/../b` | `/b` | 102 | 103 | Canonical path `P` works similarly to `p` but has some differences: 104 | 105 | - Evaluation will fail for a non-existent path. 106 | - Result will always be an absolute path. 107 | - If path is a symbolic link, it will be resolved. 108 | 109 | ## Directory separator 110 | 111 | | Filter | Description | 112 | | ------ | ----------------------------------- | 113 | | `z` | Ensure trailing directory separator | 114 | | `Z` | Remove trailing directory separator | 115 | 116 | Directory separator filters `z` and `Z` can be useful when dealing with root and unnormalized paths. 117 | 118 | | Input | `{}b` | `{}/b` | `{z}b` | `{Z}/b` | 119 | | ------ | ----- | -------| ------ | ------- | 120 | | `/` | `/b` | `//b` | `/b` | `/b` | 121 | | `a` | `ab` | `a/b` | `a/b` | `a/b` | 122 | | `a/` | `a/b` | `a//b` | `a/b` | `a/b` | 123 | -------------------------------------------------------------------------------- /docs/filters/regex.md: -------------------------------------------------------------------------------- 1 | # ⭐️ Regex filters 2 | 3 | ## Regex replace 4 | 5 | | Filter | Description | 6 | | ---------------- | --------------------------------------------- | 7 | | `s:X:Y` | Replace first match of a regular expression `X` with `Y`.
*`Y` can reference capture groups from `X` using `$0`, `$1`, `$2`, ...
Any other character than `:` can be also used as a delimiter.* | 8 | | `s:X` | Remove first match of a regular expression `X`.
*Equivalent to `s:X:`.* | 9 | | `S:X:Y`
`S:X` | Same as `s` but replaces/removes all matches. | 10 | 11 | Examples: 12 | 13 | | Input | Pattern | Output | 14 | | --------- | --------------------| ------- | 15 | | `12_34` | `{s:\d+:x}` | `x_34` | 16 | | `12_34` | `{S:\d+:x}` | `x_x` | 17 | | `12_34` | `{s:(\d)(\d):$2$1}` | `21_34` | 18 | | `12_34` | `{S:(\d)(\d):$2$1}` | `21_43` | 19 | 20 | ## Regex match 21 | 22 | | Filter | Description | 23 | | -------- | ----------------------------------------------------- | 24 | | `=N:E` | `N`-th match of a regular expression `E`.
*Indices `N` start from 1.
Use `-N` for backward indexing.
Any other character than `:` can be also used as a delimiter.* | 25 | | `=N-M:E` | `N`-th to `M`-th match of a regular expression `E`.
*This also includes any characters between matches.* | 26 | | `=N-:E` | `N`-th to the last match of a regular expression `E`. | 27 | 28 | Examples: 29 | 30 | | Input | Pattern | Output | Input | Pattern | Output | 31 | | ------------ | ------------ | ---------- | ------------ | ------------- | ---------- | 32 | | `_12_34_56_` | `{=1:\d+}` | `12` | `_12_34_56_` | `{=-1:\d+}` | `56` | 33 | | `_12_34_56_` | `{=2:\d+}` | `34` | `_12_34_56_` | `{=-2:\d+}` | `34` | 34 | | `_12_34_56_` | `{=1-2:\d+}` | `12_34` | `_12_34_56_` | `{=-1-2:\d+}` | `34_56` | 35 | | `_12_34_56_` | `{=1-:\d+}` | `12_34_56` | `_12_34_56_` | `{=-1-:\d+}` | `12_34_56` | 36 | | `_12_34_56_` | `{=2-:\d+}` | `34_56` | `_12_34_56_` | `{=-2-:\d+}` | `12_34` | 37 | 38 | ## Regex switch 39 | 40 | | Filter | Description | 41 | | --------------------- | ----------------------------------------------------------------- | 42 | | `@:X:Y` | Output `Y` if input matches regular expression `X`
Output nothing when there is no match..
*`Y` can reference capture groups from `X` using `$0`, `$1`, `$2`, ...
Any other character than `:` can be also used as a delimiter.* | 43 | | `@:X:Y:D` | Output `Y` if input matches regular expression `X`.
Output `D` when there is no match. | 44 | | `@:X1:Y1:...:Xn:Yn:D` | Output `Yi` for first regular expression `Xi` that matches input.
Output `D` when there is no match. | 45 | | `@:D` | A switch without any `Xi`/`Yi` cases which will always output `D`. | 46 | 47 | Examples: 48 | 49 | | Input | Pattern | Output | 50 | | ------- | --------------------------------------------| ------------------ | 51 | | *(any)* | `{@:def}` | `def` | 52 | | `12` | `{@:^\d+$:number}` | `number` | 53 | | `1x` | `{@:^\d+$:number}` | *(empty)* | 54 | | `12` | `{@:^\d+$:number:string}` | `number` | 55 | | `1x` | `{@:^\d+$:number:string}` | `string` | 56 | | `ab` | `{@:^[a-z]+$:lower:^[A-Z]+$:upper:mixed}` | `lower` | 57 | | `AB` | `{@:^[a-z]+$:lower:^[A-Z]+$:upper:mixed}` | `upper` | 58 | | `Ab` | `{@:^[a-z]+$:lower:^[A-Z]+$:upper:mixed}` | `mixed` | 59 | | `a=b` | `{@/(.+)=(.*)/key: $1, value: $2/invalid}` | `key: a, value: b` | 60 | | `ab` | `{@/(.+)=(.*)/key: $1, value: $2/invalid}` | `invalid` | 61 | 62 | ## Global regex 63 | 64 | | Filter | Description | 65 | | --------------------- | --------------------------------------------- | 66 | | `$0`, `$1`, `$2`, ... | Capture group of a global regular expression. | 67 | 68 | - Use `-e, --regex` or `-E, --regex-filename` option to define a global regular expression. 69 | - Option `-e, --regex` matches regex against each input value. 70 | - Option `-E, --regex-filename` matches regex against *filename component* of each input value. 71 | 72 | ```bash 73 | echo 'a/b.c' | rew -e '([a-z])' '{$1}' # Will print 'a' 74 | echo 'a/b.c' | rew -E '([a-z])' '{$1}' # Will print 'b' 75 | ``` 76 | -------------------------------------------------------------------------------- /docs/filters/string.md: -------------------------------------------------------------------------------- 1 | # 🆎 String filters 2 | 3 | ## Substring 4 | 5 | | Filter | Description | 6 | | ------- | ------------------------------------------------- | 7 | | `#A-B` | Substring from index `A` to `B`.
*Indices `A`, `B` start from 1 and are both inclusive.
Use `-A` for backward indexing.* | 8 | | `#A+L` | Substring from index `A` of length `L`. | 9 | | `#A-` | Substring from index `A` to end. | 10 | | `#A` | Character at index `A`.
*Equivalent to `#A-A`.* | 11 | 12 | Examples: 13 | 14 | | Input | Pattern | Output | Input | Pattern | Output | 15 | | ------- | -------- | ------ | ------- | --------- | ------ | 16 | | `abcde` | `{#2-3}` | `bc` | `abcde` | `{#-2-3}` | `cd` | 17 | | `abcde` | `{#2+3}` | `bcd` | `abcde` | `{#-2+3}` | `bcd` | 18 | | `abcde` | `{#2-}` | `bcde` | `abcde` | `{#-2-}` | `abcd` | 19 | | `abcde` | `{#2}` | `b` | `abcde` | `{#-2}` | `d` | 20 | 21 | ## String replace 22 | 23 | > ℹ️ See [regex filters](regex.md) for replacement using a regular expression. 24 | 25 | | Filter | Description | 26 | | ---------------- | ------------------------------------------------------- | 27 | | `r:X:Y` | Replace first occurrence of `X` with `Y`.
*Any other character than `:` can be also used as a delimiter.* | 28 | | `r:X` | Remove first occurrence of `X`.
*Equivalent to `r:X:`.* | 29 | | `R:X:Y`
`R:X` | Same as `r` but replaces/removes all occurrences. | 30 | | `?D` | Replace empty value with `D`. | 31 | 32 | Examples: 33 | 34 | | Input | Pattern | Output | 35 | | --------- | ----------- | ------- | 36 | | `ab_ab` | `{r:ab:xy}` | `xy_ab` | 37 | | `ab_ab` | `{R:ab:xy}` | `xy_xy` | 38 | | `ab_ab` | `{r:ab}` | `_ab` | 39 | | `ab_ab` | `{R:ab}` | `_` | 40 | | `abc` | `{?def}` | `abc` | 41 | | *(empty)* | `{?def}` | `def` | 42 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # 💡 Overview 2 | 3 | `rew` is a text processing CLI tool that rewrites FS paths according to a pattern. 4 | 5 | ## How rew works 6 | 7 | 1. Reads values from standard [input](input.md). 8 | 2. Rewrites them according to a [pattern](pattern.md). 9 | 3. Prints results to standard [output](output.md). 10 | 11 | ![How rew works](images/diagram.svg) 12 | 13 | Input values are assumed to be FS paths, however, `rew` is able to process any UTF-8 encoded text. 14 | 15 | ```bash 16 | find -iname '*.jpeg' | rew 'img_{C}.{e|l|r:e}' 17 | ``` 18 | 19 | `rew` is also distributed with two accompanying utilities (`mvb` and `cpb`) which move/copy files and directories, based on `rew` output. 20 | 21 | ```bash 22 | find -iname '*.jpeg' | rew 'img_{C}.{e|l|r:e}' -d | mvb 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/input.md: -------------------------------------------------------------------------------- 1 | # ⌨️ Input 2 | 3 | By default, input values are read as lines from standard input. 4 | Each line is expected to be terminated either by `LF` or `CR+LF` characters. 5 | The last line (before `EOF`) does not need to have a terminator. 6 | 7 | - Use `-t, --read` option to read values terminated by a specific character. 8 | - Use `-z, --read-nul` flag to read values terminated by `NUL` character. 9 | - Use `-r, --read-raw` flag to read whole input into memory as a single value. 10 | - Use `-l, --read-end` flag to read the last value (before `EOF`) only if it is properly terminated. 11 | 12 | The following table shows how an input would be parsed for valid combinations of flags/options: 13 | 14 | | Input | *(no flag)* | `-l` | `-z` | `-lz` | `-t:` | `-lt:` | `-r` | 15 | | -------- | ----------- | -------- | -------- | -------- | -------- | -------- | ------- | 16 | | `a\nb` | `a`, `b` | `a` | `a\nb` | *(none)* | `a\nb` | *(none)* |`a\nb` | 17 | | `a\nb\n` | `a`, `b` | `a`, `b` | `a\nb\n` | *(none)* | `a\nb\n` | *(none)* |`a\nb\n` | 18 | | `a\0b` | `a\0b` | *(none)* | `a`, `b` | `a` | `a\0b` | *(none)* |`a\0b` | 19 | | `a\0b\0` | `a\0b\0` | *(none)* | `a`, `b` | `a`, `b` | `a\0b\0` | *(none)* |`a\0b\0` | 20 | | `a:b` | `a:b` | *(none)* | `a:b` | *(none)* | `a`, `b` | `a` |`a:b` | 21 | | `a:b:` | `a:b:` | *(none)* | `a:b:` | *(none)* | `a`, `b` | `a`, `b` |`a:b:` | 22 | 23 | Input values can be also passed as additional arguments. 24 | In such case, standard input will not be read. 25 | 26 | ```bash 27 | rew '{}' image.jpg *.txt # Wildcard expansion is done by shell 28 | ``` 29 | 30 | Use flag `-I, --no-stdin` to enforce this behaviour even if there are no additional arguments. 31 | 32 | ```bash 33 | echo a | rew '{}' # Will print "a" 34 | echo a | rew '{}' b # Will print "b" 35 | echo a | rew -I '{}' # Will print nothing 36 | echo a | rew -I '{}' b # Will print "b" 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # 📦 Installation 2 | 3 | The latest release is available for [download on GitHub](https://github.com/jpikl/rew/releases/latest). 4 | 5 | Alternatively, you can build `rew` from sources: 6 | 7 | - Set up a [Rust development environment](https://www.rust-lang.org/learn/get-started). 8 | - Install the latest release using `cargo`. 9 | 10 | ```bash 11 | cargo install rew 12 | ``` 13 | 14 | - Binaries will be installed to `.cargo/bin/` in your home directory. 15 | -------------------------------------------------------------------------------- /docs/output.md: -------------------------------------------------------------------------------- 1 | # 💬 Output 2 | 3 | By default, results are printed as lines to standard output. 4 | `LF` character is used as a line terminator. 5 | 6 | - Use `-T, --print` option to print results terminated by a specific string. 7 | - Use `-Z, --print-nul` flag to print results terminated by `NUL` character. 8 | - Use `-R, --print-raw` flag to print results without a terminator. 9 | - Use `-L, --no-print-end` flag to disable printing terminator for the last result. 10 | 11 | The following table shows how values would be printed for valid combinations of flags/options: 12 | 13 | | Values | Flags | Output | 14 | | ------------- | -------- | ----------- | 15 | | `a`, `b`, `c` | *(none)* | `a\nb\nc\n` | 16 | | `a`, `b`, `c` | `-L` | `a\nb\nc` | 17 | | `a`, `b`, `c` | `-Z` | `a\0b\0c\0` | 18 | | `a`, `b`, `c` | `-LZ` | `a\0b\0c` | 19 | | `a`, `b`, `c` | `-T:` | `a:b:c:` | 20 | | `a`, `b`, `c` | `-LT:` | `a:b:c` | 21 | | `a`, `b`, `c` | `-R` | `abc` | 22 | 23 | Apart from this (standard) mode, there are also two other output modes. 24 | 25 | ## 🤖 Diff mode 26 | 27 | - Enabled using `-d, --diff` flag. 28 | - Respects `--print*` flags/options. 29 | - Ignores `--no-print-end` flag. 30 | - Prints transformations in machine-readable format: 31 | 32 | ```text 33 | output_value_1 35 | output_value_2 37 | ... 38 | output_value_N 40 | ``` 41 | 42 | Such output can be processed by accompanying `mvb` and `cpb` utilities to perform bulk move/copy. 43 | 44 | ```bash 45 | find -name '*.jpeg' | rew -d '{B}.jpg' | mvb # Rename all *.jpeg files to *.jpg 46 | find -name '*.txt' | rew -d '{}.bak' | cpb # Make backup copy of each *.txt file 47 | ``` 48 | 49 | ## 🌹 Pretty mode 50 | 51 | - Enabled using `-p, --pretty` flag. 52 | - Ignores `--print*` flags/options. 53 | - Ignores `--no-print-end` flag. 54 | - Prints transformations in human-readable format: 55 | 56 | ```text 57 | input_value_1 -> output_value_1 58 | input_value_2 -> output_value_2 59 | ... 60 | input_value_N -> output_value_N 61 | ``` 62 | 63 | ## 💼 JSON lines mode 64 | 65 | - Enabled using `-j, --json-lines` flag. 66 | - Ignores `--print*` flags/options. 67 | - Ignores `--no-print-end` flag. 68 | - Prints transformations as JSON lines: 69 | 70 | ```jsonl 71 | {"in":"input_value_1","out":"output_value_1"} 72 | {"in":"input_value_2","out":"output_value_2"} 73 | ... 74 | {"in":"input_value_N","out":"output_value_N"} 75 | ``` 76 | -------------------------------------------------------------------------------- /docs/pattern.md: -------------------------------------------------------------------------------- 1 | # ✏️ Pattern 2 | 3 | Pattern is a string describing how to generate output from an input. 4 | 5 | Use `--explain` flag to print detailed explanation what a certain pattern does. 6 | 7 | ```bash 8 | rew --explain 'file_{c|<3:0}.{e}' 9 | ``` 10 | 11 | ## Syntax 12 | 13 | By default, pattern characters are directly copied to output. 14 | 15 | | Input | Pattern | Output | 16 | | ------- | ------- | ------ | 17 | | *(any)* | `abc` | `abc` | 18 | 19 | Characters `{` and `}` form an expression which is evaluated and replaced in output. 20 | Empty expression `{}` evaluates directly to input value. 21 | 22 | | Input | Pattern | Output | 23 | | ------- | ------------ | --------------- | 24 | | `world` | `{}` | `world` | 25 | | `world` | `Hello, {}!` | `Hello, world!` | 26 | 27 | Expression may contain one or more [filters](filters/path.md), separated by `|`. 28 | Filters are consecutively applied on input value. 29 | 30 | | Input | Pattern | Output | Description | 31 | | ---------- | --------------- | ---------- | ------------------------------------- | 32 | | `old.JPEG` | `new.{e}` | `new.JPEG` | Extension | 33 | | `old.JPEG` | `new.{e|l}` | `new.jpeg` | Extension, Lowercase | 34 | | `old.JPEG` | `new.{e|l|r:e}` | `new.jpg` | Extension, Lowercase, Remove `e` | 35 | 36 | Use `-q, --quote` flag to automatically wrap output of every expression in quotes. 37 | 38 | ```bash 39 | echo abc | rew {} # Will print abc 40 | echo abc | rew {} -q # Will print 'abc' 41 | echo abc | rew {} -qq # Will print "abc" 42 | ``` 43 | 44 | ## Escaping 45 | 46 | Character `%` starts an escape sequence. 47 | 48 | | Sequence | Description | 49 | | -------- |--------------------------- | 50 | | `%/` | System directory separator
*`\` on Windows
`/` everywhere else* | 51 | | `%n` | Line feed | 52 | | `%r` | Carriage return | 53 | | `%t` | Horizontal tab | 54 | | `%0` | Null | 55 | | `%{` | Escaped `{` | 56 | | `%|` | Escaped `|` | 57 | | `%}` | Escaped `{` | 58 | | `%%` | Escaped `%` | 59 | 60 | Use `--escape` option to set a different escape character. 61 | 62 | ```bash 63 | rew '{R:%t: }' # Replace tabs with spaces 64 | rew '{R:\t: }' --escape='\' # The same thing, different escape character 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # 🚀 Usage 2 | 3 | ```bash 4 | rew [options] [--] [pattern] [values]... 5 | ``` 6 | 7 | When no values are provided, they are read from standard input instead. 8 | 9 | ```bash 10 | input | rew [options] [--] [pattern] 11 | ``` 12 | 13 | When no pattern is provided, values are directly copied to standard output. 14 | 15 | ```bash 16 | input | rew [options] 17 | ``` 18 | 19 | Use `-d, --diff` flag when piping output to `mvb` / `cpb` utilities to perform bulk move/copy. 20 | 21 | ```bash 22 | rew [options] [--] [pattern] -d | mvb 23 | ``` 24 | 25 | Use `-h` flag to print short help, `--help` to print detailed help. 26 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: rew 2 | site_url: https://jpikl.github.io/rew 3 | site_description: A text processing CLI tool that rewrites FS paths according to a pattern. 4 | site_author: Jan Pikl 5 | copyright: Copyright © 2021 Jan Pikl 6 | repo_url: https://github.com/jpikl/rew 7 | theme: 8 | name: material 9 | icon: 10 | logo: octicons/terminal-16 11 | favicon: null 12 | palette: 13 | - scheme: default 14 | primary: indigo 15 | accent: indigo 16 | toggle: 17 | icon: material/weather-sunny 18 | name: Switch to dark mode 19 | - scheme: slate 20 | primary: red 21 | accent: red 22 | toggle: 23 | icon: material/weather-night 24 | name: Switch to light mode 25 | features: 26 | - navigation.top 27 | - navigation.instant 28 | extra: 29 | generator: false 30 | extra_css: 31 | - extra.css 32 | markdown_extensions: 33 | - meta 34 | - toc: 35 | permalink: true 36 | - pymdownx.highlight 37 | - pymdownx.superfences 38 | - pymdownx.emoji: 39 | emoji_index: !!python/name:materialx.emoji.twemoji 40 | emoji_generator: !!python/name:materialx.emoji.to_svg 41 | nav: 42 | - index.md 43 | - install.md 44 | - usage.md 45 | - pattern.md 46 | - 🕶 Filters: 47 | - filters/path.md 48 | - filters/string.md 49 | - filters/field.md 50 | - filters/regex.md 51 | - filters/format.md 52 | - filters/generate.md 53 | - input.md 54 | - output.md 55 | - examples.md 56 | - compare.md 57 | - CHANGELOG.md 58 | - 📃 License: LICENSE.md 59 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Module" 2 | group_imports = "StdExternalCrate" 3 | -------------------------------------------------------------------------------- /src/bin/cpb/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{crate_version, AppSettings, Parser}; 2 | use common::color::{parse_color, COLOR_CHOICES}; 3 | use common::help::highlight_static; 4 | use common::run::Options; 5 | use common::transfer::TransferOptions; 6 | use indoc::indoc; 7 | use termcolor::ColorChoice; 8 | 9 | #[derive(Debug, Parser)] 10 | #[clap( 11 | name = "cbp", 12 | version = crate_version!(), 13 | long_about = highlight_static(indoc!{" 14 | Bulk copy files and directories 15 | 16 | `cpb` reads instructions from standard input in the following format: 17 | 18 | dst_path_1 20 | dst_path_2 22 | ... 23 | dst_path_N 25 | 26 | Such input can be generated using accompanying `rew` utility and its `-d, --diff` flag: 27 | 28 | $> find -name '*.txt' | rew -d '{}.bak' | cpb # Make backup copy of each *.txt file 29 | 30 | Each pair of source and destination path must be either both files or both directories. Mixing these types will result in error. 31 | 32 | Source path must exist. Using non-existent source path will result in error. 33 | 34 | Destination path may exist. Existing destination file will be overwritten. Existing destination directory will have its contents merged with contents of source directory. 35 | 36 | Missing parent directories in destination path will be created as needed. 37 | 38 | Nothing will be done if source and destination paths point to the same file or directory. 39 | "}), 40 | after_help = highlight_static("Use `-h` for short descriptions and `--help` for more details."), 41 | setting(AppSettings::DeriveDisplayOrder), 42 | setting(AppSettings::DontCollapseArgsInUsage), 43 | )] 44 | /// Bulk copy files and directories 45 | pub struct Cli { 46 | /// Read instructions terminated by NUL character, not newline 47 | #[clap(short = 'z', long)] 48 | pub read_nul: bool, 49 | 50 | /// Continue processing after an error, fail at end 51 | #[clap(short = 'F', long)] 52 | pub fail_at_end: bool, 53 | 54 | /// Explain what is being done 55 | #[clap(short = 'v', long)] 56 | pub verbose: bool, 57 | 58 | /// When to use colors 59 | #[clap( 60 | long, 61 | value_name = "when", 62 | possible_values = COLOR_CHOICES, 63 | parse(try_from_str = parse_color), 64 | )] 65 | pub color: Option, 66 | 67 | /// Print help information 68 | #[clap(short = 'h', long)] 69 | pub help: bool, 70 | 71 | /// Print version information 72 | #[clap(long)] 73 | pub version: bool, 74 | } 75 | 76 | impl Options for Cli { 77 | fn color(&self) -> Option { 78 | self.color 79 | } 80 | } 81 | 82 | impl TransferOptions for Cli { 83 | fn read_nul(&self) -> bool { 84 | self.read_nul 85 | } 86 | 87 | fn verbose(&self) -> bool { 88 | self.verbose 89 | } 90 | 91 | fn fail_at_end(&self) -> bool { 92 | self.fail_at_end 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use test_case::test_case; 99 | 100 | use super::*; 101 | 102 | #[test_case(&[], None ; "default")] 103 | #[test_case(&["--color=always"], Some(ColorChoice::Always) ; "always")] 104 | fn color(args: &[&str], result: Option) { 105 | assert_eq!(run(args).color(), result); 106 | } 107 | 108 | #[test_case(&[], false ; "off")] 109 | #[test_case(&["--read-nul"], true ; "on")] 110 | fn read_nul(args: &[&str], result: bool) { 111 | assert_eq!(run(args).read_nul(), result); 112 | } 113 | 114 | #[test_case(&[], false ; "off")] 115 | #[test_case(&["--verbose"], true ; "on")] 116 | fn verbose(args: &[&str], result: bool) { 117 | assert_eq!(run(args).verbose(), result); 118 | } 119 | 120 | #[test_case(&[], false ; "off")] 121 | #[test_case(&["--fail-at-end"], true ; "on")] 122 | fn fail_at_end(args: &[&str], result: bool) { 123 | assert_eq!(run(args).fail_at_end(), result); 124 | } 125 | 126 | fn run(args: &[&str]) -> Cli { 127 | Cli::try_parse_from(&[&["cpb"], args].concat()).unwrap() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/bin/cpb/main.rs: -------------------------------------------------------------------------------- 1 | use cli::Cli; 2 | use common::run::{exec_run, Io, Result}; 3 | use common::transfer::{run_transfer, TransferMode}; 4 | 5 | mod cli; 6 | 7 | fn main() { 8 | exec_run(run); 9 | } 10 | 11 | fn run(cli: &Cli, io: &Io) -> Result { 12 | run_transfer(cli, io, TransferMode::Copy) 13 | } 14 | -------------------------------------------------------------------------------- /src/bin/mvb/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{crate_version, AppSettings, Parser}; 2 | use common::color::{parse_color, COLOR_CHOICES}; 3 | use common::help::highlight_static; 4 | use common::run::Options; 5 | use common::transfer::TransferOptions; 6 | use indoc::indoc; 7 | use termcolor::ColorChoice; 8 | 9 | #[derive(Debug, Parser)] 10 | #[clap( 11 | name = "mvb", 12 | version = crate_version!(), 13 | long_about = highlight_static(indoc!{" 14 | Bulk move (rename) files and directories 15 | 16 | `mvb` reads instructions from standard input in the following format: 17 | 18 | dst_path_1 20 | dst_path_2 22 | ... 23 | dst_path_N 25 | 26 | Such input can be generated using accompanying `rew` utility and its `-d, --diff` flag: 27 | 28 | $> find -name '*.jpeg' | rew -d '{B}.jpg' | mvb # Rename all *.jpeg files to *.jpg 29 | 30 | Each pair of source and destination path must be either both files or both directories. Mixing these types will result in error. 31 | 32 | Source path must exist. Using non-existent source path will result in error. 33 | 34 | Destination path may exist. Existing destination file will be overwritten. Existing destination directory will have its contents merged with contents of source directory. 35 | 36 | Missing parent directories in destination path will be created as needed. 37 | 38 | Nothing will be done if source and destination paths point to the same file or directory. 39 | "}), 40 | after_help = highlight_static("Use `-h` for short descriptions and `--help` for more details."), 41 | setting(AppSettings::DeriveDisplayOrder), 42 | setting(AppSettings::DontCollapseArgsInUsage), 43 | )] 44 | /// Bulk move (rename) files and directories 45 | pub struct Cli { 46 | /// Read instructions terminated by NUL character, not newline 47 | #[clap(short = 'z', long)] 48 | pub read_nul: bool, 49 | 50 | /// Continue processing after an error, fail at end 51 | #[clap(short = 'F', long)] 52 | pub fail_at_end: bool, 53 | 54 | /// Explain what is being done 55 | #[clap(short = 'v', long)] 56 | pub verbose: bool, 57 | 58 | /// When to use colors 59 | #[clap( 60 | long, 61 | value_name = "when", 62 | possible_values = COLOR_CHOICES, 63 | parse(try_from_str = parse_color), 64 | )] 65 | pub color: Option, 66 | 67 | /// Print help information 68 | #[clap(short = 'h', long)] 69 | pub help: bool, 70 | 71 | /// Print version information 72 | #[clap(long)] 73 | pub version: bool, 74 | } 75 | 76 | impl Options for Cli { 77 | fn color(&self) -> Option { 78 | self.color 79 | } 80 | } 81 | 82 | impl TransferOptions for Cli { 83 | fn read_nul(&self) -> bool { 84 | self.read_nul 85 | } 86 | 87 | fn verbose(&self) -> bool { 88 | self.verbose 89 | } 90 | 91 | fn fail_at_end(&self) -> bool { 92 | self.fail_at_end 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use test_case::test_case; 99 | 100 | use super::*; 101 | 102 | #[test_case(&[], None ; "default")] 103 | #[test_case(&["--color=always"], Some(ColorChoice::Always) ; "always")] 104 | fn color(args: &[&str], result: Option) { 105 | assert_eq!(run(args).color(), result); 106 | } 107 | 108 | #[test_case(&[], false ; "off")] 109 | #[test_case(&["--read-nul"], true ; "on")] 110 | fn read_nul(args: &[&str], result: bool) { 111 | assert_eq!(run(args).read_nul(), result); 112 | } 113 | 114 | #[test_case(&[], false ; "off")] 115 | #[test_case(&["--verbose"], true ; "on")] 116 | fn verbose(args: &[&str], result: bool) { 117 | assert_eq!(run(args).verbose(), result); 118 | } 119 | 120 | #[test_case(&[], false ; "off")] 121 | #[test_case(&["--fail-at-end"], true ; "on")] 122 | fn fail_at_end(args: &[&str], result: bool) { 123 | assert_eq!(run(args).fail_at_end(), result); 124 | } 125 | 126 | fn run(args: &[&str]) -> Cli { 127 | Cli::try_parse_from(&[&["mvb"], args].concat()).unwrap() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/bin/mvb/main.rs: -------------------------------------------------------------------------------- 1 | use cli::Cli; 2 | use common::run::{exec_run, Io, Result}; 3 | use common::transfer::{run_transfer, TransferMode}; 4 | 5 | mod cli; 6 | 7 | fn main() { 8 | exec_run(run); 9 | } 10 | 11 | fn run(cli: &Cli, io: &Io) -> Result { 12 | run_transfer(cli, io, TransferMode::Move) 13 | } 14 | -------------------------------------------------------------------------------- /src/bin/rew/counter.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::str::FromStr; 3 | 4 | use num_traits::PrimInt; 5 | 6 | use crate::pattern::path; 7 | 8 | const INIT_ERROR: &str = "Invalid init value"; 9 | const STEP_ERROR: &str = "Invalid step value"; 10 | 11 | pub trait Value: PrimInt + FromStr {} 12 | 13 | impl Value for T {} 14 | 15 | #[derive(Debug, Copy, Clone, PartialEq)] 16 | pub struct Config { 17 | pub init: T, 18 | pub step: T, 19 | } 20 | 21 | impl Default for Config { 22 | fn default() -> Self { 23 | Self { 24 | init: T::one(), 25 | step: T::one(), 26 | } 27 | } 28 | } 29 | 30 | impl FromStr for Config { 31 | type Err = &'static str; 32 | 33 | fn from_str(string: &str) -> Result { 34 | if let Some(delimiter_index) = string.find(':') { 35 | let init_str = &string[0..delimiter_index]; 36 | let init = init_str.parse().map_err(|_| INIT_ERROR)?; 37 | 38 | let step = if delimiter_index < string.len() - 1 { 39 | let step_str = &string[(delimiter_index + 1)..]; 40 | step_str.parse().map_err(|_| STEP_ERROR)? 41 | } else { 42 | return Err(STEP_ERROR); 43 | }; 44 | 45 | Ok(Self { init, step }) 46 | } else { 47 | let init = string.parse().map_err(|_| INIT_ERROR)?; 48 | let step = Config::default().step; 49 | 50 | Ok(Self { init, step }) 51 | } 52 | } 53 | } 54 | 55 | #[derive(PartialEq, Debug)] 56 | pub struct GlobalGenerator { 57 | value: T, 58 | step: T, 59 | } 60 | 61 | impl GlobalGenerator { 62 | pub fn new(init: T, step: T) -> Self { 63 | Self { value: init, step } 64 | } 65 | 66 | pub fn next(&mut self) -> T { 67 | let value = self.value; 68 | self.value = self.value.add(self.step); 69 | value 70 | } 71 | } 72 | 73 | impl From<&Config> for GlobalGenerator { 74 | fn from(config: &Config) -> Self { 75 | Self::new(config.init, config.step) 76 | } 77 | } 78 | 79 | #[derive(PartialEq, Debug)] 80 | pub struct LocalGenerator { 81 | values: HashMap, 82 | init: T, 83 | step: T, 84 | } 85 | 86 | impl From<&Config> for LocalGenerator { 87 | fn from(config: &Config) -> Self { 88 | Self::new(config.init, config.step) 89 | } 90 | } 91 | 92 | impl LocalGenerator { 93 | pub fn new(init: T, step: T) -> Self { 94 | Self { 95 | values: HashMap::new(), 96 | init, 97 | step, 98 | } 99 | } 100 | 101 | pub fn next(&mut self, value: &str) -> T { 102 | let key = match path::get_parent_directory(value.to_string()) { 103 | Ok(parent) => path::normalize(&parent).unwrap_or_default(), 104 | Err(_) => String::new(), 105 | }; 106 | if let Some(value) = self.values.get_mut(&key) { 107 | *value = value.add(self.step); 108 | *value 109 | } else { 110 | self.values.insert(key, self.init); 111 | self.init 112 | } 113 | } 114 | } 115 | 116 | #[cfg(test)] 117 | mod tests { 118 | use super::*; 119 | type Value = u32; 120 | 121 | mod config { 122 | use super::*; 123 | 124 | #[test] 125 | fn default() { 126 | let config = Config::::default(); 127 | assert_eq!(config.init, 1); 128 | assert_eq!(config.step, 1); 129 | } 130 | 131 | mod from_str { 132 | use test_case::test_case; 133 | 134 | use super::*; 135 | 136 | #[test_case("", INIT_ERROR ; "empty")] 137 | #[test_case(":", INIT_ERROR ; "separator")] 138 | #[test_case(":34", INIT_ERROR ; "separator number")] 139 | #[test_case(":cd", INIT_ERROR ; "separator string")] 140 | #[test_case("12:", STEP_ERROR ; "number separator")] 141 | #[test_case("12:cd", STEP_ERROR ; "number separator string")] 142 | #[test_case("ab", INIT_ERROR ; "string")] 143 | #[test_case("ab:", INIT_ERROR ; "string separator")] 144 | #[test_case("ab:34", INIT_ERROR ; "string separator number")] 145 | #[test_case("ab:cd", INIT_ERROR ; "string separator string")] 146 | fn err(input: &str, error: &str) { 147 | assert_eq!(Config::::from_str(input), Err(error)); 148 | } 149 | 150 | #[test_case("12", 12, 1 ; "init")] 151 | #[test_case("12:34", 12, 34 ; "init and step")] 152 | fn ok(input: &str, init: Value, step: Value) { 153 | assert_eq!(Config::from_str(input), Ok(Config { init, step })); 154 | } 155 | } 156 | } 157 | 158 | mod global_generator { 159 | use test_case::test_case; 160 | 161 | use super::*; 162 | 163 | #[test] 164 | fn from_config() { 165 | assert_eq!( 166 | GlobalGenerator::new(12, 34), 167 | GlobalGenerator::from(&Config { init: 12, step: 34 }) 168 | ); 169 | } 170 | 171 | #[test_case(0, 1, 0, 0 ; "0:1 iteration 1")] 172 | #[test_case(0, 1, 1, 1 ; "0:1 iteration 2")] 173 | #[test_case(0, 1, 2, 2 ; "0:1 iteration 3")] 174 | #[test_case(1, 10, 0, 1 ; "1:10 iteration 1")] 175 | #[test_case(1, 10, 1, 11 ; "1:10 iteration 2")] 176 | #[test_case(1, 10, 2, 21 ; "1:10 iteration 3")] 177 | fn next(init: Value, step: Value, index: usize, result: Value) { 178 | let mut counter = GlobalGenerator::new(init, step); 179 | for _ in 0..index { 180 | counter.next(); 181 | } 182 | assert_eq!(counter.next(), result); 183 | } 184 | } 185 | 186 | mod local_generator { 187 | use test_case::test_case; 188 | 189 | use super::*; 190 | 191 | const A: &str = "a"; 192 | const AX: &str = "a/b/.."; 193 | const B: &str = "a/b"; 194 | const C: &str = "a/b/c"; 195 | const CX: &str = "./a/b/c"; 196 | 197 | #[test] 198 | fn from_config() { 199 | assert_eq!( 200 | LocalGenerator::new(12, 34), 201 | LocalGenerator::from(&Config { init: 12, step: 34 }) 202 | ); 203 | } 204 | 205 | #[test_case(0, 1, &[], C, 0 ; "0:1 iteration 1")] 206 | #[test_case(0, 1, &[C], C, 1 ; "0:1 iteration 2")] 207 | #[test_case(0, 1, &[C, C], C, 2 ; "0:1 iteration 3")] 208 | #[test_case(0, 1, &[C, C, C], B, 0 ; "0:1 iteration 4")] 209 | #[test_case(0, 1, &[C, C, C, B], A, 0 ; "0:1 iteration 5")] 210 | #[test_case(0, 1, &[C, C, C, B, A], B, 1 ; "0:1 iteration 6")] 211 | #[test_case(0, 1, &[C, C, C, B, A, B], B, 2 ; "0:1 iteration 7")] 212 | #[test_case(0, 1, &[C, C, C, B, A, B, B], A, 1 ; "0:1 iteration 8")] 213 | #[test_case(0, 1, &[C, C, C, B, A, B, B, A], A, 2 ; "0:1 iteration 9")] 214 | #[test_case(1, 10, &[], C, 1 ; "1:10 iteration 1")] 215 | #[test_case(1, 10, &[C], C, 11 ; "1:10 iteration 2")] 216 | #[test_case(1, 10, &[C, C], C, 21 ; "1:10 iteration 3")] 217 | #[test_case(1, 10, &[C, C, C], B, 1 ; "1:10 iteration 4")] 218 | #[test_case(1, 10, &[C, C, C, B], A, 1 ; "1:10 iteration 5")] 219 | #[test_case(1, 10, &[C, C, C, B, A], B, 11 ; "1:10 iteration 6")] 220 | #[test_case(1, 10, &[C, C, C, B, A, B], B, 21 ; "1:10 iteration 7")] 221 | #[test_case(1, 10, &[C, C, C, B, A, B, B], A, 11 ; "1:10 iteration 8")] 222 | #[test_case(1, 10, &[C, C, C, B, A, B, B, A], A, 21 ; "1:10 iteration 9")] 223 | #[test_case(0, 1, &[], C, 0 ; "normalize dirs 1")] 224 | #[test_case(0, 1, &[C], CX, 1 ; "normalize dirs 2")] 225 | #[test_case(0, 1, &[C, CX], A, 0 ; "normalize dirs 3")] 226 | #[test_case(0, 1, &[C, CX, A], AX, 1 ; "normalize dirs 4")] 227 | fn next(init: Value, step: Value, prev_paths: &[&str], next_path: &str, result: Value) { 228 | let mut counter = LocalGenerator::new(init, step); 229 | for prev_path in prev_paths { 230 | counter.next(prev_path); 231 | } 232 | assert_eq!(counter.next(next_path), result); 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/bin/rew/input.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufRead, Result}; 2 | use std::slice::Iter; 3 | 4 | use common::input::{Splitter, Terminator}; 5 | 6 | pub enum Values<'a, A: AsRef, I: BufRead> { 7 | Args { iter: Iter<'a, A> }, 8 | Stdin { splitter: Splitter }, 9 | } 10 | 11 | impl<'a, A: AsRef, I: BufRead> Values<'a, A, I> { 12 | pub fn from_args(values: &'a [A]) -> Self { 13 | Values::Args { 14 | iter: values.iter(), 15 | } 16 | } 17 | 18 | pub fn from_stdin(stdin: I, terminator: Terminator) -> Self { 19 | Values::Stdin { 20 | splitter: Splitter::new(stdin, terminator), 21 | } 22 | } 23 | 24 | pub fn next(&mut self) -> Result> { 25 | match self { 26 | Self::Args { iter } => Ok(iter.next().map(A::as_ref)), 27 | Self::Stdin { splitter: reader } => Ok(reader.read()?.map(|(value, _)| value)), 28 | } 29 | } 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use common::testing::unpack_io_error; 35 | use test_case::test_case; 36 | 37 | use super::*; 38 | 39 | #[test_case(args(), 0, Some("a") ; "args 0")] 40 | #[test_case(args(), 1, Some("b") ; "args 1")] 41 | #[test_case(args(), 2, None ; "args 2")] 42 | #[test_case(stdin(), 0, Some("a") ; "stdin 0")] 43 | #[test_case(stdin(), 1, Some("b") ; "stdin 1")] 44 | #[test_case(stdin(), 2, None ; "stdin 2")] 45 | fn next(mut values: Values<&str, &[u8]>, position: usize, result: Option<&str>) { 46 | for _ in 0..position { 47 | values.next().unwrap_or_default(); 48 | } 49 | assert_eq!(values.next().map_err(unpack_io_error), Ok(result)); 50 | } 51 | 52 | fn args<'a>() -> Values<'a, &'a str, &'a [u8]> { 53 | Values::from_args(&["a", "b"][..]) 54 | } 55 | 56 | fn stdin<'a>() -> Values<'a, &'a str, &'a [u8]> { 57 | Values::from_stdin(&b"a\nb"[..], Terminator::Newline { required: false }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/bin/rew/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::io::Write; 3 | 4 | use ::regex::Regex; 5 | use common::help::highlight; 6 | use common::input::Terminator; 7 | use common::run::{exec_run, Io, Result, EXIT_CODE_OK}; 8 | 9 | use crate::cli::Cli; 10 | use crate::output::write_pattern_error; 11 | use crate::pattern::parse::Separator; 12 | use crate::pattern::regex::RegexHolder; 13 | use crate::pattern::{eval, help, parse, Pattern}; 14 | 15 | mod cli; 16 | mod counter; 17 | mod input; 18 | mod output; 19 | mod pattern; 20 | mod regex; 21 | 22 | const EXIT_CODE_PARSE_ERROR: i32 = 3; 23 | const EXIT_CODE_EVAL_ERROR: i32 = 4; 24 | 25 | fn main() { 26 | exec_run(run); 27 | } 28 | 29 | fn run(cli: &Cli, io: &Io) -> Result { 30 | if cli.help_pattern { 31 | highlight(&mut io.stdout(), help::PATTERN)?; 32 | return Ok(EXIT_CODE_OK); 33 | } 34 | 35 | if cli.help_filters { 36 | highlight(&mut io.stdout(), help::FILTERS)?; 37 | return Ok(EXIT_CODE_OK); 38 | } 39 | 40 | let mut input_values = if cli.values.is_empty() && !cli.no_stdin { 41 | let required = cli.read_end; 42 | let terminator = if let Some(value) = cli.read { 43 | Terminator::Byte { value, required } 44 | } else if cli.read_nul { 45 | Terminator::Byte { value: 0, required } 46 | } else if cli.read_raw { 47 | Terminator::None 48 | } else { 49 | Terminator::Newline { required } 50 | }; 51 | input::Values::from_stdin(io.stdin(), terminator) 52 | } else { 53 | input::Values::from_args(cli.values.as_slice()) 54 | }; 55 | 56 | let output_mode = if cli.pretty { 57 | output::Mode::Pretty 58 | } else if cli.diff { 59 | output::Mode::Diff 60 | } else if cli.json_lines { 61 | output::Mode::JsonLines 62 | } else if cli.no_print_end { 63 | output::Mode::StandardNoEnd 64 | } else { 65 | output::Mode::Standard 66 | }; 67 | 68 | let output_terminator = if let Some(terminator) = &cli.print { 69 | terminator 70 | } else if cli.print_raw { 71 | "" 72 | } else if cli.print_nul { 73 | "\0" 74 | } else { 75 | "\n" 76 | }; 77 | 78 | let mut output_values = output::Values::new(io.stdout(), output_mode, output_terminator); 79 | let mut exit_code = EXIT_CODE_OK; 80 | 81 | if let Some(raw_pattern) = cli.pattern.as_ref() { 82 | let separator = if let Some(separator) = &cli.separator { 83 | Separator::String(separator.clone()) 84 | } else if let Some(separator) = &cli.separator_regex { 85 | Separator::Regex(RegexHolder(separator.clone())) 86 | } else { 87 | Separator::Regex(RegexHolder( 88 | Regex::new("\\s+").expect("Failed to create default separator from regex"), 89 | )) 90 | }; 91 | 92 | let parse_config = parse::Config { 93 | escape: cli.escape.unwrap_or('%'), 94 | separator, 95 | }; 96 | 97 | let pattern = match Pattern::parse(raw_pattern, &parse_config) { 98 | Ok(pattern) => pattern, 99 | Err(error) => { 100 | let mut stderr = io.stderr(); 101 | write_pattern_error(&mut stderr, &error, raw_pattern)?; 102 | 103 | if let Some(hint) = error.kind.hint() { 104 | writeln!(stderr)?; 105 | let message = match hint { 106 | parse::ErrorHint::RegexSyntax => help::REGEX_HINT, 107 | parse::ErrorHint::PatternSyntax => help::PATTERN_HINT, 108 | parse::ErrorHint::FilterUsage => help::FILTERS_HINT, 109 | }; 110 | highlight(&mut stderr, message)?; 111 | } 112 | 113 | return Ok(EXIT_CODE_PARSE_ERROR); 114 | } 115 | }; 116 | 117 | if cli.explain || cli.explain_filters { 118 | pattern.explain(&mut io.stdout(), cli.explain)?; 119 | return Ok(EXIT_CODE_OK); 120 | } 121 | 122 | let global_counter_used = pattern.uses_global_counter(); 123 | let local_counter_used = pattern.uses_local_counter(); 124 | let regex_capture_used = pattern.uses_regex_capture(); 125 | 126 | let global_counter_config = cli.global_counter.unwrap_or_default(); 127 | let local_counter_config = cli.local_counter.unwrap_or_default(); 128 | 129 | let mut global_counter_generator = counter::GlobalGenerator::from(&global_counter_config); 130 | let mut local_counter_generator = counter::LocalGenerator::from(&local_counter_config); 131 | 132 | let regex_solver = if let Some(regex) = &cli.regex { 133 | regex::Solver::Value(regex) 134 | } else if let Some(regex) = &cli.regex_filename { 135 | regex::Solver::FileName(regex) 136 | } else { 137 | regex::Solver::None 138 | }; 139 | 140 | let working_dir = if let Some(working_dir) = &cli.working_directory { 141 | if working_dir.is_relative() { 142 | env::current_dir()?.join(working_dir) 143 | } else { 144 | working_dir.clone() 145 | } 146 | } else { 147 | env::current_dir()? 148 | }; 149 | 150 | let expression_quotes = match cli.quote { 151 | 0 => None, 152 | 1 => Some('\''), 153 | _ => Some('"'), 154 | }; 155 | 156 | while let Some(input_value) = input_values.next()? { 157 | let global_counter = if global_counter_used { 158 | global_counter_generator.next() 159 | } else { 160 | 0 161 | }; 162 | 163 | let local_counter = if local_counter_used { 164 | local_counter_generator.next(input_value) 165 | } else { 166 | 0 167 | }; 168 | 169 | let regex_captures = if regex_capture_used { 170 | regex_solver.eval(input_value) 171 | } else { 172 | None 173 | }; 174 | 175 | let context = eval::Context { 176 | working_dir: &working_dir, 177 | global_counter, 178 | local_counter, 179 | regex_captures, 180 | expression_quotes, 181 | }; 182 | 183 | let output_value = match pattern.eval(input_value, &context) { 184 | Ok(value) => value, 185 | Err(error) => { 186 | write_pattern_error(&mut io.stderr(), &error, raw_pattern)?; 187 | if cli.fail_at_end { 188 | exit_code = EXIT_CODE_EVAL_ERROR; 189 | continue; 190 | } else { 191 | return Ok(EXIT_CODE_EVAL_ERROR); 192 | } 193 | } 194 | }; 195 | 196 | output_values.write(input_value, &output_value)?; 197 | } 198 | } else { 199 | while let Some(value) = input_values.next()? { 200 | output_values.write(value, value)?; 201 | } 202 | }; 203 | 204 | io.stdout().flush()?; // output::Values may not do flush if there is no last terminator. 205 | Ok(exit_code) 206 | } 207 | -------------------------------------------------------------------------------- /src/bin/rew/output.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::io::{Result, Write}; 3 | use std::ops::Range; 4 | 5 | use common::color::{spec_bold_color, spec_color}; 6 | use common::output::write_error; 7 | use common::symbols::{DIFF_IN, DIFF_OUT}; 8 | use termcolor::{Color, WriteColor}; 9 | 10 | use crate::pattern::error::GetErrorRange; 11 | 12 | pub enum Mode { 13 | Standard, 14 | StandardNoEnd, 15 | Diff, 16 | Pretty, 17 | JsonLines, 18 | } 19 | 20 | pub struct Values { 21 | output: O, 22 | mode: Mode, 23 | terminator: String, 24 | first_result: bool, 25 | flush_needed: bool, 26 | } 27 | 28 | impl Values { 29 | pub fn new(output: O, mode: Mode, terminator: &str) -> Self { 30 | Self { 31 | output, 32 | mode, 33 | terminator: terminator.into(), 34 | first_result: true, 35 | flush_needed: !terminator.ends_with('\n'), 36 | } 37 | } 38 | 39 | pub fn write(&mut self, input_value: &str, output_value: &str) -> Result<()> { 40 | match self.mode { 41 | Mode::Standard => { 42 | write!(self.output, "{}{}", output_value, self.terminator)?; 43 | self.flush_if_needed() 44 | } 45 | Mode::StandardNoEnd => { 46 | if self.first_result { 47 | self.first_result = false; 48 | } else { 49 | write!(self.output, "{}", self.terminator)?; 50 | self.flush_if_needed()?; 51 | } 52 | write!(self.output, "{}", output_value) 53 | } 54 | Mode::Diff => { 55 | write!( 56 | self.output, 57 | "{}{}{}{}{}{}", 58 | DIFF_IN, input_value, self.terminator, DIFF_OUT, output_value, self.terminator 59 | )?; 60 | self.flush_if_needed() 61 | } 62 | Mode::Pretty => { 63 | self.output.set_color(&spec_color(Color::Blue))?; 64 | write!(self.output, "{}", input_value)?; 65 | self.output.reset()?; 66 | write!(self.output, " -> ")?; 67 | self.output.set_color(&spec_color(Color::Green))?; 68 | writeln!(self.output, "{}", output_value) 69 | } 70 | Mode::JsonLines => { 71 | writeln!( 72 | self.output, 73 | r#"{{"in":"{}","out":"{}"}}"#, 74 | input_value, output_value 75 | ) 76 | } 77 | } 78 | } 79 | 80 | fn flush_if_needed(&mut self) -> Result<()> { 81 | if self.flush_needed { 82 | self.output.flush() 83 | } else { 84 | Ok(()) 85 | } 86 | } 87 | } 88 | 89 | pub fn write_pattern_error( 90 | output: &mut O, 91 | error: &E, 92 | raw_pattern: &str, 93 | ) -> Result<()> { 94 | write_error(output, error)?; 95 | writeln!(output)?; 96 | highlight_range(output, raw_pattern, error.error_range(), Color::Red)?; 97 | output.reset() 98 | } 99 | 100 | pub fn highlight_range( 101 | output: &mut O, 102 | string: &str, 103 | range: &Range, 104 | color: Color, 105 | ) -> Result<()> { 106 | write!(output, "{}", &string[..range.start])?; 107 | output.set_color(&spec_bold_color(color))?; 108 | write!(output, "{}", &string[range.start..range.end])?; 109 | output.reset()?; 110 | writeln!(output, "{}", &string[range.end..])?; 111 | 112 | let spaces_count = string[..range.start].chars().count(); 113 | let markers_count = string[range.start..range.end].chars().count().max(1); 114 | 115 | write!(output, "{}", " ".repeat(spaces_count))?; 116 | output.set_color(&spec_bold_color(color))?; 117 | write!(output, "{}", "^".repeat(markers_count))?; 118 | output.reset()?; 119 | 120 | writeln!(output) 121 | } 122 | 123 | #[cfg(test)] 124 | mod tests { 125 | use common::testing::{ColoredOuput, OutputChunk}; 126 | use indoc::indoc; 127 | use test_case::test_case; 128 | 129 | use super::*; 130 | use crate::pattern::error::ErrorRange; 131 | 132 | #[test_case(Mode::Standard, "", plain("bd") ; "standard no terminator")] 133 | #[test_case(Mode::Standard, "\n", plain("b\nd\n") ; "standard newline terminator")] 134 | #[test_case(Mode::Standard, "\0", plain("b\0d\0") ; "standard null terminator")] 135 | #[test_case(Mode::StandardNoEnd, "", plain("bd") ; "standard no end no terminator")] 136 | #[test_case(Mode::StandardNoEnd, "\n", plain("b\nd") ; "standard no end newline terminator")] 137 | #[test_case(Mode::StandardNoEnd, "\0", plain("b\0d") ; "standard no end null terminator")] 138 | #[test_case(Mode::Diff, "", plain("bd") ; "diff no terminator")] 139 | #[test_case(Mode::Diff, "\n", plain("b\nd\n") ; "diff newline terminator")] 140 | #[test_case(Mode::Diff, "\0", plain("b\0d\0") ; "diff null terminator")] 141 | #[test_case(Mode::Pretty, "x", pretty() ; "pretty ")] 142 | #[test_case(Mode::JsonLines, "x", plain(indoc! {r#" 143 | {"in":"a","out":"b"} 144 | {"in":"c","out":"d"} 145 | "#}) ; "json lines")] 146 | fn values_write(mode: Mode, terminator: &str, chunks: Vec) { 147 | let mut output = ColoredOuput::new(); 148 | let mut values = Values::new(&mut output, mode, terminator); 149 | values.write("a", "b").unwrap(); 150 | values.write("c", "d").unwrap(); 151 | assert_eq!(output.chunks(), &chunks); 152 | } 153 | 154 | pub fn plain(value: &str) -> Vec { 155 | vec![OutputChunk::plain(value)] 156 | } 157 | 158 | fn pretty() -> Vec { 159 | vec![ 160 | OutputChunk::color(Color::Blue, "a"), 161 | OutputChunk::plain(" -> "), 162 | OutputChunk::color(Color::Green, "b\n"), 163 | OutputChunk::color(Color::Blue, "c"), 164 | OutputChunk::plain(" -> "), 165 | OutputChunk::color(Color::Green, "d\n"), 166 | ] 167 | } 168 | 169 | #[test] 170 | fn write_pattern_error() { 171 | use std::fmt; 172 | 173 | use super::*; 174 | 175 | #[derive(Debug)] 176 | struct CustomError {} 177 | impl Error for CustomError {} 178 | 179 | impl fmt::Display for CustomError { 180 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 181 | formatter.write_str("msg") 182 | } 183 | } 184 | 185 | impl GetErrorRange for CustomError { 186 | fn error_range(&self) -> &ErrorRange { 187 | &(1..3) 188 | } 189 | } 190 | 191 | let mut output = ColoredOuput::new(); 192 | super::write_pattern_error(&mut output, &CustomError {}, "abcd").unwrap(); 193 | 194 | assert_eq!( 195 | output.chunks(), 196 | &[ 197 | OutputChunk::color(Color::Red, "error:"), 198 | OutputChunk::plain(" msg\n\na"), 199 | OutputChunk::bold_color(Color::Red, "bc"), 200 | OutputChunk::plain("d\n "), 201 | OutputChunk::bold_color(Color::Red, "^^"), 202 | OutputChunk::plain("\n") 203 | ] 204 | ); 205 | } 206 | 207 | #[test] 208 | fn highlight_range() { 209 | let mut output = ColoredOuput::new(); 210 | super::highlight_range(&mut output, "abcde", &(1..4), Color::Green).unwrap(); 211 | 212 | assert_eq!( 213 | output.chunks(), 214 | &[ 215 | OutputChunk::plain("a"), 216 | OutputChunk::bold_color(Color::Green, "bcd"), 217 | OutputChunk::plain("e\n "), 218 | OutputChunk::bold_color(Color::Green, "^^^"), 219 | OutputChunk::plain("\n") 220 | ] 221 | ); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/char.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::Debug; 3 | use std::ops::Deref; 4 | 5 | use crate::pattern::escape::escape_char; 6 | 7 | pub type EscapeSequence = [char; 2]; 8 | 9 | #[derive(Debug, PartialEq, Clone)] 10 | pub enum Char { 11 | Raw(char), 12 | Escaped(char, EscapeSequence), 13 | } 14 | 15 | impl From for Char { 16 | fn from(value: char) -> Self { 17 | Self::Raw(value) 18 | } 19 | } 20 | 21 | impl From for Char { 22 | fn from(sequence: [char; 2]) -> Self { 23 | Self::Escaped(sequence[1], sequence) 24 | } 25 | } 26 | 27 | impl fmt::Display for Char { 28 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 29 | match self { 30 | Self::Raw(value) => write!(formatter, "'{}'", escape_char(*value)), 31 | Self::Escaped(value, sequence) => { 32 | write!( 33 | formatter, 34 | "'{}' (escape sequence '{}{}')", 35 | escape_char(*value), 36 | escape_char(sequence[0]), 37 | escape_char(sequence[1]) 38 | ) 39 | } 40 | } 41 | } 42 | } 43 | 44 | pub trait AsChar: From { 45 | fn as_char(&self) -> char; 46 | 47 | fn len_utf8(&self) -> usize; 48 | } 49 | 50 | impl AsChar for char { 51 | fn as_char(&self) -> char { 52 | *self 53 | } 54 | 55 | fn len_utf8(&self) -> usize { 56 | char::len_utf8(*self) 57 | } 58 | } 59 | 60 | impl AsChar for Char { 61 | fn as_char(&self) -> char { 62 | match self { 63 | Self::Raw(value) => *value, 64 | Self::Escaped(value, _) => *value, 65 | } 66 | } 67 | 68 | fn len_utf8(&self) -> usize { 69 | match self { 70 | Self::Raw(value) => value.len_utf8(), 71 | Self::Escaped(_, sequence) => sequence[0].len_utf8() + sequence[1].len_utf8(), 72 | } 73 | } 74 | } 75 | 76 | #[derive(Debug, PartialEq)] 77 | pub struct Chars<'a, T: AsChar>(&'a [T]); 78 | 79 | impl<'a, T: AsChar> Chars<'a, T> { 80 | pub fn len_utf8(&self) -> usize { 81 | self.0.iter().fold(0, |sum, char| sum + char.len_utf8()) 82 | } 83 | } 84 | 85 | impl<'a, T: AsChar> From<&'a [T]> for Chars<'a, T> { 86 | fn from(chars: &'a [T]) -> Self { 87 | Self(chars) 88 | } 89 | } 90 | 91 | impl<'a, T: AsChar> Deref for Chars<'a, T> { 92 | type Target = [T]; 93 | 94 | fn deref(&self) -> &Self::Target { 95 | self.0 96 | } 97 | } 98 | 99 | impl<'a, T: AsChar> ToString for Chars<'a, T> { 100 | fn to_string(&self) -> String { 101 | self.0.iter().map(T::as_char).collect() 102 | } 103 | } 104 | 105 | #[cfg(test)] 106 | mod tests { 107 | use super::*; 108 | 109 | mod char_raw { 110 | use test_case::test_case; 111 | 112 | use super::*; 113 | 114 | #[test] 115 | fn from_char() { 116 | assert_eq!(Char::from('a'), Char::Raw('a')); 117 | } 118 | 119 | #[test] 120 | fn as_char() { 121 | assert_eq!(Char::Raw('a').as_char(), 'a'); 122 | } 123 | 124 | #[test_case('a', 1 ; "ascii")] 125 | #[test_case('á', 2 ; "non-ascii")] 126 | fn len_utf8(value: char, len: usize) { 127 | assert_eq!(Char::Raw(value).len_utf8(), len); 128 | } 129 | 130 | #[test_case('a', "'a'" ; "ascii")] 131 | #[test_case('á', "'á'" ; "non-ascii")] 132 | #[test_case('\0', "'\\0'" ; "null")] 133 | #[test_case('\n', "'\\n'" ; "line feed")] 134 | #[test_case('\r', "'\\r'" ; "carriage return")] 135 | #[test_case('\t', "'\\t'" ; "horizontal tab")] 136 | fn display(value: char, result: &str) { 137 | assert_eq!(Char::Raw(value).to_string(), result); 138 | } 139 | } 140 | 141 | mod char_escaped { 142 | use test_case::test_case; 143 | 144 | use super::*; 145 | 146 | #[test] 147 | fn as_char() { 148 | assert_eq!(Char::Escaped('a', ['b', 'c']).as_char(), 'a'); 149 | } 150 | 151 | #[test_case('a', ['b', 'c'], 2 ; "ascii")] 152 | #[test_case('á', ['b', 'č'], 3 ; "non-ascii")] 153 | fn len_utf8(value: char, sequence: EscapeSequence, len: usize) { 154 | assert_eq!(Char::Escaped(value, sequence).len_utf8(), len); 155 | } 156 | 157 | #[test_case('a', ['b', 'c'], "'a' (escape sequence 'bc')" ; "ascii")] 158 | #[test_case('á', ['b', 'č'], "'á' (escape sequence 'bč')" ; "non-ascii")] 159 | #[test_case('\0', ['%', '0'], "'\\0' (escape sequence '%0')" ; "null")] 160 | #[test_case('\n', ['%', 'n'], "'\\n' (escape sequence '%n')" ; "line feed")] 161 | #[test_case('\r', ['%', 'r'], "'\\r' (escape sequence '%r')" ; "carriage return")] 162 | #[test_case('\t', ['%', 't'], "'\\t' (escape sequence '%t')" ; "horizontal tab")] 163 | fn display(value: char, sequence: EscapeSequence, result: &str) { 164 | assert_eq!(Char::Escaped(value, sequence).to_string(), result); 165 | } 166 | } 167 | 168 | mod chars { 169 | use super::*; 170 | 171 | #[test] 172 | fn from() { 173 | let chars = [Char::Raw('a'), Char::Escaped('b', ['c', 'd'])]; 174 | assert_eq!(Chars(&chars), Chars::from(&chars[..])); 175 | } 176 | 177 | #[test] 178 | fn len_utf8() { 179 | let chars = [Char::Raw('a'), Char::Escaped('b', ['c', 'd'])]; 180 | assert_eq!(Chars(&chars).len_utf8(), 3); 181 | } 182 | 183 | #[test] 184 | fn to_string() { 185 | let chars = [Char::Raw('a'), Char::Escaped('b', ['c', 'd'])]; 186 | assert_eq!(Chars(&chars).to_string(), "ab"); 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/error.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | pub type ErrorRange = Range; 4 | 5 | pub trait GetErrorRange { 6 | fn error_range(&self) -> &ErrorRange; 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/escape.rs: -------------------------------------------------------------------------------- 1 | use std::ascii::escape_default; 2 | use std::fmt::Write; 3 | 4 | pub fn escape_char(char: char) -> String { 5 | let mut result = String::new(); 6 | append_escaped_char(&mut result, char); 7 | result 8 | } 9 | 10 | pub fn escape_str(str: &str) -> String { 11 | let mut result = String::new(); 12 | for char in str.chars() { 13 | append_escaped_char(&mut result, char); 14 | } 15 | result 16 | } 17 | 18 | fn append_escaped_char(string: &mut String, char: char) { 19 | if char.is_ascii() { 20 | if char == '\0' { 21 | string.push_str("\\0"); // We do not want the default '\x00' output 22 | } else { 23 | write!(string, "{}", escape_default(char as u8)) 24 | .expect("Failed to append escaped char to string"); 25 | } 26 | } else { 27 | string.push(char); 28 | } 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | use test_case::test_case; 34 | 35 | use super::*; 36 | 37 | #[test_case('a', "a" ; "ascii")] 38 | #[test_case('á', "á" ; "non-ascii")] 39 | #[test_case('\0', "\\0" ; "null")] 40 | #[test_case('\x01', "\\x01" ; "0x01")] 41 | #[test_case('\n', "\\n" ; "line feed")] 42 | #[test_case('\r', "\\r" ; "carriage return")] 43 | #[test_case('\t', "\\t" ; "horizontal tab")] 44 | fn escape_char(value: char, result: &str) { 45 | assert_eq!(super::escape_char(value), result) 46 | } 47 | 48 | #[test_case("abc123", "abc123" ; "no escaping")] 49 | #[test_case("a\0\0x01\n\r\tá", "a\\0\\0x01\\n\\r\\tá" ; "with escaping")] 50 | fn escape_str(value: &str, result: &str) { 51 | assert_eq!(super::escape_str(value), result) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/eval.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::{error, fmt, result}; 3 | 4 | use crate::pattern::error::{ErrorRange, GetErrorRange}; 5 | use crate::pattern::filter::Filter; 6 | use crate::pattern::utils::AnyString; 7 | 8 | pub type Counter = u32; 9 | 10 | pub struct Context<'a> { 11 | pub working_dir: &'a Path, 12 | pub global_counter: Counter, 13 | pub local_counter: Counter, 14 | pub regex_captures: Option>, 15 | pub expression_quotes: Option, 16 | } 17 | 18 | impl<'a> Context<'a> { 19 | pub fn regex_capture(&self, position: usize) -> &str { 20 | self.regex_captures 21 | .as_ref() 22 | .and_then(|captures| captures.get(position)) 23 | .map_or("", |capture| capture.as_str()) 24 | } 25 | 26 | #[cfg(test)] 27 | pub fn fixture() -> Self { 28 | Context { 29 | #[cfg(unix)] 30 | working_dir: Path::new("/work"), 31 | #[cfg(windows)] 32 | working_dir: Path::new("C:\\work"), 33 | local_counter: 1, 34 | global_counter: 2, 35 | regex_captures: regex::Regex::new("(.).(.)").unwrap().captures("abc"), 36 | expression_quotes: None, 37 | } 38 | } 39 | } 40 | 41 | pub type Result<'a, T> = result::Result>; 42 | pub type BaseResult = result::Result; 43 | 44 | #[derive(Debug, PartialEq)] 45 | pub struct Error<'a> { 46 | pub kind: ErrorKind, 47 | pub value: String, 48 | pub cause: &'a Filter, 49 | pub range: &'a ErrorRange, 50 | } 51 | 52 | impl<'a> error::Error for Error<'a> {} 53 | 54 | impl<'a> GetErrorRange for Error<'a> { 55 | fn error_range(&self) -> &ErrorRange { 56 | self.range 57 | } 58 | } 59 | 60 | impl<'a> fmt::Display for Error<'a> { 61 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 62 | write!( 63 | formatter, 64 | "'{}' evaluation failed for value '{}': {}", 65 | self.cause, self.value, self.kind 66 | ) 67 | } 68 | } 69 | 70 | #[derive(Debug, PartialEq)] 71 | pub enum ErrorKind { 72 | InputNotUtf8, 73 | CanonicalizationFailed(AnyString), 74 | } 75 | 76 | impl fmt::Display for ErrorKind { 77 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 78 | match self { 79 | Self::InputNotUtf8 => write!(formatter, "Input does not have UTF-8 encoding"), 80 | Self::CanonicalizationFailed(reason) => { 81 | write!(formatter, "Path canonicalization failed: {}", reason) 82 | } 83 | } 84 | } 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use test_case::test_case; 90 | 91 | use super::*; 92 | 93 | mod eval_context_regex_capture { 94 | use test_case::test_case; 95 | 96 | use super::*; 97 | 98 | #[test_case(0 ; "position 0")] 99 | #[test_case(1 ; "position 1")] 100 | fn none(position: usize) { 101 | let mut context = Context::fixture(); 102 | context.regex_captures = None; 103 | assert_eq!(context.regex_capture(position), ""); 104 | } 105 | 106 | #[test_case(0, "abc" ; "position 0")] 107 | #[test_case(1, "a" ; "position 1")] 108 | #[test_case(2, "c" ; "position 2")] 109 | #[test_case(3, "" ; "position 3")] 110 | fn some(number: usize, result: &str) { 111 | assert_eq!(Context::fixture().regex_capture(number), result); 112 | } 113 | } 114 | 115 | mod error { 116 | use super::*; 117 | 118 | #[test] 119 | fn range() { 120 | assert_eq!( 121 | Error { 122 | kind: ErrorKind::InputNotUtf8, 123 | cause: &Filter::AbsolutePath, 124 | value: "abc".into(), 125 | range: &(1..2) 126 | } 127 | .error_range(), 128 | &(1..2) 129 | ) 130 | } 131 | 132 | #[test] 133 | fn display() { 134 | assert_eq!( 135 | Error { 136 | kind: ErrorKind::InputNotUtf8, 137 | cause: &Filter::AbsolutePath, 138 | value: "abc".into(), 139 | range: &(1..2) 140 | } 141 | .to_string(), 142 | "'Absolute path' evaluation failed for value 'abc': Input does not have UTF-8 encoding" 143 | ); 144 | } 145 | } 146 | 147 | #[test_case(ErrorKind::InputNotUtf8, "Input does not have UTF-8 encoding" ; "input not utf-8")] 148 | #[test_case(ErrorKind::CanonicalizationFailed("abc".into()), "Path canonicalization failed: abc" ; "canonicalization failed")] 149 | fn error_kind_display(kind: ErrorKind, result: &str) { 150 | assert_eq!(kind.to_string(), result); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/explain.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::io::{Result, Write}; 3 | 4 | use common::color::spec_color; 5 | use termcolor::{Color, WriteColor}; 6 | 7 | use crate::output::highlight_range; 8 | use crate::pattern::parse::Parsed; 9 | use crate::pattern::parser::Item; 10 | use crate::pattern::Pattern; 11 | 12 | impl Pattern { 13 | pub fn explain(&self, output: &mut O, all: bool) -> Result<()> { 14 | for item in &self.items { 15 | match &item.value { 16 | Item::Constant(_) => { 17 | if all { 18 | self.explain_part(output, item, Color::Green)?; 19 | } 20 | } 21 | Item::Expression(filters) => { 22 | if all { 23 | self.explain_part(output, item, Color::Yellow)?; 24 | } 25 | for filter in filters { 26 | self.explain_part(output, filter, Color::Blue)?; 27 | } 28 | } 29 | } 30 | } 31 | Ok(()) 32 | } 33 | 34 | fn explain_part(&self, output: &mut O, part: &Parsed, color: Color) -> Result<()> 35 | where 36 | O: Write + WriteColor, 37 | T: Display, 38 | { 39 | highlight_range(output, &self.source, &part.range, color)?; 40 | writeln!(output)?; 41 | output.set_color(&spec_color(color))?; 42 | write!(output, "{}", part.value)?; 43 | output.reset()?; 44 | write!(output, "\n\n") 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use common::testing::{ColoredOuput, OutputChunk}; 51 | use test_case::test_case; 52 | 53 | use super::*; 54 | use crate::pattern::filter::Filter; 55 | use crate::pattern::parse::Parsed; 56 | 57 | #[test_case(empty_pattern(), false, Vec::new() ; "empty filters")] 58 | #[test_case(empty_pattern(), true, Vec::new() ; "empty all")] 59 | #[test_case(nonempty_pattern(), false, filter_chunks() ; "nonempty filters")] 60 | #[test_case(nonempty_pattern(), true, all_chunks() ; "nonempty all")] 61 | fn explain(pattern: Pattern, all: bool, chunks: Vec) { 62 | let mut output = ColoredOuput::new(); 63 | pattern.explain(&mut output, all).unwrap(); 64 | assert_eq!(output.chunks(), &chunks); 65 | } 66 | 67 | fn empty_pattern() -> Pattern { 68 | Pattern { 69 | source: String::new(), 70 | items: Vec::new(), 71 | } 72 | } 73 | 74 | fn nonempty_pattern() -> Pattern { 75 | Pattern { 76 | source: "_{f|t}".into(), 77 | items: vec![ 78 | Parsed { 79 | value: Item::Constant("_".into()), 80 | range: 0..1, 81 | }, 82 | Parsed { 83 | value: Item::Expression(vec![ 84 | Parsed { 85 | value: Filter::FileName, 86 | range: 2..3, 87 | }, 88 | Parsed { 89 | value: Filter::Trim, 90 | range: 4..5, 91 | }, 92 | ]), 93 | range: 1..6, 94 | }, 95 | ], 96 | } 97 | } 98 | 99 | fn all_chunks() -> Vec { 100 | vec![ 101 | OutputChunk::bold_color(Color::Green, "_"), 102 | OutputChunk::plain("{f|t}\n"), 103 | OutputChunk::bold_color(Color::Green, "^"), 104 | OutputChunk::plain("\n\n"), 105 | OutputChunk::color(Color::Green, "Constant '_'"), 106 | OutputChunk::plain("\n\n_"), 107 | OutputChunk::bold_color(Color::Yellow, "{f|t}"), 108 | OutputChunk::plain("\n "), 109 | OutputChunk::bold_color(Color::Yellow, "^^^^^"), 110 | OutputChunk::plain("\n\n"), 111 | OutputChunk::color(Color::Yellow, "Expression with 2 filters"), 112 | OutputChunk::plain("\n\n_{"), 113 | OutputChunk::bold_color(Color::Blue, "f"), 114 | OutputChunk::plain("|t}\n "), 115 | OutputChunk::bold_color(Color::Blue, "^"), 116 | OutputChunk::plain("\n\n"), 117 | OutputChunk::color(Color::Blue, "File name"), 118 | OutputChunk::plain("\n\n_{f|"), 119 | OutputChunk::bold_color(Color::Blue, "t"), 120 | OutputChunk::plain("}\n "), 121 | OutputChunk::bold_color(Color::Blue, "^"), 122 | OutputChunk::plain("\n\n"), 123 | OutputChunk::color(Color::Blue, "Trim"), 124 | OutputChunk::plain("\n\n"), 125 | ] 126 | } 127 | 128 | fn filter_chunks() -> Vec { 129 | vec![ 130 | OutputChunk::plain("_{"), 131 | OutputChunk::bold_color(Color::Blue, "f"), 132 | OutputChunk::plain("|t}\n "), 133 | OutputChunk::bold_color(Color::Blue, "^"), 134 | OutputChunk::plain("\n\n"), 135 | OutputChunk::color(Color::Blue, "File name"), 136 | OutputChunk::plain("\n\n_{f|"), 137 | OutputChunk::bold_color(Color::Blue, "t"), 138 | OutputChunk::plain("}\n "), 139 | OutputChunk::bold_color(Color::Blue, "^"), 140 | OutputChunk::plain("\n\n"), 141 | OutputChunk::color(Color::Blue, "Trim"), 142 | OutputChunk::plain("\n\n"), 143 | ] 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/help.rs: -------------------------------------------------------------------------------- 1 | use indoc::indoc; 2 | 3 | pub const PATTERN: &str = indoc! {r#" 4 | # SYNTAX 5 | 6 | `abc` Constant 7 | `{}` Empty expression 8 | `{x}` Expression with a filter 9 | `{x|y|z}` Expression with multiple filters 10 | `a{}b{x|y}c` Mixed constant and expresions. 11 | 12 | # RULES 13 | 14 | 1. Constants are directly copied to output. 15 | 2. Expression is replaced by input value. 16 | 3. Filters are consecutively applied on input value. 17 | 18 | # ESCAPING 19 | 20 | `%/` System directory separator 21 | `%n` Line feed 22 | `%r` Carriage return 23 | `%t` Horizontal tab 24 | `%0` Null 25 | `%{` Escaped `{` 26 | `%|` Escaped `|` 27 | `%}` Escaped `{` 28 | `%%` Escaped `%` 29 | "#}; 30 | 31 | pub const FILTERS: &str = indoc! {r#" 32 | # PATH 33 | 34 | `f` File name `d` Parent directory 35 | `F` Last name `D` Remove last name 36 | 37 | `b` Base name `e` Extension 38 | `B` Remove extension `E` Extension with dot 39 | 40 | `w` Working directory 41 | 42 | `a` Absolute path `p` Normalized path 43 | `A` Relative path `P` Canonical path 44 | 45 | `z` Ensure trailing directory separator 46 | `Z` Remove trailing directory separator 47 | 48 | # SUBSTRING 49 | 50 | `#A-B` From `A` to `B` (`A`, `B` = inclusive 1-based index) 51 | `#A+L` From `A` of length `L` (`-A` = backward indexing) 52 | `#A-` From `A` to end 53 | `#A` Character at `A` 54 | 55 | # FIELD 56 | 57 | `&N:S` Field `N`, string separator `S` (`:` = any delimiter char except `/`) 58 | `&N/S` Field `N`, regex separator `S` (`N` = 1-based index) 59 | `&N` Field `N`, default separator (`-N` = backward indexing) 60 | 61 | # STRING REPLACE 62 | 63 | `r:X:Y` Replace `X` with `Y` (`r` = first occurence) 64 | `r:X` Remove `X` (`R` = all occurences) 65 | `?D` Replace empty with `D` (`:` = any delimiter char) 66 | 67 | # REGEX REPLACE 68 | 69 | `s:X:Y` Replace match of `X` with `Y` (`s` = first occurence) 70 | `s:X` Remove match of `X` (`S` = all occurences) 71 | 72 | # REGEX MATCH 73 | 74 | `=N:E` `N`-th match of regex `E` (`:` = any delimiter char) 75 | `=N-M:E` `N`-th to `M`-th match of regex `E` (`N`, `M` = inclusive 1-based index) 76 | `=N-:E` `N`-th to the last match of regex `E` (`-N` = backward indexing) 77 | 78 | # REGEX SWITCH 79 | 80 | `@:X:Y` Output `Y` if input matches `X` (`:` = any delimiter char) 81 | `@:X:Y:D` Output `Y` if input matches `X`, `D` for no match 82 | 83 | `@:X1:Y1:...:Xn:Yn:D` Output `Yi` for first match of `Xi`, `D` for no match 84 | 85 | # REGEX CAPTURES 86 | 87 | `$0`, `$1`, `$2`, ... Capture group of a global regex or `s/S/@` regex 88 | 89 | # FORMATTING 90 | 91 | `t` Trim 92 | `^` To uppercase `i` To ASCII 93 | `v` To lowercase `I` Remove non-ASCII chars 94 | 95 | `*N` Repeat `N` times 96 | `<>` or `>` to right pad) 97 | `(reader: &mut Reader) -> Result { 7 | let position = reader.position(); 8 | let value = parse_integer(reader)?; 9 | 10 | shift_index(value).map_err(|kind| Error { 11 | kind, 12 | range: position..reader.position(), 13 | }) 14 | } 15 | 16 | pub fn shift_index(value: T) -> BaseResult { 17 | if value >= T::one() { 18 | Ok(value - T::one()) 19 | } else { 20 | Err(ErrorKind::IndexZero) 21 | } 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use test_case::test_case; 27 | 28 | use super::*; 29 | use crate::pattern::error::ErrorRange; 30 | 31 | mod parse_index { 32 | use test_case::test_case; 33 | 34 | use super::*; 35 | 36 | #[test_case("abc", 0..3, ErrorKind::ExpectedNumber ; "invalid")] 37 | #[test_case("0abc", 0..1, ErrorKind::IndexZero ; "zero")] 38 | fn err(input: &str, range: ErrorRange, kind: ErrorKind) { 39 | assert_eq!( 40 | parse_index::(&mut Reader::from(input)), 41 | Err(Error { kind, range }) 42 | ); 43 | } 44 | 45 | #[test_case("1", 0 ; "one")] 46 | #[test_case("123abc", 122 ; "multiple digits and chars")] 47 | fn ok(input: &str, result: usize) { 48 | assert_eq!(parse_index(&mut Reader::from(input)), Ok(result)); 49 | } 50 | } 51 | 52 | #[test_case(0, Err(ErrorKind::IndexZero) ; "zero")] 53 | #[test_case(1, Ok(0) ; "positive")] 54 | fn shift_index(index: usize, result: BaseResult) { 55 | assert_eq!(super::shift_index(index), result) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/integer.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | use std::str::FromStr; 3 | 4 | use num_traits::PrimInt; 5 | 6 | use crate::pattern::char::Char; 7 | use crate::pattern::parse::{Error, ErrorKind, Result}; 8 | use crate::pattern::reader::Reader; 9 | 10 | pub trait ParsableInt: PrimInt + FromStr + Display + Debug {} 11 | 12 | impl ParsableInt for T {} 13 | 14 | pub fn parse_integer(reader: &mut Reader) -> Result { 15 | let position = reader.position(); 16 | let mut buffer = String::new(); 17 | 18 | while let Some(digit @ '0'..='9') = reader.peek_char() { 19 | buffer.push(digit); 20 | reader.seek(); 21 | } 22 | 23 | if buffer.is_empty() { 24 | Err(Error { 25 | kind: ErrorKind::ExpectedNumber, 26 | range: reader.position()..reader.end(), 27 | }) 28 | } else { 29 | buffer.parse::().map_err(|_| Error { 30 | // This is the only possible reason for an error 31 | kind: ErrorKind::IntegerOverflow(T::max_value().to_string()), 32 | range: position..reader.position(), 33 | }) 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | 41 | mod parse_integer { 42 | use test_case::test_case; 43 | 44 | use super::*; 45 | use crate::pattern::error::ErrorRange; 46 | 47 | #[test_case("", 0..0, ErrorKind::ExpectedNumber ; "empty")] 48 | #[test_case("ab", 0..2, ErrorKind::ExpectedNumber ; "alpha")] 49 | #[test_case("256", 0..3, ErrorKind::IntegerOverflow("255".into()) ; "overflow")] 50 | #[test_case("256a", 0..3, ErrorKind::IntegerOverflow("255".into()) ; "overflow then alpha")] 51 | fn err(input: &str, range: ErrorRange, kind: ErrorKind) { 52 | assert_eq!( 53 | parse_integer::(&mut Reader::from(input)), 54 | Err(Error { kind, range }) 55 | ); 56 | } 57 | 58 | #[test_case("0", 0, 1 ; "zero")] 59 | #[test_case("00", 0, 2 ; "zero then zero")] 60 | #[test_case("01", 1, 2 ; "zero then nonzero")] 61 | #[test_case("0a", 0, 1 ; "zero then alpha")] 62 | #[test_case("1", 1, 1 ; "single digit")] 63 | #[test_case("1a", 1, 1 ; "single digit then alpha")] 64 | #[test_case("123", 123, 3 ; "multiple digits")] 65 | #[test_case("123a", 123, 3 ; "multiple digits then alpha")] 66 | fn ok(input: &str, output: usize, position: usize) { 67 | let mut reader = Reader::from(input); 68 | assert_eq!(parse_integer(&mut reader), Ok(output)); 69 | assert_eq!(reader.position(), position); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::pattern::filter::Filter; 2 | use crate::pattern::parser::{Item, ParsedItem, Parser}; 3 | 4 | mod char; 5 | pub mod error; 6 | mod escape; 7 | pub mod eval; 8 | mod explain; 9 | mod field; 10 | pub mod filter; 11 | pub mod help; 12 | mod index; 13 | mod integer; 14 | mod lexer; 15 | mod number; 16 | mod padding; 17 | pub mod parse; 18 | mod parser; 19 | pub mod path; 20 | mod range; 21 | mod reader; 22 | pub mod regex; 23 | mod repeat; 24 | mod replace; 25 | mod substr; 26 | mod switch; 27 | pub mod symbols; 28 | mod utils; 29 | mod uuid; 30 | 31 | #[derive(Debug, PartialEq)] 32 | pub struct Pattern { 33 | source: String, 34 | items: Vec, 35 | } 36 | 37 | impl Pattern { 38 | pub fn parse(source: &str, config: &parse::Config) -> parse::Result { 39 | Ok(Self { 40 | source: source.into(), 41 | items: Parser::new(source, config).parse_items()?, 42 | }) 43 | } 44 | 45 | pub fn uses_local_counter(&self) -> bool { 46 | self.uses_filter(|filter| *filter == Filter::LocalCounter) 47 | } 48 | 49 | pub fn uses_global_counter(&self) -> bool { 50 | self.uses_filter(|filter| *filter == Filter::GlobalCounter) 51 | } 52 | 53 | pub fn uses_regex_capture(&self) -> bool { 54 | self.uses_filter(|variable| matches!(variable, Filter::RegexCapture(_))) 55 | } 56 | 57 | fn uses_filter bool>(&self, test: F) -> bool { 58 | self.items.iter().any(|item| { 59 | if let Item::Expression(filters) = &item.value { 60 | filters.iter().any(|filter| test(&filter.value)) 61 | } else { 62 | false 63 | } 64 | }) 65 | } 66 | 67 | pub fn eval(&self, input: &str, context: &eval::Context) -> eval::Result { 68 | let mut output = String::new(); 69 | 70 | for item in &self.items { 71 | match &item.value { 72 | Item::Constant(value) => output.push_str(value), 73 | Item::Expression(filters) => { 74 | let mut value = input.to_string(); 75 | 76 | for filter in filters.iter() { 77 | match filter.value.eval(value, context) { 78 | Ok(result) => value = result, 79 | Err(kind) => { 80 | return Err(eval::Error { 81 | kind, 82 | value: input.to_string(), 83 | cause: &filter.value, 84 | range: &filter.range, 85 | }); 86 | } 87 | } 88 | } 89 | 90 | if let Some(quotes) = context.expression_quotes { 91 | output.push(quotes); 92 | output.push_str(&value); 93 | output.push(quotes); 94 | } else { 95 | output.push_str(&value); 96 | } 97 | } 98 | } 99 | } 100 | 101 | Ok(output) 102 | } 103 | } 104 | 105 | #[cfg(test)] 106 | impl From> for Pattern { 107 | fn from(items: Vec) -> Self { 108 | Self { 109 | source: String::new(), 110 | items, 111 | } 112 | } 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use test_case::test_case; 118 | 119 | use super::filter::Filter; 120 | use super::parse::Parsed; 121 | use super::parser::Item; 122 | use super::Pattern; 123 | use crate::pattern::utils::AnyString; 124 | 125 | mod parse { 126 | use super::super::parse::{Config, Error, ErrorKind, Parsed}; 127 | use super::*; 128 | 129 | #[test] 130 | fn err() { 131 | assert_eq!( 132 | Pattern::parse("{", &Config::fixture()), 133 | Err(Error { 134 | kind: ErrorKind::UnmatchedExprStart, 135 | range: 0..1 136 | }) 137 | ) 138 | } 139 | 140 | #[test] 141 | fn ok() { 142 | assert_eq!( 143 | Pattern::parse("_%{{f|v}%}_", &Config::fixture()), 144 | Ok(Pattern { 145 | source: "_%{{f|v}%}_".into(), 146 | items: vec![ 147 | Parsed { 148 | value: Item::Constant("_{".into()), 149 | range: 0..3, 150 | }, 151 | Parsed { 152 | value: Item::Expression(vec![ 153 | Parsed { 154 | value: Filter::FileName, 155 | range: 4..5, 156 | }, 157 | Parsed { 158 | value: Filter::ToLowercase, 159 | range: 6..7, 160 | } 161 | ]), 162 | range: 3..8, 163 | }, 164 | Parsed { 165 | value: Item::Constant("}_".into()), 166 | range: 8..11, 167 | }, 168 | ] 169 | }) 170 | ) 171 | } 172 | } 173 | 174 | #[test_case(Filter::FileName, false, false, false ; "none")] 175 | #[test_case(Filter::LocalCounter, true, false, false ; "local counter")] 176 | #[test_case(Filter::GlobalCounter, false, true, false ; "global counter")] 177 | #[test_case(Filter::RegexCapture(1), false, false, true ; "regex capture")] 178 | fn uses(filter: Filter, local_counter: bool, global_counter: bool, regex_capture: bool) { 179 | let pattern = Pattern::from(vec![ 180 | Parsed::from(Item::Constant("a".into())), 181 | Parsed::from(Item::Expression(vec![Parsed::from(filter)])), 182 | ]); 183 | assert_eq!(pattern.uses_local_counter(), local_counter); 184 | assert_eq!(pattern.uses_global_counter(), global_counter); 185 | assert_eq!(pattern.uses_regex_capture(), regex_capture); 186 | } 187 | 188 | mod eval { 189 | use test_case::test_case; 190 | 191 | use super::super::eval::{Context, Error, ErrorKind}; 192 | use super::*; 193 | use crate::pattern::parser::ParsedItem; 194 | 195 | #[test] 196 | fn err() { 197 | let pattern = Pattern::from(vec![Parsed::from(Item::Expression(vec![Parsed { 198 | value: Filter::CanonicalPath, 199 | range: 1..2, 200 | }]))]); 201 | assert_eq!( 202 | pattern.eval("dir/file.ext", &Context::fixture()), 203 | Err(Error { 204 | kind: ErrorKind::CanonicalizationFailed(AnyString::any()), 205 | value: "dir/file.ext".into(), 206 | cause: &Filter::CanonicalPath, 207 | range: &(1..2usize), 208 | }) 209 | ); 210 | } 211 | 212 | #[test_case("", constant(), None, "abc" ; "constant ")] 213 | #[test_case("a/b", empty_expr(), None, "a/b" ; "empty expression")] 214 | #[test_case("a/b", single_filter(), None, "b" ; "single filter ")] 215 | #[test_case("a/b", multi_filter(), None, "B" ; "multi filter ")] 216 | #[test_case("a/b", complex_expr(), None, "1 a 2 B 3" ; "complex expression")] 217 | #[test_case("a/b", complex_expr(), Some('\''), "1 'a' 2 'B' 3" ; "quoted complex expression")] 218 | fn ok(input: &str, items: Vec, quotes: Option, output: &str) { 219 | let pattern = Pattern::from(items); 220 | let mut context = Context::fixture(); 221 | context.expression_quotes = quotes; 222 | assert_eq!(pattern.eval(input, &context), Ok(output.into())); 223 | } 224 | 225 | fn constant() -> Vec { 226 | vec![Parsed::from(Item::Constant("abc".into()))] 227 | } 228 | 229 | fn empty_expr() -> Vec { 230 | vec![Parsed::from(Item::Expression(vec![]))] 231 | } 232 | 233 | fn single_filter() -> Vec { 234 | vec![Parsed::from(Item::Expression(vec![Parsed::from( 235 | Filter::FileName, 236 | )]))] 237 | } 238 | 239 | fn multi_filter() -> Vec { 240 | vec![Parsed::from(Item::Expression(vec![ 241 | Parsed::from(Filter::FileName), 242 | Parsed::from(Filter::ToUppercase), 243 | ]))] 244 | } 245 | 246 | fn complex_expr() -> Vec { 247 | vec![ 248 | Parsed::from(Item::Constant("1 ".into())), 249 | Parsed::from(Item::Expression(vec![Parsed::from( 250 | Filter::ParentDirectory, 251 | )])), 252 | Parsed::from(Item::Constant(" 2 ".into())), 253 | Parsed::from(Item::Expression(vec![ 254 | Parsed::from(Filter::FileName), 255 | Parsed::from(Filter::ToUppercase), 256 | ])), 257 | Parsed::from(Item::Constant(" 3".into())), 258 | ] 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/number.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use rand::{thread_rng, Rng}; 4 | 5 | use crate::pattern::range::{Range, RangeType}; 6 | 7 | pub type Number = u64; 8 | pub type NumberRange = Range; 9 | 10 | #[derive(PartialEq, Debug)] 11 | pub struct NumberRangeType; 12 | 13 | impl RangeType for NumberRangeType { 14 | type Value = Number; 15 | 16 | const INDEX: bool = false; 17 | const EMPTY_ALLOWED: bool = true; 18 | const DELIMITER_REQUIRED: bool = true; 19 | const LENGTH_ALLOWED: bool = false; 20 | } 21 | 22 | impl NumberRange { 23 | pub fn random(&self) -> Number { 24 | let start = self.start(); 25 | let end = self.end().unwrap_or(Number::MAX); 26 | 27 | if start == 0 && end == Number::MAX { 28 | thread_rng().gen() // gen_range(start..=end) would cause an overflow in rand lib 29 | } else { 30 | thread_rng().gen_range(start..=end) 31 | } 32 | } 33 | } 34 | 35 | impl fmt::Display for NumberRange { 36 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 37 | match &self { 38 | Range(start, Some(end)) => write!(formatter, "[{}, {}]", start, end - 1), 39 | Range(start, None) => write!( 40 | formatter, 41 | "[{}, 2^{})", 42 | start, 43 | std::mem::size_of::() * 8 44 | ), 45 | } 46 | } 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use test_case::test_case; 52 | 53 | use super::*; 54 | 55 | mod parse { 56 | use test_case::test_case; 57 | 58 | use super::*; 59 | use crate::pattern::error::ErrorRange; 60 | use crate::pattern::parse::{Error, ErrorKind}; 61 | use crate::pattern::reader::Reader; 62 | 63 | #[test_case("2-1", 0..3, ErrorKind::RangeStartOverEnd("2".into(), "1".into()) ; "start above end")] 64 | #[test_case("0+1", 1..2, ErrorKind::ExpectedRangeDelimiter(Some('+'.into())) ; "start with length")] 65 | #[test_case("1", 1..1, ErrorKind::ExpectedRangeDelimiter(None) ; "no delimiter")] 66 | #[test_case("1ab", 1..2, ErrorKind::ExpectedRangeDelimiter(Some('a'.into())) ; "wrong delimiter")] 67 | fn err(input: &str, range: ErrorRange, kind: ErrorKind) { 68 | assert_eq!( 69 | NumberRange::parse(&mut Reader::from(input)), 70 | Err(Error { kind, range }) 71 | ); 72 | } 73 | 74 | #[test_case("", 0, None ; "empty")] 75 | #[test_case("-", 0, None ; "empty ignored chars")] 76 | #[test_case("0-", 0, None ; "start no end")] 77 | #[test_case("0-1", 0, Some(2) ; "start below end")] 78 | #[test_case("1-1", 1, Some(2) ; "start equals end")] 79 | fn ok(input: &str, start: Number, end: Option) { 80 | assert_eq!( 81 | NumberRange::parse(&mut Reader::from(input)), 82 | Ok(NumberRange::new(start, end)) 83 | ); 84 | } 85 | } 86 | 87 | mod random { 88 | use test_case::test_case; 89 | 90 | use super::*; 91 | 92 | const MAX: Number = Number::MAX; 93 | 94 | #[test_case(0, Some(0), 0 ; "lowest")] 95 | #[test_case(MAX, None, MAX ; "highest")] 96 | fn certain(start: Number, end: Option, result: Number) { 97 | assert_eq!(NumberRange::new(start, end).random(), result); 98 | } 99 | 100 | #[test_case(0, Some(MAX) ; "from 0 to max")] // Should not overflow 101 | #[test_case(1, Some(MAX) ; "from 1 to max")] 102 | #[test_case(0, Some(MAX - 1) ; "from 0 to max-1")] 103 | fn uncertain(start: Number, end: Option) { 104 | NumberRange::new(start, end).random(); 105 | } 106 | } 107 | 108 | #[test_case(1, None, "[1, 2^64)" ; "open")] 109 | #[test_case(1, Some(3), "[1, 2]" ; "closed")] 110 | fn display(start: Number, end: Option, result: &str) { 111 | assert_eq!(NumberRange::new(start, end).to_string(), result); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/padding.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::fmt; 3 | 4 | use crate::pattern::char::{AsChar, Char}; 5 | use crate::pattern::escape::escape_str; 6 | use crate::pattern::parse::{Error, ErrorKind, Result}; 7 | use crate::pattern::reader::Reader; 8 | use crate::pattern::repeat::Repetition; 9 | 10 | #[derive(Debug, PartialEq)] 11 | pub enum Padding { 12 | Fixed(String), 13 | Repeated(Repetition), 14 | } 15 | 16 | impl Padding { 17 | pub fn parse(reader: &mut Reader, fixed_prefix: char) -> Result { 18 | let position = reader.position(); 19 | match reader.peek() { 20 | Some(prefix) => match prefix.as_char() { 21 | '0'..='9' => Ok(Self::Repeated(Repetition::parse_with_delimiter(reader)?)), 22 | prefix if prefix == fixed_prefix => { 23 | reader.seek(); 24 | Ok(Self::Fixed(reader.read_to_end().to_string())) 25 | } 26 | _ => Err(Error { 27 | kind: ErrorKind::PaddingPrefixInvalid(fixed_prefix, Some(prefix.clone())), 28 | range: position..(position + prefix.len_utf8()), 29 | }), 30 | }, 31 | None => Err(Error { 32 | kind: ErrorKind::PaddingPrefixInvalid(fixed_prefix, None), 33 | range: position..position, 34 | }), 35 | } 36 | } 37 | 38 | pub fn apply_left(&self, mut value: String) -> String { 39 | for char in self.expand().chars().rev().skip(value.len()) { 40 | value.insert(0, char); 41 | } 42 | value 43 | } 44 | 45 | pub fn apply_right(&self, mut value: String) -> String { 46 | for char in self.expand().chars().skip(value.len()) { 47 | value.push(char); 48 | } 49 | value 50 | } 51 | 52 | fn expand(&self) -> Cow { 53 | match self { 54 | Self::Fixed(value) => Cow::Borrowed(value), 55 | Self::Repeated(repetition) => Cow::Owned(repetition.expand("")), 56 | } 57 | } 58 | } 59 | 60 | impl fmt::Display for Padding { 61 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 62 | match self { 63 | Self::Fixed(value) => write!(formatter, "'{}'", escape_str(value)), 64 | Self::Repeated(repetition) => write!(formatter, "{}", repetition), 65 | } 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | mod tests { 71 | use test_case::test_case; 72 | 73 | use super::*; 74 | use crate::pattern::error::ErrorRange; 75 | 76 | #[test_case("", 0..0, ErrorKind::PaddingPrefixInvalid('<', None) ; "no prefix")] 77 | #[test_case(">abc", 0..1, ErrorKind::PaddingPrefixInvalid('<', Some('>'.into())) ; "invalid prefix")] 78 | fn parse_err(input: &str, range: ErrorRange, kind: ErrorKind) { 79 | assert_eq!( 80 | Padding::parse(&mut Reader::from(input), '<'), 81 | Err(Error { kind, range }) 82 | ); 83 | } 84 | 85 | mod fixed { 86 | use test_case::test_case; 87 | 88 | use super::*; 89 | 90 | #[test_case("<", "" ; "empty")] 91 | #[test_case("(pub T::Value, pub Option); 21 | 22 | impl Range { 23 | #[allow(dead_code)] // Clippy bug 24 | pub fn new(start: T::Value, end: Option) -> Self { 25 | Self(start, end) 26 | } 27 | 28 | pub fn parse(reader: &mut Reader) -> Result { 29 | match reader.peek_char() { 30 | Some('0'..='9') => { 31 | let start_pos = reader.position(); 32 | let start_val = parse_integer(reader)?; 33 | 34 | let start = Self::maybe_shift(start_val).map_err(|kind| Error { 35 | kind, 36 | range: start_pos..reader.position(), 37 | })?; 38 | 39 | match reader.peek_char() { 40 | Some(RANGE_TO) => { 41 | reader.seek(); 42 | 43 | if let Some('0'..='9') = reader.peek_char() { 44 | let end_pos = reader.position(); 45 | let end_val = parse_integer(reader)?; 46 | 47 | let end = Self::maybe_shift(end_val).map_err(|kind| Error { 48 | kind, 49 | range: end_pos..reader.position(), 50 | })?; 51 | 52 | if start_val > end_val { 53 | Err(Error { 54 | kind: ErrorKind::RangeStartOverEnd( 55 | start_val.to_string(), 56 | end_val.to_string(), 57 | ), 58 | range: start_pos..reader.position(), 59 | }) 60 | } else if let Some(end) = end.checked_add(&T::Value::one()) { 61 | Ok(Self(start, Some(end))) 62 | } else { 63 | Ok(Self(start, None)) 64 | } 65 | } else { 66 | Ok(Self(start, None)) 67 | } 68 | } 69 | 70 | Some(RANGE_OF_LENGTH) if T::LENGTH_ALLOWED => { 71 | reader.seek(); 72 | 73 | if let Some('0'..='9') = reader.peek_char() { 74 | let length: T::Value = parse_integer(reader)?; 75 | 76 | if let Some(end) = start.checked_add(&length) { 77 | Ok(Self(start, Some(end))) 78 | } else { 79 | Ok(Self(start, None)) 80 | } 81 | } else { 82 | Err(Error { 83 | kind: ErrorKind::ExpectedRangeLength, 84 | range: reader.position()..reader.end(), 85 | }) 86 | } 87 | } 88 | 89 | _ if T::DELIMITER_REQUIRED => { 90 | let position = reader.position(); 91 | let char = reader.read(); 92 | 93 | Err(Error { 94 | kind: ErrorKind::ExpectedRangeDelimiter(char.map(Clone::clone)), 95 | range: position..reader.position(), 96 | }) 97 | } 98 | 99 | _ => Ok(Self(start, start.checked_add(&T::Value::one()))), 100 | } 101 | } 102 | 103 | Some(_) | None if T::EMPTY_ALLOWED => Ok(Self(T::Value::zero(), None)), 104 | 105 | Some(_) => Err(Error { 106 | kind: ErrorKind::RangeInvalid(reader.peek_to_end().to_string()), 107 | range: reader.position()..reader.end(), 108 | }), 109 | 110 | None => Err(Error { 111 | kind: ErrorKind::ExpectedRange, 112 | range: reader.position()..reader.end(), 113 | }), 114 | } 115 | } 116 | 117 | fn maybe_shift(value: T::Value) -> BaseResult { 118 | if T::INDEX { 119 | shift_index(value) 120 | } else { 121 | Ok(value) 122 | } 123 | } 124 | 125 | pub fn start(&self) -> T::Value { 126 | self.0 127 | } 128 | 129 | pub fn end(&self) -> Option { 130 | self.1 131 | } 132 | 133 | pub fn length(&self) -> Option { 134 | self.1.map(|end| { 135 | let start = self.start(); 136 | assert!(end >= start, "Range start {} >= end {}", start, end); 137 | end - start 138 | }) 139 | } 140 | } 141 | 142 | #[cfg(test)] 143 | pub mod tests { 144 | use test_case::test_case; 145 | 146 | use super::*; 147 | 148 | struct TestType; 149 | type TestValue = u32; 150 | 151 | impl RangeType for TestType { 152 | type Value = TestValue; 153 | 154 | const INDEX: bool = false; 155 | const EMPTY_ALLOWED: bool = false; 156 | const DELIMITER_REQUIRED: bool = false; 157 | const LENGTH_ALLOWED: bool = false; 158 | } 159 | 160 | #[test_case(0, None, 0 ; "from 0 to none")] 161 | #[test_case(1, None, 1 ; "from 1 to none")] 162 | #[test_case(0, Some(0), 0 ; "from 0 to 0")] 163 | #[test_case(0, Some(1), 0 ; "from 0 to 1")] 164 | #[test_case(1, Some(1), 1 ; "from 1 to 1")] 165 | fn start(start: TestValue, end: Option, result: TestValue) { 166 | assert_eq!(Range::(start, end).start(), result); 167 | } 168 | 169 | #[test_case(0, None, None ; "from 0 to none")] 170 | #[test_case(1, None, None ; "from 1 to none")] 171 | #[test_case(0, Some(0), Some(0) ; "from 0 to 0")] 172 | #[test_case(0, Some(1), Some(1) ; "from 0 to 1")] 173 | #[test_case(1, Some(1), Some(1) ; "from 1 to 1")] 174 | fn end(start: TestValue, end: Option, result: Option) { 175 | assert_eq!(Range::(start, end).end(), result); 176 | } 177 | 178 | #[test_case(0, None, None ; "from 0 to none")] 179 | #[test_case(1, None, None ; "from 1 to none")] 180 | #[test_case(0, Some(0), Some(0) ; "from 0 to 0")] 181 | #[test_case(0, Some(1), Some(1) ; "from 0 to 1")] 182 | #[test_case(1, Some(1), Some(0) ; "from 1 to 1")] 183 | fn length(start: TestValue, end: Option, result: Option) { 184 | assert_eq!(Range::(start, end).length(), result); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/reader.rs: -------------------------------------------------------------------------------- 1 | use crate::pattern::char::{AsChar, Chars}; 2 | 3 | pub struct Reader { 4 | chars: Vec, 5 | index: usize, 6 | } 7 | 8 | impl From<&str> for Reader { 9 | fn from(input: &str) -> Self { 10 | Self::new(input.chars().map(T::from).collect()) 11 | } 12 | } 13 | 14 | impl Reader { 15 | pub fn new(chars: Vec) -> Self { 16 | Self { chars, index: 0 } 17 | } 18 | 19 | pub fn position(&self) -> usize { 20 | Chars::from(&self.chars[..self.index]).len_utf8() 21 | } 22 | 23 | pub fn end(&self) -> usize { 24 | Chars::from(&self.chars[..]).len_utf8() 25 | } 26 | 27 | pub fn seek(&mut self) { 28 | self.seek_to(self.index + 1) 29 | } 30 | 31 | pub fn seek_to_end(&mut self) { 32 | self.seek_to(self.chars.len()) 33 | } 34 | 35 | fn seek_to(&mut self, index: usize) { 36 | self.index = self.chars.len().min(index); 37 | } 38 | 39 | pub fn peek(&self) -> Option<&T> { 40 | self.peek_at(self.index) 41 | } 42 | 43 | fn peek_at(&self, index: usize) -> Option<&T> { 44 | if index < self.chars.len() { 45 | Some(&self.chars[index]) 46 | } else { 47 | None 48 | } 49 | } 50 | 51 | pub fn peek_char(&self) -> Option { 52 | self.peek().map(T::as_char) 53 | } 54 | 55 | pub fn peek_to_end(&self) -> Chars { 56 | self.peek_to_end_at(self.index) 57 | } 58 | 59 | fn peek_to_end_at(&self, index: usize) -> Chars { 60 | Chars::from(&self.chars[index..]) 61 | } 62 | 63 | pub fn read(&mut self) -> Option<&T> { 64 | let index = self.index; 65 | self.seek(); 66 | self.peek_at(index) 67 | } 68 | 69 | pub fn read_char(&mut self) -> Option { 70 | self.read().map(T::as_char) 71 | } 72 | 73 | pub fn read_expected(&mut self, expected: char) -> bool { 74 | match self.peek_char() { 75 | Some(value) if value == expected => { 76 | self.seek(); 77 | true 78 | } 79 | _ => false, 80 | } 81 | } 82 | 83 | pub fn read_to_end(&mut self) -> Chars { 84 | let index = self.index; 85 | self.seek_to_end(); 86 | self.peek_to_end_at(index) 87 | } 88 | 89 | pub fn read_until(&mut self, delimiter: &T) -> Chars { 90 | for i in self.index..self.chars.len() { 91 | if self.chars[i].as_char() == delimiter.as_char() { 92 | let index = self.index; 93 | self.seek_to(i + 1); 94 | return Chars::from(&self.chars[index..i]); 95 | } 96 | } 97 | self.read_to_end() 98 | } 99 | } 100 | 101 | #[cfg(test)] 102 | mod tests { 103 | use test_case::test_case; 104 | 105 | use super::*; 106 | use crate::pattern::char::Char; 107 | 108 | const CHARS: [Char; 3] = [ 109 | Char::Raw('a'), 110 | Char::Escaped('b', ['x', 'y']), 111 | Char::Raw('č'), 112 | ]; 113 | 114 | #[test_case(0, 0 ; "index 0")] 115 | #[test_case(1, 1 ; "index 1")] 116 | #[test_case(2, 3 ; "index 2")] 117 | #[test_case(3, 5 ; "index 3")] 118 | fn position(index: usize, position: usize) { 119 | assert_eq!(make_reader_at(index).position(), position); 120 | } 121 | 122 | #[test_case(0, 5 ; "index 0")] 123 | #[test_case(1, 5 ; "index 1")] 124 | #[test_case(2, 5 ; "index 2")] 125 | #[test_case(3, 5 ; "index 3")] 126 | fn end(index: usize, position: usize) { 127 | assert_eq!(make_reader_at(index).end(), position); 128 | } 129 | 130 | #[test_case(0, 1 ; "index 0")] 131 | #[test_case(1, 3 ; "index 1")] 132 | #[test_case(2, 5 ; "index 2")] 133 | #[test_case(3, 5 ; "index 3")] 134 | fn seek(index: usize, position: usize) { 135 | let mut reader = make_reader_at(index); 136 | reader.seek(); 137 | assert_eq!(reader.position(), position); 138 | } 139 | 140 | #[test_case(0, 5 ; "index 0")] 141 | #[test_case(1, 5 ; "index 1")] 142 | #[test_case(2, 5 ; "index 2")] 143 | #[test_case(3, 5 ; "index 3")] 144 | fn seek_to_end(index: usize, position: usize) { 145 | let mut reader = make_reader_at(index); 146 | reader.seek_to_end(); 147 | assert_eq!(reader.position(), position); 148 | } 149 | 150 | #[test_case(0, Some(&CHARS[0]), 0 ; "index 0")] 151 | #[test_case(1, Some(&CHARS[1]), 1 ; "index 1")] 152 | #[test_case(2, Some(&CHARS[2]), 3 ; "index 2")] 153 | #[test_case(3, None, 5 ; "index 3")] 154 | fn peek(index: usize, result: Option<&Char>, position: usize) { 155 | let reader = make_reader_at(index); 156 | assert_eq!(reader.peek(), result); 157 | assert_eq!(reader.position(), position); 158 | } 159 | 160 | #[test_case(0, Some('a'), 0 ; "index 0")] 161 | #[test_case(1, Some('b'), 1 ; "index 1")] 162 | #[test_case(2, Some('č'), 3 ; "index 2")] 163 | #[test_case(3, None, 5 ; "index 3")] 164 | fn peek_char(index: usize, result: Option, position: usize) { 165 | let reader = make_reader_at(index); 166 | assert_eq!(reader.peek_char(), result); 167 | assert_eq!(reader.position(), position); 168 | } 169 | 170 | #[test_case(0, &CHARS[..], 0 ; "index 0")] 171 | #[test_case(1, &CHARS[1..], 1 ; "index 1")] 172 | #[test_case(2, &CHARS[2..], 3 ; "index 2")] 173 | #[test_case(3, &[][..], 5 ; "index 3")] 174 | fn peek_to_end(index: usize, result: &[Char], position: usize) { 175 | let reader = make_reader_at(index); 176 | assert_eq!(reader.peek_to_end(), result.into()); 177 | assert_eq!(reader.position(), position); 178 | } 179 | 180 | #[test_case(0, Some(&CHARS[0]), 1 ; "index 0")] 181 | #[test_case(1, Some(&CHARS[1]), 3 ; "index 1")] 182 | #[test_case(2, Some(&CHARS[2]), 5 ; "index 2")] 183 | #[test_case(3, None, 5 ; "index 3")] 184 | fn read(index: usize, result: Option<&Char>, position: usize) { 185 | let mut reader = make_reader_at(index); 186 | assert_eq!(reader.read(), result); 187 | assert_eq!(reader.position(), position); 188 | } 189 | 190 | #[test_case(0, Some('a'), 1 ; "index 0")] 191 | #[test_case(1, Some('b'), 3 ; "index 1")] 192 | #[test_case(2, Some('č'), 5 ; "index 2")] 193 | #[test_case(3, None, 5 ; "index 3")] 194 | fn read_char(index: usize, result: Option, position: usize) { 195 | let mut reader = make_reader_at(index); 196 | assert_eq!(reader.read_char(), result); 197 | assert_eq!(reader.position(), position); 198 | } 199 | 200 | #[test_case(0, 'a', true, 1 ; "index 0 hit")] 201 | #[test_case(1, 'b', true, 3 ; "index 1 hit")] 202 | #[test_case(2, 'č', true, 5 ; "index 2 hit")] 203 | #[test_case(0, 'x', false, 0 ; "index 0 miss")] 204 | #[test_case(1, 'x', false, 1 ; "index 1 miss")] 205 | #[test_case(2, 'x', false, 3 ; "index 2 miss")] 206 | #[test_case(3, 'x', false, 5 ; "index 3 miss")] 207 | fn read_expected(index: usize, expected: char, result: bool, position: usize) { 208 | let mut reader = make_reader_at(index); 209 | assert_eq!(reader.read_expected(expected), result); 210 | assert_eq!(reader.position(), position); 211 | } 212 | 213 | #[test_case(0, &CHARS[..], 5 ; "index 0")] 214 | #[test_case(1, &CHARS[1..], 5 ; "index 1")] 215 | #[test_case(2, &CHARS[2..], 5 ; "index 2")] 216 | #[test_case(3, &[][..], 5 ; "index 3")] 217 | fn read_to_end(index: usize, result: &[Char], position: usize) { 218 | let mut reader = make_reader_at(index); 219 | assert_eq!(reader.read_to_end(), result.into()); 220 | assert_eq!(reader.position(), position); 221 | } 222 | 223 | #[test_case(0, &CHARS[0], &[][..], 1 ; "index 0 to 0")] 224 | #[test_case(1, &CHARS[1], &[][..], 3 ; "index 1 to 1")] 225 | #[test_case(2, &CHARS[2], &[][..], 5 ; "index 2 to 2")] 226 | #[test_case(0, &CHARS[1], &CHARS[..1], 3 ; "index 0 to 1")] 227 | #[test_case(0, &CHARS[2], &CHARS[..2], 5 ; "index 0 to 2")] 228 | #[test_case(1, &CHARS[2], &CHARS[1..2], 5 ; "index 1 to 2")] 229 | #[test_case(1, &'x'.into(), &CHARS[1..], 5 ; "index 1 to end")] 230 | #[test_case(0, &'x'.into(), &CHARS[..], 5 ; "index 0 to end")] 231 | #[test_case(2, &'x'.into(), &CHARS[2..], 5 ; "index 2 to end")] 232 | #[test_case(3, &'x'.into(), &[][..], 5 ; "index 3 to end")] 233 | fn read_until(index: usize, delimiter: &Char, result: &[Char], position: usize) { 234 | let mut reader = make_reader_at(index); 235 | assert_eq!(reader.read_until(delimiter), result.into()); 236 | assert_eq!(reader.position(), position); 237 | } 238 | 239 | fn make_reader_at(index: usize) -> Reader { 240 | let mut reader = Reader::new(CHARS.into()); 241 | if index > 0 { 242 | reader.seek_to(index) 243 | } 244 | reader 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/repeat.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::pattern::char::Char; 4 | use crate::pattern::escape::escape_str; 5 | use crate::pattern::integer::parse_integer; 6 | use crate::pattern::parse::{Error, ErrorKind, Result}; 7 | use crate::pattern::reader::Reader; 8 | 9 | #[derive(Debug, PartialEq)] 10 | pub struct Repetition { 11 | pub count: usize, 12 | pub value: Option, 13 | } 14 | 15 | impl Repetition { 16 | pub fn parse(reader: &mut Reader) -> Result { 17 | Self::parse_impl(reader, false) 18 | } 19 | 20 | pub fn parse_with_delimiter(reader: &mut Reader) -> Result { 21 | Self::parse_impl(reader, true) 22 | } 23 | 24 | fn parse_impl(reader: &mut Reader, delimiter_required: bool) -> Result { 25 | if reader.peek().is_none() { 26 | return Err(Error { 27 | kind: ErrorKind::ExpectedRepetition, 28 | range: reader.position()..reader.position(), 29 | }); 30 | } 31 | 32 | let count = parse_integer(reader)?; 33 | if reader.read().is_some() { 34 | let value = Some(reader.read_to_end().to_string()); 35 | Ok(Self { count, value }) 36 | } else if delimiter_required { 37 | Err(Error { 38 | kind: ErrorKind::ExpectedDelimiterChar, 39 | range: reader.position()..reader.end(), 40 | }) 41 | } else { 42 | Ok(Self { count, value: None }) 43 | } 44 | } 45 | 46 | pub fn expand(&self, default: &str) -> String { 47 | self.value.as_deref().unwrap_or(default).repeat(self.count) 48 | } 49 | } 50 | 51 | impl fmt::Display for Repetition { 52 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 53 | match &self.value { 54 | None => write!(formatter, "{}x", self.count), 55 | Some(value) => write!(formatter, "{}x '{}'", self.count, escape_str(value)), 56 | } 57 | } 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use test_case::test_case; 63 | 64 | use super::*; 65 | 66 | mod parse { 67 | use test_case::test_case; 68 | 69 | use super::*; 70 | use crate::pattern::error::ErrorRange; 71 | use crate::pattern::parse::{Error, ErrorKind}; 72 | use crate::pattern::reader::Reader; 73 | 74 | #[test_case("", 0..0, ErrorKind::ExpectedRepetition ; "empty")] 75 | #[test_case("ab", 0..2, ErrorKind::ExpectedNumber ; "invalid count")] 76 | fn err(input: &str, range: ErrorRange, kind: ErrorKind) { 77 | assert_eq!( 78 | Repetition::parse(&mut Reader::from(input)), 79 | Err(Error { kind, range }) 80 | ); 81 | } 82 | 83 | #[test_case("12", 12, None ; "missing delimiter")] 84 | #[test_case("12:", 12, Some("") ; "empty value")] 85 | #[test_case("12:ab", 12, Some("ab") ; "nonempty value")] 86 | fn ok(input: &str, count: usize, value: Option<&str>) { 87 | assert_eq!( 88 | Repetition::parse(&mut Reader::from(input)), 89 | Ok(Repetition { 90 | count, 91 | value: value.map(String::from) 92 | }) 93 | ); 94 | } 95 | } 96 | 97 | mod parse_with_delimiter { 98 | use test_case::test_case; 99 | 100 | use super::*; 101 | use crate::pattern::error::ErrorRange; 102 | use crate::pattern::parse::{Error, ErrorKind}; 103 | use crate::pattern::reader::Reader; 104 | 105 | #[test_case("", 0..0, ErrorKind::ExpectedRepetition ; "empty")] 106 | #[test_case("ab", 0..2, ErrorKind::ExpectedNumber ; "invalid count")] 107 | #[test_case("12", 2..2, ErrorKind::ExpectedDelimiterChar ; "missing delimiter")] 108 | fn err(input: &str, range: ErrorRange, kind: ErrorKind) { 109 | assert_eq!( 110 | Repetition::parse_with_delimiter(&mut Reader::from(input)), 111 | Err(Error { kind, range }) 112 | ); 113 | } 114 | 115 | #[test_case("12:", 12, Some("") ; "empty value")] 116 | #[test_case("12:ab", 12, Some("ab") ; "nonempty value")] 117 | fn ok(input: &str, count: usize, value: Option<&str>) { 118 | assert_eq!( 119 | Repetition::parse_with_delimiter(&mut Reader::from(input)), 120 | Ok(Repetition { 121 | count, 122 | value: value.map(String::from) 123 | }) 124 | ); 125 | } 126 | } 127 | 128 | #[test_case(0, None, "", "" ; "default empty zero times")] 129 | #[test_case(1, None, "", "" ; "default empty one time")] 130 | #[test_case(2, None, "", "" ; "default empty multiple times")] 131 | #[test_case(0, None, "xy", "" ; "default nonempty zero times")] 132 | #[test_case(1, None, "xy", "xy" ; "default nonempty one time")] 133 | #[test_case(2, None, "xy", "xyxy" ; "default nonempty multiple times")] 134 | #[test_case(0, Some(""), "xy", "" ; "value empty zero times")] 135 | #[test_case(1, Some(""), "xy", "" ; "value empty one time")] 136 | #[test_case(2, Some(""), "xy", "" ; "value empty multiple times")] 137 | #[test_case(0, Some("ab"),"xy", "" ; "value nonempty zero times")] 138 | #[test_case(1, Some("ab"),"xy", "ab" ; "value nonempty one time")] 139 | #[test_case(2, Some("ab"),"xy", "abab" ; "value nonempty multiple times")] 140 | fn expand(count: usize, value: Option<&str>, default: &str, output: &str) { 141 | assert_eq!( 142 | Repetition { 143 | count, 144 | value: value.map(String::from) 145 | } 146 | .expand(default), 147 | output 148 | ) 149 | } 150 | 151 | #[test_case(5, None, "5x" ; "repeat")] 152 | #[test_case(5, Some("abc"), "5x 'abc'" ; "repeat value")] 153 | fn display(count: usize, value: Option<&str>, result: &str) { 154 | assert_eq!( 155 | Repetition { 156 | count, 157 | value: value.map(String::from) 158 | } 159 | .to_string(), 160 | result 161 | ); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/substr.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::pattern::range::{Range, RangeType}; 4 | 5 | pub type CharIndexRange = Range; 6 | 7 | #[derive(PartialEq, Debug)] 8 | pub struct CharIndexRangeType; 9 | 10 | impl RangeType for CharIndexRangeType { 11 | type Value = usize; 12 | 13 | const INDEX: bool = true; 14 | const EMPTY_ALLOWED: bool = false; 15 | const DELIMITER_REQUIRED: bool = false; 16 | const LENGTH_ALLOWED: bool = true; 17 | } 18 | 19 | impl CharIndexRange { 20 | pub fn substr(&self, mut value: String) -> String { 21 | if let Some((start, _)) = value.char_indices().nth(self.start()) { 22 | value.replace_range(..start, ""); 23 | } else { 24 | value.clear(); 25 | } 26 | 27 | if let Some(length) = self.length() { 28 | if let Some((end, _)) = value.char_indices().nth(length) { 29 | value.truncate(end); 30 | } 31 | } 32 | 33 | value 34 | } 35 | 36 | pub fn substr_rev(&self, mut value: String) -> String { 37 | let start = self.start(); 38 | if start > 0 { 39 | if let Some((start, _)) = value.char_indices().nth_back(start - 1) { 40 | value.truncate(start); 41 | } else { 42 | value.clear(); 43 | } 44 | } 45 | 46 | if let Some(length) = self.length() { 47 | if length > 0 { 48 | if let Some((end, _)) = value.char_indices().nth_back(length - 1) { 49 | value.replace_range(..end, ""); 50 | } 51 | } else { 52 | value.clear(); 53 | } 54 | } 55 | 56 | value 57 | } 58 | } 59 | 60 | impl fmt::Display for CharIndexRange { 61 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 62 | match &self { 63 | Range(start, Some(end)) => write!(formatter, "{}..{}", start + 1, end), 64 | Range(start, None) => write!(formatter, "{}..", start + 1), 65 | } 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | mod tests { 71 | use test_case::test_case; 72 | 73 | use super::*; 74 | use crate::pattern::error::ErrorRange; 75 | 76 | mod parse { 77 | use test_case::test_case; 78 | 79 | use super::*; 80 | use crate::pattern::parse::{Error, ErrorKind}; 81 | use crate::pattern::reader::Reader; 82 | 83 | #[test_case("", 0..0, ErrorKind::ExpectedRange ; "empty")] 84 | #[test_case("-", 0..1, ErrorKind::RangeInvalid("-".into()) ; "invalid")] 85 | #[test_case("0-", 0..1, ErrorKind::IndexZero ; "zero start")] 86 | #[test_case("1-0", 2..3, ErrorKind::IndexZero ; "zero end")] 87 | #[test_case("2-1", 0..3, ErrorKind::RangeStartOverEnd("2".into(), "1".into()) ; "start above end")] 88 | #[test_case("1+", 2..2, ErrorKind::ExpectedRangeLength ; "start no length")] 89 | #[test_case("1+ab", 2..4, ErrorKind::ExpectedRangeLength ; "start no length but chars")] 90 | fn err(input: &str, range: ErrorRange, kind: ErrorKind) { 91 | assert_eq!( 92 | CharIndexRange::parse(&mut Reader::from(input)), 93 | Err(Error { kind, range }) 94 | ); 95 | } 96 | 97 | #[test_case("1", 0, Some(1) ; "no delimiter")] 98 | #[test_case("2ab", 1, Some(2) ; "no delimiter but chars")] 99 | #[test_case("1-", 0, None ; "no end")] 100 | #[test_case("1-2", 0, Some(2) ; "start below end")] 101 | #[test_case("1-1", 0, Some(1) ; "start equals end")] 102 | #[test_case("2+0", 1, Some(1) ; "start with zero length")] 103 | #[test_case("2+3", 1, Some(4) ; "start with positive length")] 104 | #[test_case(&format!("2+{}", usize::MAX), 1, None ; "start with overflow length")] 105 | fn ok(input: &str, start: usize, end: Option) { 106 | assert_eq!( 107 | CharIndexRange::parse(&mut Reader::from(input)), 108 | Ok(CharIndexRange::new(start, end)) 109 | ); 110 | } 111 | } 112 | 113 | #[test_case("", 0, None, "" ; "empty")] 114 | #[test_case("ábčd", 0, None, "ábčd" ; "before first")] 115 | #[test_case("ábčd", 3, None, "d" ; "before last")] 116 | #[test_case("ábčd", 4, None, "" ; "after last")] 117 | #[test_case("ábčd", 5, None, "" ; "over last")] 118 | #[test_case("ábčd", 0, Some(0), "" ; "before first before first")] 119 | #[test_case("ábčd", 0, Some(1), "á" ; "before first after first")] 120 | #[test_case("ábčd", 0, Some(3), "ábč" ; "before first before last")] 121 | #[test_case("ábčd", 0, Some(4), "ábčd" ; "before first after last")] 122 | #[test_case("ábčd", 0, Some(5), "ábčd" ; "before first over last")] 123 | #[test_case("ábčd", 3, Some(3), "" ; "before last before last")] 124 | #[test_case("ábčd", 3, Some(4), "d" ; "before last after last")] 125 | #[test_case("ábčd", 3, Some(5), "d" ; "before last over last")] 126 | #[test_case("ábčd", 4, Some(4), "" ; "after last after last")] 127 | #[test_case("ábčd", 4, Some(5), "" ; "after last over last")] 128 | #[test_case("ábčd", 5, Some(5), "" ; "over last over last")] 129 | fn substr(input: &str, start: usize, end: Option, output: &str) { 130 | assert_eq!(CharIndexRange::new(start, end).substr(input.into()), output); 131 | } 132 | 133 | #[test_case("", 0, None, "" ; "empty")] 134 | #[test_case("ábčd", 0, None, "ábčd" ; "before first")] 135 | #[test_case("ábčd", 3, None, "á" ; "before last")] 136 | #[test_case("ábčd", 4, None, "" ; "after last")] 137 | #[test_case("ábčd", 5, None, "" ; "over last")] 138 | #[test_case("ábčd", 0, Some(0), "" ; "before first before first")] 139 | #[test_case("ábčd", 0, Some(1), "d" ; "before first after first")] 140 | #[test_case("ábčd", 0, Some(3), "bčd" ; "before first before last")] 141 | #[test_case("ábčd", 0, Some(4), "ábčd" ; "before first after last")] 142 | #[test_case("ábčd", 0, Some(5), "ábčd" ; "before first over last")] 143 | #[test_case("ábčd", 3, Some(3), "" ; "before last before last")] 144 | #[test_case("ábčd", 3, Some(4), "á" ; "before last after last")] 145 | #[test_case("ábčd", 3, Some(5), "á" ; "before last over last")] 146 | #[test_case("ábčd", 4, Some(4), "" ; "after last after last")] 147 | #[test_case("ábčd", 4, Some(5), "" ; "after last over last")] 148 | #[test_case("ábčd", 5, Some(5), "" ; "over last over last")] 149 | fn substr_rev(input: &str, start: usize, end: Option, output: &str) { 150 | assert_eq!( 151 | CharIndexRange::new(start, end).substr_rev(input.into()), 152 | output 153 | ); 154 | } 155 | 156 | #[test_case(1, None, "2.." ; "open")] 157 | #[test_case(1, Some(3), "2..3" ; "closed")] 158 | fn display(start: usize, end: Option, result: &str) { 159 | assert_eq!(CharIndexRange::new(start, end).to_string(), result); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/symbols.rs: -------------------------------------------------------------------------------- 1 | pub const EXPR_START: char = '{'; 2 | pub const EXPR_END: char = '}'; 3 | pub const PIPE: char = '|'; 4 | 5 | pub const REVERSE_INDEX: char = '-'; 6 | pub const RANGE_TO: char = '-'; 7 | pub const RANGE_OF_LENGTH: char = '+'; 8 | 9 | pub const DIR_SEPARATOR: char = '/'; 10 | pub const LINE_FEED: char = 'n'; 11 | pub const CARRIAGE_RETURN: char = 'r'; 12 | pub const HORIZONTAL_TAB: char = 't'; 13 | pub const NULL: char = '0'; 14 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/utils.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Debug, Clone, PartialEq)] 4 | pub struct Empty; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct AnyString(pub String); 8 | 9 | impl PartialEq for AnyString { 10 | fn eq(&self, _: &Self) -> bool { 11 | // This is only useful when comparing system error messages in tests, 12 | // because we cannot rely on a specific error message. 13 | true 14 | } 15 | } 16 | 17 | impl fmt::Display for AnyString { 18 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 19 | fmt::Display::fmt(&self.0, formatter) 20 | } 21 | } 22 | 23 | #[cfg(test)] 24 | impl From<&str> for AnyString { 25 | fn from(value: &str) -> Self { 26 | Self(value.into()) 27 | } 28 | } 29 | 30 | #[cfg(test)] 31 | impl AnyString { 32 | pub fn any() -> Self { 33 | "This value is not compared by test assertions".into() 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | 41 | mod any_string { 42 | use test_case::test_case; 43 | 44 | use super::*; 45 | 46 | #[test_case("a", "a" ; "same")] 47 | #[test_case("a", "b" ; "different")] 48 | fn partial_eq(left: &str, right: &str) { 49 | assert_eq!(AnyString::from(left), AnyString::from(right)); 50 | } 51 | 52 | #[test] 53 | fn display() { 54 | assert_eq!(AnyString::from("abc").to_string(), "abc"); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/bin/rew/pattern/uuid.rs: -------------------------------------------------------------------------------- 1 | use uuid::Uuid; 2 | 3 | pub fn random_uuid() -> String { 4 | let mut buffer = Uuid::encode_buffer(); 5 | let str = Uuid::new_v4().to_hyphenated().encode_lower(&mut buffer); 6 | (*str).to_string() 7 | } 8 | 9 | #[cfg(test)] 10 | pub fn assert_uuid(value: &str) { 11 | let regex_str = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"; 12 | let regex = regex::Regex::new(regex_str).unwrap(); 13 | assert!(regex.is_match(&value), "{} is UUID v4", value); 14 | } 15 | 16 | #[cfg(test)] 17 | mod tests { 18 | #[test] 19 | fn random_uuid() { 20 | super::assert_uuid(&super::random_uuid()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/bin/rew/regex.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use regex::{Captures, Regex}; 4 | 5 | pub enum Solver<'a> { 6 | Value(&'a Regex), 7 | FileName(&'a Regex), 8 | None, 9 | } 10 | 11 | impl<'a> Solver<'a> { 12 | pub fn eval<'t>(&self, value: &'t str) -> Option> { 13 | match self { 14 | Self::Value(regex) => regex.captures(value), 15 | Self::FileName(regex) => { 16 | if let Some(file_name) = Path::new(value).file_name() { 17 | regex.captures( 18 | file_name 19 | .to_str() 20 | .expect("Expected file name to be in UTF-8"), // Because input is also in UTF-8 21 | ) 22 | } else { 23 | None 24 | } 25 | } 26 | Self::None => None, 27 | } 28 | } 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | use claim::*; 34 | use regex::Match; 35 | use test_case::test_case; 36 | 37 | use super::*; 38 | 39 | #[test_case(Solver::Value(®ex()), "" ; "value empty")] 40 | #[test_case(Solver::Value(®ex()), "ab/cd" ; "value nonempty")] 41 | #[test_case(Solver::FileName(®ex()), "" ; "file name empty")] 42 | #[test_case(Solver::FileName(®ex()), "ab/cd" ; "file name nonempty")] 43 | #[test_case(Solver::None, "" ; "none empty")] 44 | #[test_case(Solver::None, "ab/cd" ; "none nonempty")] 45 | fn missed(solver: Solver, input: &str) { 46 | assert_none!(solver.eval(input)); 47 | } 48 | 49 | #[test_case(Solver::Value(®ex()), "aB/cD", 0, Some("aB") ; "value group 0")] 50 | #[test_case(Solver::Value(®ex()), "aB/cD", 1, Some("a") ; "value group 1")] 51 | #[test_case(Solver::Value(®ex()), "aB/cD", 2, Some("B") ; "value group 2")] 52 | #[test_case(Solver::Value(®ex()), "aB/cD", 3, None ; "value group 3")] 53 | #[test_case(Solver::FileName(®ex()), "aB/cD", 0, Some("cD") ; "file name group 0")] 54 | #[test_case(Solver::FileName(®ex()), "aB/cD", 1, Some("c") ; "file name group 1")] 55 | #[test_case(Solver::FileName(®ex()), "aB/cD", 2, Some("D") ; "file name group 2")] 56 | #[test_case(Solver::FileName(®ex()), "aB/cD", 3, None ; "file name group 3")] 57 | fn captured(solver: Solver, input: &str, group: usize, result: Option<&str>) { 58 | assert_eq!( 59 | solver 60 | .eval(input) 61 | .unwrap() 62 | .get(group) 63 | .as_ref() 64 | .map(Match::as_str), 65 | result 66 | ); 67 | } 68 | 69 | fn regex() -> Regex { 70 | Regex::new("([a-z])([A-Z])").unwrap() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/common/color.rs: -------------------------------------------------------------------------------- 1 | use termcolor::{Color, ColorChoice, ColorSpec}; 2 | 3 | pub const COLOR_CHOICES: &[&str] = &[AUTO, ALWAYS, ANSI, NEVER]; 4 | 5 | const AUTO: &str = "auto"; 6 | const ALWAYS: &str = "always"; 7 | const ANSI: &str = "ansi"; 8 | const NEVER: &str = "never"; 9 | 10 | pub fn parse_color(string: &str) -> Result { 11 | match string { 12 | AUTO => Ok(ColorChoice::Auto), 13 | ALWAYS => Ok(ColorChoice::Always), 14 | ANSI => Ok(ColorChoice::AlwaysAnsi), 15 | NEVER => Ok(ColorChoice::Never), 16 | _ => Err("invalid value"), 17 | } 18 | } 19 | 20 | pub fn choose_color(color: Option) -> ColorChoice { 21 | match color { 22 | Some(ColorChoice::Auto) | None => detect_color(), 23 | Some(other) => other, 24 | } 25 | } 26 | 27 | fn detect_color() -> ColorChoice { 28 | if atty::is(atty::Stream::Stdout) { 29 | ColorChoice::Auto 30 | } else { 31 | ColorChoice::Never 32 | } 33 | } 34 | 35 | pub fn spec_color(color: Color) -> ColorSpec { 36 | let mut spec = ColorSpec::new(); 37 | spec.set_fg(Some(color)); 38 | spec 39 | } 40 | 41 | pub fn spec_bold_color(color: Color) -> ColorSpec { 42 | let mut spec = spec_color(color); 43 | spec.set_bold(true); 44 | spec 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use test_case::test_case; 50 | 51 | use super::*; 52 | 53 | #[test_case("", Err("invalid value") ; "empty")] 54 | #[test_case("x", Err("invalid value") ; "invalid")] 55 | #[test_case(ALWAYS, Ok(ColorChoice::Always) ; "always")] 56 | #[test_case(ANSI, Ok(ColorChoice::AlwaysAnsi) ; "ansi")] 57 | #[test_case(AUTO, Ok(ColorChoice::Auto) ; "auto")] 58 | #[test_case(NEVER, Ok(ColorChoice::Never) ; "never")] 59 | fn parse_color(input: &str, result: Result) { 60 | assert_eq!(super::parse_color(input), result); 61 | } 62 | 63 | #[test_case(None, detect_color() ; "none")] 64 | #[test_case(Some(ColorChoice::Auto), detect_color() ; "auto")] 65 | #[test_case(Some(ColorChoice::Always), ColorChoice::Always ; "always")] 66 | #[test_case(Some(ColorChoice::AlwaysAnsi), ColorChoice::AlwaysAnsi ; "ansi")] 67 | #[test_case(Some(ColorChoice::Never), ColorChoice::Never ; "never")] 68 | fn choose_color(value: Option, result: ColorChoice) { 69 | assert_eq!(super::choose_color(value), result) 70 | } 71 | 72 | #[test] 73 | fn spec_color() { 74 | assert_eq!( 75 | &super::spec_color(Color::Red), 76 | ColorSpec::new().set_fg(Some(Color::Red)) 77 | ); 78 | } 79 | 80 | #[test] 81 | fn spec_bold_color() { 82 | assert_eq!( 83 | &super::spec_bold_color(Color::Red), 84 | ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true) 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/common/help.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Result, Write}; 2 | 3 | use lazy_static::lazy_static; 4 | use termcolor::{Buffer, Color, WriteColor}; 5 | 6 | use crate::color::spec_color; 7 | use crate::utils::{into_static_str, str_from_utf8}; 8 | 9 | const HEADING_PREFIX: &str = "# "; 10 | const PADDED_BLOCK_PREFIX: &str = " "; 11 | const SHELL_PREFIX: &str = "$>"; 12 | 13 | const CODE_CHAR: char = '`'; 14 | const COMMENT_CHAR: char = '#'; 15 | 16 | const PRIMARY_COLOR: Color = Color::Yellow; 17 | const SECONDARY_COLOR: Color = Color::Cyan; 18 | const CODE_COLOR: Color = Color::Green; 19 | 20 | lazy_static! { 21 | static ref COLORED_HELP_ENABLED: bool = atty::is(atty::Stream::Stdout) 22 | && std::env::args().any(|arg| arg == "-h" || arg == "--help"); 23 | } 24 | 25 | pub fn highlight_static(text: &'static str) -> &'static str { 26 | if *COLORED_HELP_ENABLED { 27 | highlight_to_string(text).map_or(text, into_static_str) 28 | } else { 29 | text 30 | } 31 | } 32 | 33 | fn highlight_to_string(text: &str) -> Result { 34 | let mut buffer = Buffer::ansi(); 35 | highlight(&mut buffer, text)?; 36 | str_from_utf8(buffer.as_slice()).map(String::from) 37 | } 38 | 39 | pub fn highlight(output: &mut O, text: &str) -> Result<()> { 40 | for line in text.lines() { 41 | if let Some(header) = line.strip_prefix(HEADING_PREFIX) { 42 | output.set_color(&spec_color(PRIMARY_COLOR))?; 43 | write!(output, "{}", header)?; 44 | } else if let Some(block) = line.strip_prefix(PADDED_BLOCK_PREFIX) { 45 | write!(output, "{}", PADDED_BLOCK_PREFIX)?; 46 | output.set_color(&spec_color(SECONDARY_COLOR))?; 47 | 48 | if let Some(command) = block.strip_prefix(SHELL_PREFIX) { 49 | write!(output, "{}", SHELL_PREFIX)?; 50 | output.set_color(&spec_color(CODE_COLOR))?; 51 | 52 | if let Some(comment_index) = command.rfind(COMMENT_CHAR) { 53 | write!(output, "{}", &command[..comment_index])?; 54 | output.set_color(&spec_color(SECONDARY_COLOR))?; 55 | write!(output, "{}", &command[comment_index..])?; 56 | } else { 57 | write!(output, "{}", command)?; 58 | } 59 | } else { 60 | write!(output, "{}", block)?; 61 | } 62 | } else { 63 | highlight_code(output, line)?; 64 | } 65 | 66 | output.reset()?; 67 | writeln!(output)?; 68 | } 69 | 70 | Ok(()) 71 | } 72 | 73 | fn highlight_code(output: &mut O, line: &str) -> Result<()> { 74 | let mut in_code = false; 75 | let mut last_index = 0; 76 | 77 | for (index, char) in line.char_indices() { 78 | if char == CODE_CHAR { 79 | write!(output, "{}", &line[last_index..index])?; 80 | if in_code { 81 | output.reset()?; 82 | } else { 83 | output.set_color(&spec_color(CODE_COLOR))?; 84 | } 85 | last_index = index + 1; 86 | in_code = !in_code; 87 | } 88 | } 89 | 90 | write!(output, "{}", &line[last_index..]) 91 | } 92 | 93 | #[cfg(test)] 94 | mod tests { 95 | use claim::*; 96 | use indoc::indoc; 97 | 98 | use super::*; 99 | use crate::testing::{ColoredOuput, OutputChunk}; 100 | 101 | const SAMPLE_HELP: &str = indoc! {" 102 | # Heading 103 | 104 | Text. 105 | Text with `code`. 106 | Text with `code` and `more code`. 107 | 108 | Padded block. 109 | Padded block with `code`. 110 | 111 | Text. 112 | 113 | $> ls -la 114 | $> ls -la # Shell comment 115 | "}; 116 | 117 | #[test] 118 | fn highlight_to_string() { 119 | assert_gt!(super::highlight_to_string(SAMPLE_HELP).unwrap().len(), 0); 120 | } 121 | 122 | #[test] 123 | fn highlight() { 124 | let mut ouput = ColoredOuput::new(); 125 | super::highlight(&mut ouput, SAMPLE_HELP).unwrap(); 126 | assert_eq!( 127 | ouput.chunks(), 128 | &[ 129 | OutputChunk::color(Color::Yellow, "Heading"), 130 | OutputChunk::plain("\n\nText.\nText with "), 131 | OutputChunk::color(Color::Green, "code"), 132 | OutputChunk::plain(".\nText with "), 133 | OutputChunk::color(Color::Green, "code"), 134 | OutputChunk::plain(" and "), 135 | OutputChunk::color(Color::Green, "more code"), 136 | OutputChunk::plain(".\n\n "), 137 | OutputChunk::color(Color::Cyan, "Padded block."), 138 | OutputChunk::plain("\n "), 139 | OutputChunk::color(Color::Cyan, "Padded block with `code`."), 140 | OutputChunk::plain("\n\nText.\n\n "), 141 | OutputChunk::color(Color::Cyan, "$>"), 142 | OutputChunk::color(Color::Green, " ls -la"), 143 | OutputChunk::plain("\n "), 144 | OutputChunk::color(Color::Cyan, "$>"), 145 | OutputChunk::color(Color::Green, " ls -la "), 146 | OutputChunk::color(Color::Cyan, "# Shell comment"), 147 | OutputChunk::plain("\n") 148 | ] 149 | ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/common/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod color; 2 | pub mod help; 3 | pub mod input; 4 | pub mod output; 5 | pub mod run; 6 | pub mod symbols; 7 | pub mod testing; 8 | pub mod transfer; 9 | pub mod utils; 10 | -------------------------------------------------------------------------------- /src/common/output.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::io::{Result, Write}; 3 | 4 | use termcolor::{Color, WriteColor}; 5 | 6 | use crate::color::spec_color; 7 | 8 | pub fn write_error(output: &mut O, error: &E) -> Result<()> { 9 | output.set_color(&spec_color(Color::Red))?; 10 | write!(output, "error:")?; 11 | output.reset()?; 12 | writeln!(output, " {}", error) 13 | } 14 | 15 | #[cfg(test)] 16 | pub mod tests { 17 | use std::io::{self, ErrorKind}; 18 | 19 | use super::*; 20 | use crate::testing::{ColoredOuput, OutputChunk}; 21 | 22 | #[test] 23 | fn write_error() { 24 | let mut output = ColoredOuput::new(); 25 | let error = io::Error::new(ErrorKind::InvalidData, "message"); 26 | super::write_error(&mut output, &error).unwrap(); 27 | 28 | assert_eq!( 29 | output.chunks(), 30 | &[ 31 | OutputChunk::color(Color::Red, "error:"), 32 | OutputChunk::plain(" message\n") 33 | ] 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/common/run.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Stdin, StdinLock}; 2 | use std::{io, process}; 3 | 4 | use clap::Parser; 5 | use termcolor::{ColorChoice, StandardStream, StandardStreamLock}; 6 | 7 | use crate::color::choose_color; 8 | use crate::output::write_error; 9 | 10 | pub const EXIT_CODE_OK: i32 = 0; 11 | pub const EXIT_CODE_IO_ERROR: i32 = 1; 12 | pub const EXIT_CODE_CLI_ERROR: i32 = 2; 13 | 14 | pub type Result = io::Result; 15 | 16 | pub trait Options: Parser { 17 | fn color(&self) -> Option; 18 | } 19 | 20 | pub struct Io { 21 | stdin: Stdin, 22 | stdout: StandardStream, 23 | stderr: StandardStream, 24 | } 25 | 26 | impl Io { 27 | pub fn new(color: ColorChoice) -> Self { 28 | Self { 29 | stdin: io::stdin(), 30 | stdout: StandardStream::stdout(color), 31 | stderr: StandardStream::stderr(color), 32 | } 33 | } 34 | 35 | pub fn stdin(&self) -> StdinLock { 36 | self.stdin.lock() 37 | } 38 | 39 | pub fn stdout(&self) -> StandardStreamLock { 40 | self.stdout.lock() 41 | } 42 | 43 | pub fn stderr(&self) -> StandardStreamLock { 44 | self.stderr.lock() 45 | } 46 | } 47 | 48 | pub fn exec_run(run: R) 49 | where 50 | O: Options, 51 | R: FnOnce(&O, &Io) -> Result, 52 | { 53 | let options = O::parse(); 54 | let color = choose_color(options.color()); 55 | let io = Io::new(color); 56 | 57 | let exit_code = match run(&options, &io) { 58 | Ok(exit_code) => exit_code, 59 | Err(io_error) => { 60 | write_error(&mut io.stderr(), &io_error).expect("Failed to write to stderr!"); 61 | EXIT_CODE_IO_ERROR 62 | } 63 | }; 64 | 65 | process::exit(exit_code); 66 | } 67 | 68 | #[cfg(test)] 69 | mod tests { 70 | use std::io::{Read, Write}; 71 | 72 | use claim::*; 73 | 74 | use super::*; 75 | 76 | #[test] 77 | fn io() { 78 | let io = Io::new(ColorChoice::Never); 79 | assert_ok!(io.stdin().read_exact(&mut [])); 80 | assert_ok!(io.stdout().flush()); 81 | assert_ok!(io.stderr().flush()); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/common/symbols.rs: -------------------------------------------------------------------------------- 1 | pub const DIFF_IN: char = '<'; 2 | pub const DIFF_OUT: char = '>'; 3 | -------------------------------------------------------------------------------- /src/common/testing.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::io::{Error, ErrorKind, Result, Write}; 3 | 4 | use termcolor::{Color, ColorSpec, WriteColor}; 5 | 6 | use crate::color::{spec_bold_color, spec_color}; 7 | use crate::utils::str_from_utf8; 8 | 9 | pub fn unpack_io_error(error: Error) -> (ErrorKind, String) { 10 | (error.kind(), error.to_string()) 11 | } 12 | 13 | #[derive(Default)] 14 | pub struct ColoredOuput { 15 | spec: ColorSpec, 16 | chunks: Vec, 17 | } 18 | 19 | impl ColoredOuput { 20 | pub fn new() -> Self { 21 | Self::default() 22 | } 23 | 24 | pub fn chunks(&self) -> &Vec { 25 | &self.chunks 26 | } 27 | } 28 | 29 | impl Write for ColoredOuput { 30 | fn write(&mut self, buf: &[u8]) -> Result { 31 | let spec = &self.spec; 32 | let value = str_from_utf8(buf)?; 33 | 34 | if let Some(chunk) = self.chunks.last_mut().filter(|chunk| &chunk.spec == spec) { 35 | chunk.value += value; 36 | } else { 37 | self.chunks.push(OutputChunk { 38 | spec: self.spec.clone(), 39 | value: value.into(), 40 | }) 41 | } 42 | 43 | Ok(buf.len()) 44 | } 45 | 46 | fn flush(&mut self) -> Result<()> { 47 | Ok(()) 48 | } 49 | } 50 | 51 | impl WriteColor for ColoredOuput { 52 | fn supports_color(&self) -> bool { 53 | true 54 | } 55 | 56 | fn set_color(&mut self, spec: &ColorSpec) -> Result<()> { 57 | self.spec = spec.clone(); 58 | Ok(()) 59 | } 60 | 61 | fn reset(&mut self) -> Result<()> { 62 | self.spec = ColorSpec::new(); 63 | Ok(()) 64 | } 65 | } 66 | 67 | #[derive(PartialEq, Clone)] 68 | pub struct OutputChunk { 69 | pub spec: ColorSpec, 70 | pub value: String, 71 | } 72 | 73 | impl OutputChunk { 74 | pub fn plain(value: &str) -> Self { 75 | Self { 76 | spec: ColorSpec::new(), 77 | value: value.into(), 78 | } 79 | } 80 | 81 | pub fn color(color: Color, value: &str) -> Self { 82 | Self { 83 | spec: spec_color(color), 84 | value: value.into(), 85 | } 86 | } 87 | 88 | pub fn bold_color(color: Color, value: &str) -> Self { 89 | Self { 90 | spec: spec_bold_color(color), 91 | value: value.into(), 92 | } 93 | } 94 | } 95 | 96 | impl fmt::Debug for OutputChunk { 97 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 98 | write!(fmt, "OutputChunk::")?; 99 | 100 | match (self.spec.fg(), self.spec.bold()) { 101 | (None, _) => write!(fmt, "plain(")?, 102 | (Some(color), true) => write!(fmt, "bold_color(Color::{:?}, ", color)?, 103 | (Some(color), false) => write!(fmt, "color(Color::{:?}, ", color)?, 104 | } 105 | 106 | write!(fmt, "{:?})", self.value.replace('\n', "\\n")) 107 | } 108 | } 109 | 110 | #[cfg(test)] 111 | mod tests { 112 | use ntest::*; 113 | 114 | use super::*; 115 | 116 | #[test] 117 | fn unpack_io_error() { 118 | assert_eq!( 119 | super::unpack_io_error(Error::new(ErrorKind::Other, "test")), 120 | (ErrorKind::Other, "test".into()) 121 | ); 122 | } 123 | 124 | mod colored_output { 125 | use super::*; 126 | 127 | #[test] 128 | fn supports_color() { 129 | assert_true!(ColoredOuput::new().supports_color()); 130 | } 131 | 132 | #[test] 133 | fn write() { 134 | let mut output = ColoredOuput::new(); 135 | 136 | write!(output, "a").unwrap(); 137 | write!(output, "b").unwrap(); 138 | output.set_color(&spec_color(Color::Red)).unwrap(); 139 | write!(output, "c").unwrap(); 140 | write!(output, "d").unwrap(); 141 | output.set_color(&spec_bold_color(Color::Blue)).unwrap(); 142 | write!(output, "e").unwrap(); 143 | write!(output, "f").unwrap(); 144 | output.reset().unwrap(); 145 | write!(output, "g").unwrap(); 146 | output.flush().unwrap(); 147 | 148 | assert_eq!( 149 | output.chunks, 150 | &[ 151 | OutputChunk::plain("ab"), 152 | OutputChunk::color(Color::Red, "cd"), 153 | OutputChunk::bold_color(Color::Blue, "ef"), 154 | OutputChunk::plain("g"), 155 | ] 156 | ); 157 | } 158 | } 159 | 160 | mod output_chunk { 161 | use test_case::test_case; 162 | 163 | use super::*; 164 | 165 | #[test_case(OutputChunk::plain("ab"), ColorSpec::new(), "ab" ; "plain")] 166 | #[test_case(OutputChunk::color(Color::Red, "cd"), spec_color(Color::Red), "cd" ; "color")] 167 | #[test_case(OutputChunk::bold_color(Color::Blue, "ef"), spec_bold_color(Color::Blue), "ef" ; "bold color")] 168 | fn create(chunk: OutputChunk, spec: ColorSpec, value: &str) { 169 | assert_eq!(chunk.spec, spec); 170 | assert_eq!(chunk.value, value); 171 | } 172 | 173 | #[test_case(OutputChunk::plain("a\nb"), r#"OutputChunk::plain("a\\nb")"# ; "plain")] 174 | #[test_case(OutputChunk::color(Color::Red, "c\nd"), r#"OutputChunk::color(Color::Red, "c\\nd")"# ; "color")] 175 | #[test_case(OutputChunk::bold_color(Color::Blue, "e\nf"), r#"OutputChunk::bold_color(Color::Blue, "e\\nf")"# ; "bold color")] 176 | fn debug(chunk: OutputChunk, result: &str) { 177 | assert_eq!(format!("{:?}", chunk), result); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/common/transfer/input.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::io::{BufRead, Error, ErrorKind, Result}; 3 | use std::path::PathBuf; 4 | 5 | use crate::input::{Splitter, Terminator}; 6 | use crate::symbols::{DIFF_IN, DIFF_OUT}; 7 | 8 | struct Position { 9 | item: usize, 10 | offset: usize, 11 | } 12 | 13 | impl Position { 14 | pub fn new() -> Self { 15 | Self { item: 1, offset: 0 } 16 | } 17 | 18 | pub fn increment(&mut self, size: usize) { 19 | self.item += 1; 20 | self.offset += size; 21 | } 22 | } 23 | 24 | impl fmt::Display for Position { 25 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 26 | write!(formatter, "item #{} at offset {}", self.item, self.offset) 27 | } 28 | } 29 | 30 | pub struct PathDiff { 31 | splitter: Splitter, 32 | position: Position, 33 | } 34 | 35 | impl PathDiff { 36 | pub fn new(input: I, terminator: Terminator) -> Self { 37 | Self { 38 | splitter: Splitter::new(input, terminator), 39 | position: Position::new(), 40 | } 41 | } 42 | 43 | pub fn read(&mut self) -> Result> { 44 | let (in_path, in_size) = match self.splitter.read()? { 45 | Some((value, size)) => (extract_path(value, &self.position, DIFF_IN)?, size), 46 | None => return Ok(None), 47 | }; 48 | self.position.increment(in_size); 49 | 50 | let (out_path, out_size) = match self.splitter.read()? { 51 | Some((value, size)) => (extract_path(value, &self.position, DIFF_OUT)?, size), 52 | None => return Err(make_unexpected_eof_error(&self.position, DIFF_OUT)), 53 | }; 54 | self.position.increment(out_size); 55 | 56 | Ok(Some((in_path, out_path))) 57 | } 58 | } 59 | 60 | fn extract_path(value: &str, position: &Position, prefix: char) -> Result { 61 | if let Some(first_char) = value.chars().next() { 62 | if first_char == prefix { 63 | let path = &value[prefix.len_utf8()..]; 64 | if path.is_empty() { 65 | Err(Error::new( 66 | ErrorKind::UnexpectedEof, 67 | format!("Expected a path after '{}' ({})", prefix, position), 68 | )) 69 | } else { 70 | Ok(path.into()) 71 | } 72 | } else { 73 | Err(Error::new( 74 | ErrorKind::InvalidData, 75 | format!( 76 | "Expected '{}' but got '{}' ({})", 77 | prefix, first_char, position 78 | ), 79 | )) 80 | } 81 | } else { 82 | Err(make_unexpected_eof_error(position, prefix)) 83 | } 84 | } 85 | 86 | fn make_unexpected_eof_error(position: &Position, prefix: char) -> Error { 87 | Error::new( 88 | ErrorKind::UnexpectedEof, 89 | format!("Expected '{}' ({})", prefix, position), 90 | ) 91 | } 92 | 93 | #[cfg(test)] 94 | mod tests { 95 | use super::*; 96 | 97 | #[test] 98 | fn position() { 99 | let mut position = Position::new(); 100 | assert_eq!(position.to_string(), "item #1 at offset 0"); 101 | position.increment(1); 102 | assert_eq!(position.to_string(), "item #2 at offset 1"); 103 | position.increment(2); 104 | assert_eq!(position.to_string(), "item #3 at offset 3"); 105 | } 106 | 107 | mod path_diff { 108 | use test_case::test_case; 109 | 110 | use super::*; 111 | use crate::testing::unpack_io_error; 112 | 113 | #[test_case("", 0, None ; "empty")] 114 | #[test_case("def\n< g \n> h ", 0, Some(("abc", "def")) ; "nonempty 0")] 115 | #[test_case("def\n< g \n> h ", 1, Some((" g ", " h ")) ; "nonempty 1")] 116 | #[test_case("def\n< g \n> h ", 2, None ; "nonempty 2")] 117 | fn ok(input: &str, position: usize, result: Option<(&str, &str)>) { 118 | let mut path_diff = 119 | PathDiff::new(input.as_bytes(), Terminator::Newline { required: false }); 120 | 121 | for _ in 0..position { 122 | path_diff.read().unwrap_or_default(); 123 | } 124 | 125 | assert_eq!( 126 | path_diff.read().map_err(unpack_io_error), 127 | Ok(result.map(|(first, second)| (first.into(), second.into()))) 128 | ); 129 | } 130 | 131 | type E = ErrorKind; 132 | 133 | #[test_case("a", E::InvalidData, "Expected '<' but got 'a' (item #1 at offset 0)" ; "in prefix invalid")] 134 | #[test_case("<", E::UnexpectedEof, "Expected a path after '<' (item #1 at offset 0)" ; "in path missing")] 135 | #[test_case("' (item #2 at offset 2)" ; "in terminator missing")] 136 | #[test_case("' (item #2 at offset 3)" ; "out prefix missing")] 137 | #[test_case("' but got 'b' (item #2 at offset 3)" ; "out prefix invalid")] 138 | #[test_case("", E::UnexpectedEof, "Expected a path after '>' (item #2 at offset 3)" ; "out path missing")] 139 | fn err(input: &str, kind: ErrorKind, message: &str) { 140 | assert_eq!( 141 | PathDiff::new(input.as_bytes(), Terminator::Newline { required: false }) 142 | .read() 143 | .map_err(unpack_io_error), 144 | Err((kind, message.into())) 145 | ) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/common/transfer/mod.rs: -------------------------------------------------------------------------------- 1 | pub use fs::TransferMode; 2 | pub use run::{run_transfer, TransferOptions}; 3 | 4 | mod fs; 5 | mod input; 6 | mod output; 7 | mod run; 8 | #[cfg(test)] 9 | mod testing; 10 | -------------------------------------------------------------------------------- /src/common/transfer/output.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Result, Write}; 2 | use std::path::Path; 3 | 4 | use termcolor::{Color, WriteColor}; 5 | 6 | use crate::color::spec_color; 7 | use crate::transfer::fs::TransferMode; 8 | 9 | pub struct TransferLog { 10 | output: O, 11 | } 12 | 13 | impl TransferLog { 14 | pub fn new(output: O) -> Self { 15 | Self { output } 16 | } 17 | 18 | pub fn begin_transfer( 19 | &mut self, 20 | mode: TransferMode, 21 | src_path: &Path, 22 | dst_path: &Path, 23 | ) -> Result<()> { 24 | let action = match mode { 25 | TransferMode::Move => "Moving", 26 | TransferMode::Copy => "Copying", 27 | }; 28 | write!(self.output, "{} '", action)?; 29 | self.output.set_color(&spec_color(Color::Blue))?; 30 | write!(self.output, "{}", src_path.to_string_lossy())?; 31 | self.output.reset()?; 32 | write!(self.output, "' to '")?; 33 | self.output.set_color(&spec_color(Color::Blue))?; 34 | write!(self.output, "{}", dst_path.to_string_lossy())?; 35 | self.output.reset()?; 36 | write!(self.output, "' ... ")?; 37 | self.output.flush() 38 | } 39 | 40 | pub fn end_with_success(&mut self) -> Result<()> { 41 | self.end_transfer(Color::Green, "OK") 42 | } 43 | 44 | pub fn end_with_failure(&mut self) -> Result<()> { 45 | self.end_transfer(Color::Red, "FAILED") 46 | } 47 | 48 | pub fn end_transfer(&mut self, color: Color, result: &str) -> Result<()> { 49 | self.output.set_color(&spec_color(color))?; 50 | write!(self.output, "{}", result)?; 51 | self.output.reset()?; 52 | writeln!(self.output) 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | pub mod tests { 58 | use test_case::test_case; 59 | 60 | use super::*; 61 | use crate::testing::{ColoredOuput, OutputChunk}; 62 | 63 | #[test_case(TransferMode::Move, "Moving" ; "move ")] 64 | #[test_case(TransferMode::Copy, "Copying" ; "copy")] 65 | fn begin_transfer(mode: TransferMode, output_action: &str) { 66 | let mut output = ColoredOuput::new(); 67 | 68 | TransferLog::new(&mut output) 69 | .begin_transfer(mode, &Path::new("a/b.c"), &Path::new("d/e.f")) 70 | .unwrap(); 71 | 72 | assert_eq!( 73 | output.chunks(), 74 | &[ 75 | OutputChunk::plain(&format!("{} '", output_action)), 76 | OutputChunk::color(Color::Blue, "a/b.c"), 77 | OutputChunk::plain("' to '"), 78 | OutputChunk::color(Color::Blue, "d/e.f"), 79 | OutputChunk::plain("' ... ") 80 | ] 81 | ); 82 | } 83 | 84 | #[test] 85 | fn end_with_success() { 86 | let mut output = ColoredOuput::new(); 87 | TransferLog::new(&mut output).end_with_success().unwrap(); 88 | 89 | assert_eq!( 90 | output.chunks(), 91 | &[ 92 | OutputChunk::color(Color::Green, "OK"), 93 | OutputChunk::plain("\n") 94 | ] 95 | ); 96 | } 97 | 98 | #[test] 99 | fn end_with_failure() { 100 | let mut output = ColoredOuput::new(); 101 | TransferLog::new(&mut output).end_with_failure().unwrap(); 102 | 103 | assert_eq!( 104 | output.chunks(), 105 | &[ 106 | OutputChunk::color(Color::Red, "FAILED"), 107 | OutputChunk::plain("\n") 108 | ] 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/common/transfer/run.rs: -------------------------------------------------------------------------------- 1 | use crate::input::Terminator; 2 | use crate::output::write_error; 3 | use crate::run::{Io, Options, Result, EXIT_CODE_IO_ERROR, EXIT_CODE_OK}; 4 | use crate::transfer::fs::{transfer_path, TransferMode}; 5 | use crate::transfer::input::PathDiff; 6 | use crate::transfer::output::TransferLog; 7 | 8 | pub trait TransferOptions { 9 | fn read_nul(&self) -> bool; 10 | fn verbose(&self) -> bool; 11 | fn fail_at_end(&self) -> bool; 12 | } 13 | 14 | pub fn run_transfer(options: &O, io: &Io, mode: TransferMode) -> Result 15 | where 16 | O: Options + TransferOptions, 17 | { 18 | let terminator = if options.read_nul() { 19 | Terminator::Byte { 20 | value: 0, 21 | required: false, 22 | } 23 | } else { 24 | Terminator::Newline { required: false } 25 | }; 26 | 27 | let mut path_diff = PathDiff::new(io.stdin(), terminator); 28 | let mut log = TransferLog::new(io.stdout()); 29 | let mut exit_code = EXIT_CODE_OK; 30 | 31 | while let Some((src_path, dst_path)) = path_diff.read()? { 32 | if options.verbose() { 33 | log.begin_transfer(mode, &src_path, &dst_path)?; 34 | } 35 | 36 | match transfer_path(&src_path, &dst_path, mode) { 37 | Ok(()) => { 38 | if options.verbose() { 39 | log.end_with_success()?; 40 | } 41 | } 42 | Err(error) => { 43 | if options.verbose() { 44 | log.end_with_failure()?; 45 | } 46 | 47 | write_error(&mut io.stderr(), &error)?; 48 | 49 | if options.fail_at_end() { 50 | exit_code = EXIT_CODE_IO_ERROR; 51 | } else { 52 | return Ok(EXIT_CODE_IO_ERROR); 53 | } 54 | } 55 | } 56 | } 57 | 58 | Ok(exit_code) 59 | } 60 | -------------------------------------------------------------------------------- /src/common/transfer/testing.rs: -------------------------------------------------------------------------------- 1 | use fs_extra::error::{Error, ErrorKind}; 2 | 3 | pub fn unpack_fse_error(error: Error) -> (String, String) { 4 | let message = error.to_string(); 5 | (debug_fse_error_kind(error.kind), message) 6 | } 7 | 8 | pub fn debug_fse_error_kind(error_kind: ErrorKind) -> String { 9 | format!("ErrorKind::{:?}", error_kind) 10 | } 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | use super::*; 15 | 16 | #[test] 17 | fn unpack_fse_error() { 18 | assert_eq!( 19 | super::unpack_fse_error(Error::new(ErrorKind::Other, "test")), 20 | ("ErrorKind::Other".into(), "test".into()) 21 | ); 22 | } 23 | 24 | #[test] 25 | fn debug_fse_error_kind() { 26 | assert_eq!( 27 | super::debug_fse_error_kind(ErrorKind::Other), 28 | "ErrorKind::Other" 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/common/utils.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error, ErrorKind, Result}; 2 | 3 | static mut STATIC_STRINGS: Vec = Vec::new(); 4 | 5 | pub fn into_static_str(value: String) -> &'static str { 6 | unsafe { 7 | // Well, this is ugly but the current usage should be actually safe: 8 | // 1) It's used only by cli.rs to generate static strings for clap attributes. 9 | // 2) Values are never modified after being pushed to vector. 10 | // 3) Vectors is only modified / accessed by a single thread. 11 | STATIC_STRINGS.push(value); 12 | STATIC_STRINGS 13 | .last() 14 | .expect("Expected at least one static string result") 15 | .as_str() 16 | } 17 | } 18 | 19 | pub fn str_from_utf8(data: &[u8]) -> Result<&str> { 20 | match std::str::from_utf8(data) { 21 | Ok(str) => Ok(str), 22 | Err(error) => Err(Error::new( 23 | ErrorKind::InvalidData, 24 | format!( 25 | "Value does not have UTF-8 encoding (offset {})", 26 | error.valid_up_to() 27 | ), 28 | )), 29 | } 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use super::*; 35 | 36 | #[test] // Do not use test_case, this is expected to work only for a single thread. 37 | fn into_static_str() { 38 | let str_1 = super::into_static_str("a".into()); 39 | let str_2 = super::into_static_str("ab".into()); 40 | let str_3 = super::into_static_str("abc".into()); 41 | 42 | assert_eq!(str_1, "a"); 43 | assert_eq!(str_2, "ab"); 44 | assert_eq!(str_3, "abc"); 45 | } 46 | 47 | mod str_from_utf8 { 48 | use super::*; 49 | use crate::testing::unpack_io_error; 50 | 51 | #[test] 52 | fn ok() { 53 | assert_eq!( 54 | str_from_utf8(&[b'a', b'b', b'c'][..]).map_err(unpack_io_error), 55 | Ok("abc") 56 | ); 57 | } 58 | 59 | #[test] 60 | fn err() { 61 | assert_eq!( 62 | str_from_utf8(&[0, 159, 146, 150][..]).map_err(unpack_io_error), 63 | Err(( 64 | ErrorKind::InvalidData, 65 | "Value does not have UTF-8 encoding (offset 1)".into() 66 | )) 67 | ); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/cpb.rs: -------------------------------------------------------------------------------- 1 | #[path = "utils.rs"] 2 | mod utils; 3 | 4 | use assert_fs::prelude::*; 5 | use predicates::prelude::*; 6 | use utils::{cpb, temp_dir, write}; 7 | 8 | #[test] 9 | fn no_input() { 10 | cpb().assert().success(); 11 | } 12 | 13 | mod input_terminator { 14 | use super::*; 15 | 16 | #[test] 17 | fn line() { 18 | let dir = temp_dir(); 19 | 20 | let src_file = write(dir.child("a"), "1"); 21 | let dst_file = dir.child("b"); 22 | 23 | cpb() 24 | .current_dir(dir.path()) 25 | .write_stdin("b") 26 | .assert() 27 | .success() 28 | .stdout("") 29 | .stderr(""); 30 | 31 | src_file.assert("1"); 32 | dst_file.assert("1"); 33 | } 34 | 35 | #[test] 36 | fn null() { 37 | let dir = temp_dir(); 38 | 39 | let src_file = write(dir.child("a"), "1"); 40 | let dst_file = dir.child("b"); 41 | 42 | cpb() 43 | .current_dir(dir.path()) 44 | .arg("--read-nul") 45 | .write_stdin("b") 46 | .assert() 47 | .success() 48 | .stdout("") 49 | .stderr(""); 50 | 51 | src_file.assert("1"); 52 | dst_file.assert("1"); 53 | } 54 | } 55 | 56 | mod failure { 57 | use super::*; 58 | 59 | #[test] 60 | fn immediate() { 61 | let dir = temp_dir(); 62 | 63 | let src_file_1 = dir.child("a1"); 64 | let src_file_2 = write(dir.child("a2"), "2"); 65 | 66 | let dst_file_1 = dir.child("b1"); 67 | let dst_file_2 = dir.child("b2"); 68 | 69 | cpb() 70 | .current_dir(dir.path()) 71 | .write_stdin("b1\nb2") 72 | .assert() 73 | .failure() 74 | .code(1) 75 | .stdout("") 76 | .stderr("error: Path 'a1' not found or user lacks permission\n"); 77 | 78 | src_file_1.assert(predicates::path::missing()); 79 | src_file_2.assert("2"); 80 | 81 | dst_file_1.assert(predicates::path::missing()); 82 | dst_file_2.assert(predicates::path::missing()); 83 | } 84 | 85 | #[test] 86 | fn at_end() { 87 | let dir = temp_dir(); 88 | 89 | let src_file_1 = dir.child("a1"); 90 | let src_file_2 = write(dir.child("a2"), "2"); 91 | 92 | let dst_file_1 = dir.child("b1"); 93 | let dst_file_2 = dir.child("b2"); 94 | 95 | cpb() 96 | .current_dir(dir.path()) 97 | .arg("--fail-at-end") 98 | .write_stdin("b1\nb2") 99 | .assert() 100 | .failure() 101 | .code(1) 102 | .stdout("") 103 | .stderr("error: Path 'a1' not found or user lacks permission\n"); 104 | 105 | src_file_1.assert(predicates::path::missing()); 106 | src_file_2.assert("2"); 107 | 108 | dst_file_1.assert(predicates::path::missing()); 109 | dst_file_2.assert("2"); 110 | } 111 | } 112 | 113 | mod verbose { 114 | use super::*; 115 | 116 | #[test] 117 | fn success() { 118 | let dir = temp_dir(); 119 | 120 | let src_file = write(dir.child("a"), "1"); 121 | let dst_file = dir.child("b"); 122 | 123 | cpb() 124 | .current_dir(dir.path()) 125 | .arg("--verbose") 126 | .write_stdin("b") 127 | .assert() 128 | .success() 129 | .stdout("Copying 'a' to 'b' ... OK\n") 130 | .stderr(""); 131 | 132 | src_file.assert(predicates::path::is_file()); 133 | src_file.assert("1"); 134 | 135 | dst_file.assert(predicates::path::is_file()); 136 | dst_file.assert("1"); 137 | } 138 | 139 | mod failure { 140 | use super::*; 141 | 142 | #[test] 143 | fn immediate() { 144 | let dir = temp_dir(); 145 | 146 | let src_file_1 = dir.child("a1"); 147 | let src_file_2 = write(dir.child("a2"), "2"); 148 | 149 | let dst_file_1 = dir.child("b1"); 150 | let dst_file_2 = dir.child("b2"); 151 | 152 | cpb() 153 | .current_dir(dir.path()) 154 | .arg("--verbose") 155 | .write_stdin("b1\nb2") 156 | .assert() 157 | .failure() 158 | .code(1) 159 | .stdout("Copying 'a1' to 'b1' ... FAILED\n") 160 | .stderr("error: Path 'a1' not found or user lacks permission\n"); 161 | 162 | src_file_1.assert(predicates::path::missing()); 163 | src_file_2.assert("2"); 164 | 165 | dst_file_1.assert(predicates::path::missing()); 166 | dst_file_2.assert(predicates::path::missing()); 167 | } 168 | 169 | #[test] 170 | fn at_end() { 171 | let dir = temp_dir(); 172 | 173 | let src_file_1 = dir.child("a1"); 174 | let src_file_2 = write(dir.child("a2"), "2"); 175 | 176 | let dst_file_1 = dir.child("b1"); 177 | let dst_file_2 = dir.child("b2"); 178 | 179 | cpb() 180 | .current_dir(dir.path()) 181 | .arg("--verbose") 182 | .arg("--fail-at-end") 183 | .write_stdin("b1\nb2") 184 | .assert() 185 | .failure() 186 | .code(1) 187 | .stdout("Copying 'a1' to 'b1' ... FAILED\nCopying 'a2' to 'b2' ... OK\n") 188 | .stderr("error: Path 'a1' not found or user lacks permission\n"); 189 | 190 | src_file_1.assert(predicates::path::missing()); 191 | src_file_2.assert("2"); 192 | 193 | dst_file_1.assert(predicates::path::missing()); 194 | dst_file_2.assert("2"); 195 | } 196 | } 197 | } 198 | 199 | #[test] 200 | fn help() { 201 | cpb() 202 | .arg("--help") 203 | .assert() 204 | .success() 205 | .stdout(predicate::str::is_empty().not()) 206 | .stderr(""); 207 | } 208 | -------------------------------------------------------------------------------- /tests/mvb.rs: -------------------------------------------------------------------------------- 1 | #[path = "utils.rs"] 2 | mod utils; 3 | 4 | use assert_fs::prelude::*; 5 | use predicates::prelude::*; 6 | use utils::{mvb, temp_dir, write}; 7 | 8 | #[test] 9 | fn no_input() { 10 | mvb().assert().success(); 11 | } 12 | 13 | mod input_terminator { 14 | use super::*; 15 | 16 | #[test] 17 | fn line() { 18 | let dir = temp_dir(); 19 | 20 | let src_file = write(dir.child("a"), "1"); 21 | let dst_file = dir.child("b"); 22 | 23 | mvb() 24 | .current_dir(dir.path()) 25 | .write_stdin("b") 26 | .assert() 27 | .success() 28 | .stdout("") 29 | .stderr(""); 30 | 31 | src_file.assert(predicates::path::missing()); 32 | dst_file.assert("1"); 33 | } 34 | 35 | #[test] 36 | fn null() { 37 | let dir = temp_dir(); 38 | 39 | let src_file = write(dir.child("a"), "1"); 40 | let dst_file = dir.child("b"); 41 | 42 | mvb() 43 | .current_dir(dir.path()) 44 | .arg("--read-nul") 45 | .write_stdin("b") 46 | .assert() 47 | .success() 48 | .stdout("") 49 | .stderr(""); 50 | 51 | src_file.assert(predicates::path::missing()); 52 | dst_file.assert("1"); 53 | } 54 | } 55 | 56 | mod failure { 57 | use super::*; 58 | 59 | #[test] 60 | fn immediate() { 61 | let dir = temp_dir(); 62 | 63 | let src_file_1 = dir.child("a1"); 64 | let src_file_2 = write(dir.child("a2"), "2"); 65 | 66 | let dst_file_1 = dir.child("b1"); 67 | let dst_file_2 = dir.child("b2"); 68 | 69 | mvb() 70 | .current_dir(dir.path()) 71 | .write_stdin("b1\nb2") 72 | .assert() 73 | .failure() 74 | .code(1) 75 | .stdout("") 76 | .stderr("error: Path 'a1' not found or user lacks permission\n"); 77 | 78 | src_file_1.assert(predicates::path::missing()); 79 | src_file_2.assert("2"); 80 | 81 | dst_file_1.assert(predicates::path::missing()); 82 | dst_file_2.assert(predicates::path::missing()); 83 | } 84 | 85 | #[test] 86 | fn at_end() { 87 | let dir = temp_dir(); 88 | 89 | let src_file_1 = dir.child("a1"); 90 | let src_file_2 = write(dir.child("a2"), "2"); 91 | 92 | let dst_file_1 = dir.child("b1"); 93 | let dst_file_2 = dir.child("b2"); 94 | 95 | mvb() 96 | .current_dir(dir.path()) 97 | .arg("--fail-at-end") 98 | .write_stdin("b1\nb2") 99 | .assert() 100 | .failure() 101 | .code(1) 102 | .stdout("") 103 | .stderr("error: Path 'a1' not found or user lacks permission\n"); 104 | 105 | src_file_1.assert(predicates::path::missing()); 106 | src_file_2.assert(predicates::path::missing()); 107 | 108 | dst_file_1.assert(predicates::path::missing()); 109 | dst_file_2.assert("2"); 110 | } 111 | } 112 | 113 | mod verbose { 114 | use super::*; 115 | 116 | #[test] 117 | fn success() { 118 | let dir = temp_dir(); 119 | 120 | let src_file = write(dir.child("a"), "1"); 121 | let dst_file = dir.child("b"); 122 | 123 | mvb() 124 | .current_dir(dir.path()) 125 | .arg("--verbose") 126 | .write_stdin("b") 127 | .assert() 128 | .success() 129 | .stdout("Moving 'a' to 'b' ... OK\n") 130 | .stderr(""); 131 | 132 | src_file.assert(predicates::path::missing()); 133 | dst_file.assert("1"); 134 | } 135 | 136 | mod failure { 137 | use super::*; 138 | 139 | #[test] 140 | fn immediate() { 141 | let dir = temp_dir(); 142 | 143 | let src_file_1 = dir.child("a1"); 144 | let src_file_2 = write(dir.child("a2"), "2"); 145 | 146 | let dst_file_1 = dir.child("b1"); 147 | let dst_file_2 = dir.child("b2"); 148 | 149 | mvb() 150 | .current_dir(dir.path()) 151 | .arg("--verbose") 152 | .write_stdin("b1\nb2") 153 | .assert() 154 | .failure() 155 | .code(1) 156 | .stdout("Moving 'a1' to 'b1' ... FAILED\n") 157 | .stderr("error: Path 'a1' not found or user lacks permission\n"); 158 | 159 | src_file_1.assert(predicates::path::missing()); 160 | src_file_2.assert("2"); 161 | 162 | dst_file_1.assert(predicates::path::missing()); 163 | dst_file_2.assert(predicates::path::missing()); 164 | } 165 | 166 | #[test] 167 | fn at_end() { 168 | let dir = temp_dir(); 169 | 170 | let src_file_1 = dir.child("a1"); 171 | let src_file_2 = write(dir.child("a2"), "2"); 172 | 173 | let dst_file_1 = dir.child("b1"); 174 | let dst_file_2 = dir.child("b2"); 175 | 176 | mvb() 177 | .current_dir(dir.path()) 178 | .arg("--verbose") 179 | .arg("--fail-at-end") 180 | .write_stdin("b1\nb2") 181 | .assert() 182 | .failure() 183 | .code(1) 184 | .stdout("Moving 'a1' to 'b1' ... FAILED\nMoving 'a2' to 'b2' ... OK\n") 185 | .stderr("error: Path 'a1' not found or user lacks permission\n"); 186 | 187 | src_file_1.assert(predicates::path::missing()); 188 | src_file_2.assert(predicates::path::missing()); 189 | 190 | dst_file_1.assert(predicates::path::missing()); 191 | dst_file_2.assert("2"); 192 | } 193 | } 194 | } 195 | 196 | #[test] 197 | fn help() { 198 | mvb() 199 | .arg("--help") 200 | .assert() 201 | .success() 202 | .stdout(predicate::str::is_empty().not()) 203 | .stderr(""); 204 | } 205 | -------------------------------------------------------------------------------- /tests/rew_cpb.rs: -------------------------------------------------------------------------------- 1 | #[path = "utils.rs"] 2 | mod utils; 3 | 4 | use assert_fs::prelude::*; 5 | use utils::{cpb, rew, temp_dir, write}; 6 | 7 | #[test] 8 | fn test() { 9 | let dir = temp_dir(); 10 | let src_file = write(dir.child("a"), "1"); 11 | let dst_file = dir.child("b"); 12 | 13 | let rew = rew() 14 | .arg("--diff") 15 | .arg("b") 16 | .write_stdin("a") 17 | .output() 18 | .unwrap(); 19 | 20 | cpb() 21 | .current_dir(dir.path()) 22 | .arg("--verbose") 23 | .write_stdin(rew.stdout) 24 | .assert() 25 | .stdout("Copying 'a' to 'b' ... OK\n") 26 | .stderr(""); 27 | 28 | src_file.assert("1"); 29 | dst_file.assert("1"); 30 | } 31 | -------------------------------------------------------------------------------- /tests/rew_mvb.rs: -------------------------------------------------------------------------------- 1 | #[path = "utils.rs"] 2 | mod utils; 3 | 4 | use assert_fs::prelude::*; 5 | use utils::{mvb, rew, temp_dir, write}; 6 | 7 | #[test] 8 | fn test() { 9 | let dir = temp_dir(); 10 | let src_file = write(dir.child("a"), "1"); 11 | let dst_file = dir.child("b"); 12 | 13 | let rew = rew() 14 | .arg("--diff") 15 | .arg("b") 16 | .write_stdin("a") 17 | .output() 18 | .unwrap(); 19 | 20 | mvb() 21 | .current_dir(dir.path()) 22 | .arg("--verbose") 23 | .write_stdin(rew.stdout) 24 | .assert() 25 | .stdout("Moving 'a' to 'b' ... OK\n") 26 | .stderr(""); 27 | 28 | src_file.assert(predicates::path::missing()); 29 | dst_file.assert("1"); 30 | } 31 | -------------------------------------------------------------------------------- /tests/utils.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | use assert_fs::fixture::FileWriteStr; 3 | use assert_fs::TempDir; 4 | 5 | #[allow(dead_code)] 6 | pub fn rew() -> Command { 7 | command("rew") 8 | } 9 | 10 | #[allow(dead_code)] 11 | pub fn cpb() -> Command { 12 | command("cpb") 13 | } 14 | 15 | #[allow(dead_code)] 16 | pub fn mvb() -> Command { 17 | command("mvb") 18 | } 19 | 20 | pub fn command(name: &str) -> Command { 21 | Command::cargo_bin(name).unwrap() 22 | } 23 | 24 | #[allow(dead_code)] 25 | pub fn temp_dir() -> TempDir { 26 | TempDir::new().unwrap() 27 | } 28 | 29 | #[allow(dead_code)] 30 | pub fn write(file: F, data: &str) -> F { 31 | file.write_str(data).unwrap(); 32 | file 33 | } 34 | --------------------------------------------------------------------------------