├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.txt ├── Makefile ├── README.md ├── build.rs ├── demo.gif ├── docs ├── Developing.md ├── Packaging.md ├── ShellIntegrations.md ├── UserManual.md └── ea.1 ├── flake.lock ├── flake.nix ├── resources └── fixtures │ └── parsers │ ├── grouped.in.txt │ ├── grouped.out.txt │ ├── grouped2.in.txt │ ├── grouped2.out.txt │ ├── grouped2_locations.bin │ ├── grouped3.in.txt │ ├── grouped3.out.txt │ ├── grouped3_locations.bin │ ├── grouped_locations.bin │ ├── ipython3.in.txt │ ├── ipython3.out.txt │ ├── ipython3_locations.bin │ ├── linear.in.txt │ ├── linear.out.txt │ ├── linear_colored.in.txt │ ├── linear_colored.out.txt │ ├── linear_colored_locations.bin │ ├── linear_locations.bin │ ├── python3.in.txt │ ├── python3.out.txt │ ├── python3_locations.bin │ ├── rust.in.txt │ ├── rust.out.txt │ ├── rust_locations.bin │ ├── search.in.txt │ ├── search.out.txt │ └── search_locations.bin ├── scripts ├── check-version.py ├── completion │ ├── _ea │ ├── _ea.ps1 │ ├── ea.bash │ ├── ea.elv │ └── ea.fish └── pandoc-docker ├── src ├── archive.rs ├── bin │ ├── ea.rs │ └── printfile.rs ├── commands.rs ├── commands │ ├── list.rs │ ├── print.rs │ └── run.rs ├── interface.rs ├── lib.rs ├── parsers.rs └── parsers │ ├── grouped.rs │ ├── linear.rs │ ├── python.rs │ ├── rust.rs │ └── search.rs └── tests └── intergation_tests.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: [push] 4 | 5 | jobs: 6 | Nix: 7 | name: Nix 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2.4.0 11 | - uses: cachix/install-nix-action@v15 12 | with: 13 | nix_path: nixpkgs=channel:nixos-unstable 14 | - name: Build 15 | run: nix build && git diff --exit-code 16 | 17 | macOS: 18 | name: macOS 19 | runs-on: macOS-latest 20 | strategy: 21 | matrix: 22 | action: 23 | - check 24 | - build 25 | - test 26 | - cargo-publish 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Action 30 | run: make ${{ matrix.action }} 31 | - name: Check Carge.lock 32 | run: git diff --exit-code 33 | 34 | Ubuntu: 35 | name: Ubuntu 36 | runs-on: ubuntu-latest 37 | strategy: 38 | matrix: 39 | action: 40 | - check 41 | - build 42 | - test 43 | - cargo-publish 44 | - check-version 45 | steps: 46 | - uses: actions/checkout@v2 47 | - name: Action 48 | run: make ${{ matrix.action }} 49 | 50 | #Windows: 51 | # runs-on: windows-2019 52 | # strategy: 53 | # matrix: 54 | # action: 55 | # - check 56 | # - build 57 | # - test 58 | # - cargo-publish 59 | # steps: 60 | # - uses: actions/checkout@v2 61 | # - name: Action 62 | # run: make ${{ matrix.action }} 63 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # The way this works is a little weird. But basically, the create-release job 2 | # runs purely to initialize the GitHub release itself. Once done, the upload 3 | # URL of the release is saved as an artifact. 4 | # 5 | # The build-release job runs only once create-release is finished. It gets 6 | # the release upload URL by downloading the corresponding artifact (which was 7 | # uploaded by create-release). It then builds the release executables for each 8 | # supported platform and attaches them as release assets to the previously 9 | # created release. 10 | # 11 | # The key here is that we create the release only once. 12 | 13 | name: release 14 | on: 15 | push: 16 | tags: 17 | - '*' 18 | jobs: 19 | create-release: 20 | name: create-release 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Create artifacts directory 24 | run: mkdir artifacts 25 | 26 | - name: Get the release version from the tag 27 | if: env.SL_VERSION == '' 28 | run: | 29 | # Apparently, this is the right way to get a tag name. Really? 30 | # 31 | # See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 32 | echo "SL_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 33 | echo "version is: ${{ env.SL_VERSION }}" 34 | 35 | - name: Create GitHub release 36 | id: release 37 | uses: actions/create-release@v1 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | with: 41 | tag_name: ${{ env.SL_VERSION }} 42 | release_name: ${{ env.SL_VERSION }} 43 | 44 | - name: Save release upload URL to artifact 45 | run: echo "${{ steps.release.outputs.upload_url }}" > artifacts/release-upload-url 46 | 47 | - name: Save version number to artifact 48 | run: echo "${{ env.SL_VERSION }}" > artifacts/release-version 49 | 50 | - name: Upload artifacts 51 | uses: actions/upload-artifact@v1 52 | with: 53 | name: artifacts 54 | path: artifacts 55 | 56 | build-release: 57 | name: build-release 58 | needs: ['create-release'] 59 | runs-on: ${{ matrix.os }} 60 | env: 61 | # For some builds, we use cross to test on 32-bit and big-endian 62 | # systems. 63 | CARGO: cargo 64 | # When CARGO is set to CROSS, this is set to `--target matrix.target`. 65 | TARGET_FLAGS: 66 | # When CARGO is set to CROSS, TARGET_DIR includes matrix.target. 67 | TARGET_DIR: ./target 68 | # Emit backtraces on panics. 69 | RUST_BACKTRACE: 1 70 | strategy: 71 | matrix: 72 | #build: [linux, linux-arm, macos, win-msvc, win-gnu, win32-msvc] 73 | build: [linux, linux-arm, macos-arm] 74 | include: 75 | - build: linux 76 | os: ubuntu-18.04 77 | rust: stable 78 | target: x86_64-unknown-linux-musl 79 | - build: linux-arm 80 | os: ubuntu-18.04 81 | rust: stable 82 | target: arm-unknown-linux-gnueabihf 83 | - build: macos 84 | os: macos-latest 85 | rust: stable 86 | target: x86_64-apple-darwin 87 | - build: macos-arm 88 | os: macos-latest 89 | rust: stable 90 | target: aarch64-apple-darwin 91 | #- build: win-msvc 92 | # os: windows-2019 93 | # rust: stable 94 | # target: x86_64-pc-windows-msvc 95 | #- build: win-gnu 96 | # os: windows-2019 97 | # rust: stable-x86_64-gnu 98 | # target: x86_64-pc-windows-gnu 99 | #- build: win32-msvc 100 | # os: windows-2019 101 | # rust: stable 102 | # target: i686-pc-windows-msvc 103 | 104 | steps: 105 | - name: Checkout repository 106 | uses: actions/checkout@v2 107 | with: 108 | fetch-depth: 1 109 | 110 | - name: Install packages (Ubuntu) 111 | if: matrix.os == 'ubuntu-18.04' 112 | run: | 113 | sudo apt-get update 114 | sudo apt-get install -y --no-install-recommends xz-utils liblz4-tool musl-tools 115 | 116 | - name: Install Rust 117 | uses: actions-rs/toolchain@v1 118 | with: 119 | toolchain: ${{ matrix.rust }} 120 | profile: minimal 121 | override: true 122 | target: ${{ matrix.target }} 123 | 124 | - name: Use Cross 125 | # if: matrix.os != 'windows-2019' 126 | run: | 127 | cargo install --version 0.2.4 cross 128 | echo "CARGO=cross" >> $GITHUB_ENV 129 | echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV 130 | echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV 131 | 132 | - name: Show command used for Cargo 133 | run: | 134 | echo "cargo command is: ${{ env.CARGO }}" 135 | echo "target flag is: ${{ env.TARGET_FLAGS }}" 136 | echo "target dir is: ${{ env.TARGET_DIR }}" 137 | 138 | - name: Get release download URL 139 | uses: actions/download-artifact@v1 140 | with: 141 | name: artifacts 142 | path: artifacts 143 | 144 | - name: Set release upload URL and release version 145 | shell: bash 146 | run: | 147 | release_upload_url="$(cat artifacts/release-upload-url)" 148 | echo "RELEASE_UPLOAD_URL=$release_upload_url" >> $GITHUB_ENV 149 | echo "release upload url: $RELEASE_UPLOAD_URL" 150 | release_version="$(cat artifacts/release-version)" 151 | echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV 152 | echo "release version: $RELEASE_VERSION" 153 | 154 | - name: Build release binary 155 | run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }} 156 | 157 | - name: Strip release binary (linux and macos) 158 | if: matrix.build == 'linux' || matrix.build == 'macos' || matrix.build == 'macos-arm' 159 | run: strip "target/${{ matrix.target }}/release/ea" 160 | 161 | - name: Strip release binary (arm) 162 | if: matrix.build == 'linux-arm' 163 | run: | 164 | docker run --rm -v \ 165 | "$PWD/target:/target:Z" \ 166 | rustembedded/cross:arm-unknown-linux-gnueabihf \ 167 | arm-linux-gnueabihf-strip \ 168 | /target/arm-unknown-linux-gnueabihf/release/ea 169 | 170 | - name: Build archive 171 | shell: bash 172 | run: | 173 | outdir=${{ env.TARGET_DIR }} 174 | staging="ea-${{ env.RELEASE_VERSION }}-${{ matrix.target }}" 175 | 176 | if [ "${{ matrix.os }}" = "windows-2019" ]; then 177 | 7z a "$staging.zip" "./target/release/ea.exe" 178 | echo "ASSET=$staging.zip" >> $GITHUB_ENV 179 | else 180 | tar -C "target/${{ matrix.target }}/release" -cf "$staging.tar" ea 181 | #tar -C "manual" -rf "$staging.tar" ea.1 182 | gzip "$staging.tar" 183 | echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV 184 | fi 185 | 186 | - name: Upload release archive 187 | uses: actions/upload-release-asset@v1.0.1 188 | env: 189 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 190 | with: 191 | upload_url: ${{ env.RELEASE_UPLOAD_URL }} 192 | asset_path: ${{ env.ASSET }} 193 | asset_name: ${{ env.ASSET }} 194 | asset_content_type: application/octet-stream 195 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /result 3 | /.vscode 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # main 2 | 3 | # 0.2.1 4 | 5 | * Empty result won't override recorded path from previous runs. 6 | 7 | # 0.2.0 8 | 9 | * Add `ea run rust` for Rust tools such as `rustc` and `clippy`. 10 | * Add `ea run python` for python stack traces. 11 | 12 | # 0.1.0 13 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "assert_cmd" 16 | version = "2.0.4" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e" 19 | dependencies = [ 20 | "bstr", 21 | "doc-comment", 22 | "predicates", 23 | "predicates-core", 24 | "predicates-tree", 25 | "wait-timeout", 26 | ] 27 | 28 | [[package]] 29 | name = "atty" 30 | version = "0.2.14" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 33 | dependencies = [ 34 | "hermit-abi", 35 | "libc", 36 | "winapi 0.3.9", 37 | ] 38 | 39 | [[package]] 40 | name = "autocfg" 41 | version = "1.1.0" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 44 | 45 | [[package]] 46 | name = "bincode" 47 | version = "1.3.3" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 50 | dependencies = [ 51 | "serde", 52 | ] 53 | 54 | [[package]] 55 | name = "bitflags" 56 | version = "1.3.2" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 59 | 60 | [[package]] 61 | name = "bstr" 62 | version = "0.2.17" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" 65 | dependencies = [ 66 | "lazy_static", 67 | "memchr", 68 | "regex-automata", 69 | ] 70 | 71 | [[package]] 72 | name = "clap" 73 | version = "3.1.12" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "7c167e37342afc5f33fd87bbc870cedd020d2a6dffa05d45ccd9241fbdd146db" 76 | dependencies = [ 77 | "atty", 78 | "bitflags", 79 | "clap_derive", 80 | "clap_lex", 81 | "indexmap", 82 | "lazy_static", 83 | "strsim", 84 | "termcolor", 85 | "textwrap", 86 | ] 87 | 88 | [[package]] 89 | name = "clap_complete" 90 | version = "3.1.2" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "1506b87ee866f7a53a5131f7b31fba656170d797e873d0609884cfd56b8bbda8" 93 | dependencies = [ 94 | "clap", 95 | ] 96 | 97 | [[package]] 98 | name = "clap_derive" 99 | version = "3.1.7" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1" 102 | dependencies = [ 103 | "heck", 104 | "proc-macro-error", 105 | "proc-macro2", 106 | "quote", 107 | "syn", 108 | ] 109 | 110 | [[package]] 111 | name = "clap_lex" 112 | version = "0.1.1" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "189ddd3b5d32a70b35e7686054371742a937b0d99128e76dde6340210e966669" 115 | dependencies = [ 116 | "os_str_bytes", 117 | ] 118 | 119 | [[package]] 120 | name = "difflib" 121 | version = "0.4.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 124 | 125 | [[package]] 126 | name = "doc-comment" 127 | version = "0.3.3" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 130 | 131 | [[package]] 132 | name = "ea-command" 133 | version = "0.2.1" 134 | dependencies = [ 135 | "assert_cmd", 136 | "atty", 137 | "bincode", 138 | "clap", 139 | "clap_complete", 140 | "guard", 141 | "lazy_static", 142 | "pty", 143 | "regex", 144 | "serde", 145 | ] 146 | 147 | [[package]] 148 | name = "either" 149 | version = "1.6.1" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 152 | 153 | [[package]] 154 | name = "errno" 155 | version = "0.1.8" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "1e2b2decb0484e15560df3210cf0d78654bb0864b2c138977c07e377a1bae0e2" 158 | dependencies = [ 159 | "kernel32-sys", 160 | "libc", 161 | "winapi 0.2.8", 162 | ] 163 | 164 | [[package]] 165 | name = "guard" 166 | version = "0.5.1" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "ff893cc51ea04f8a3b73fbaf4376c06ebc5a0ccbe86d460896f805d9417c93ea" 169 | 170 | [[package]] 171 | name = "hashbrown" 172 | version = "0.11.2" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 175 | 176 | [[package]] 177 | name = "heck" 178 | version = "0.4.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 181 | 182 | [[package]] 183 | name = "hermit-abi" 184 | version = "0.1.19" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 187 | dependencies = [ 188 | "libc", 189 | ] 190 | 191 | [[package]] 192 | name = "indexmap" 193 | version = "1.8.1" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" 196 | dependencies = [ 197 | "autocfg", 198 | "hashbrown", 199 | ] 200 | 201 | [[package]] 202 | name = "itertools" 203 | version = "0.10.3" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" 206 | dependencies = [ 207 | "either", 208 | ] 209 | 210 | [[package]] 211 | name = "kernel32-sys" 212 | version = "0.2.2" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 215 | dependencies = [ 216 | "winapi 0.2.8", 217 | "winapi-build", 218 | ] 219 | 220 | [[package]] 221 | name = "lazy_static" 222 | version = "1.4.0" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 225 | 226 | [[package]] 227 | name = "libc" 228 | version = "0.2.117" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" 231 | 232 | [[package]] 233 | name = "memchr" 234 | version = "2.4.1" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 237 | 238 | [[package]] 239 | name = "os_str_bytes" 240 | version = "6.0.0" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" 243 | 244 | [[package]] 245 | name = "predicates" 246 | version = "2.1.1" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" 249 | dependencies = [ 250 | "difflib", 251 | "itertools", 252 | "predicates-core", 253 | ] 254 | 255 | [[package]] 256 | name = "predicates-core" 257 | version = "1.0.3" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" 260 | 261 | [[package]] 262 | name = "predicates-tree" 263 | version = "1.0.5" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" 266 | dependencies = [ 267 | "predicates-core", 268 | "termtree", 269 | ] 270 | 271 | [[package]] 272 | name = "proc-macro-error" 273 | version = "1.0.4" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 276 | dependencies = [ 277 | "proc-macro-error-attr", 278 | "proc-macro2", 279 | "quote", 280 | "syn", 281 | "version_check", 282 | ] 283 | 284 | [[package]] 285 | name = "proc-macro-error-attr" 286 | version = "1.0.4" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 289 | dependencies = [ 290 | "proc-macro2", 291 | "quote", 292 | "version_check", 293 | ] 294 | 295 | [[package]] 296 | name = "proc-macro2" 297 | version = "1.0.37" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" 300 | dependencies = [ 301 | "unicode-xid", 302 | ] 303 | 304 | [[package]] 305 | name = "pty" 306 | version = "0.2.2" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "f50f3d255966981eb4e4c5df3e983e6f7d163221f547406d83b6a460ff5c5ee8" 309 | dependencies = [ 310 | "errno", 311 | "libc", 312 | ] 313 | 314 | [[package]] 315 | name = "quote" 316 | version = "1.0.17" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58" 319 | dependencies = [ 320 | "proc-macro2", 321 | ] 322 | 323 | [[package]] 324 | name = "regex" 325 | version = "1.5.5" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" 328 | dependencies = [ 329 | "aho-corasick", 330 | "memchr", 331 | "regex-syntax", 332 | ] 333 | 334 | [[package]] 335 | name = "regex-automata" 336 | version = "0.1.10" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 339 | 340 | [[package]] 341 | name = "regex-syntax" 342 | version = "0.6.25" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 345 | 346 | [[package]] 347 | name = "serde" 348 | version = "1.0.136" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" 351 | dependencies = [ 352 | "serde_derive", 353 | ] 354 | 355 | [[package]] 356 | name = "serde_derive" 357 | version = "1.0.136" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" 360 | dependencies = [ 361 | "proc-macro2", 362 | "quote", 363 | "syn", 364 | ] 365 | 366 | [[package]] 367 | name = "strsim" 368 | version = "0.10.0" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 371 | 372 | [[package]] 373 | name = "syn" 374 | version = "1.0.91" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" 377 | dependencies = [ 378 | "proc-macro2", 379 | "quote", 380 | "unicode-xid", 381 | ] 382 | 383 | [[package]] 384 | name = "termcolor" 385 | version = "1.1.3" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 388 | dependencies = [ 389 | "winapi-util", 390 | ] 391 | 392 | [[package]] 393 | name = "termtree" 394 | version = "0.2.4" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" 397 | 398 | [[package]] 399 | name = "textwrap" 400 | version = "0.15.0" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 403 | 404 | [[package]] 405 | name = "unicode-xid" 406 | version = "0.2.2" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 409 | 410 | [[package]] 411 | name = "version_check" 412 | version = "0.9.4" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 415 | 416 | [[package]] 417 | name = "wait-timeout" 418 | version = "0.2.0" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 421 | dependencies = [ 422 | "libc", 423 | ] 424 | 425 | [[package]] 426 | name = "winapi" 427 | version = "0.2.8" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 430 | 431 | [[package]] 432 | name = "winapi" 433 | version = "0.3.9" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 436 | dependencies = [ 437 | "winapi-i686-pc-windows-gnu", 438 | "winapi-x86_64-pc-windows-gnu", 439 | ] 440 | 441 | [[package]] 442 | name = "winapi-build" 443 | version = "0.1.1" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 446 | 447 | [[package]] 448 | name = "winapi-i686-pc-windows-gnu" 449 | version = "0.4.0" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 452 | 453 | [[package]] 454 | name = "winapi-util" 455 | version = "0.1.5" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 458 | dependencies = [ 459 | "winapi 0.3.9", 460 | ] 461 | 462 | [[package]] 463 | name = "winapi-x86_64-pc-windows-gnu" 464 | version = "0.4.0" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 467 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ea-command" 3 | version = "0.2.1" 4 | license = "MIT" 5 | authors = ["Daniel Duan "] 6 | edition = "2018" 7 | description = "Editor Alias: making paths in command line tools actionable." 8 | documentation = "https://github.com/dduan/ea/blob/master/README.md" 9 | exclude = ["/.github/*", "demo.gif", "flake.nix", "flake.lock", "/scripts/*", "/resources/*", "src/bin/printfile.rs"] 10 | readme = "README.md" 11 | homepage = "https://github.com/dduan/ea" 12 | repository = "https://github.com/dduan/ea" 13 | keywords = ["cli"] 14 | categories = ["command-line-utilities"] 15 | default-run = "ea" 16 | 17 | [dependencies] 18 | atty = '0.2.14' 19 | bincode = '1.3.3' 20 | clap = { version = "3.1.8", features = ["derive"] } 21 | lazy_static = '1.4' 22 | pty = '0.2' 23 | regex = '1' 24 | serde = { version = "1.0", features = ["derive"] } 25 | guard = '0.5.1' 26 | 27 | [build-dependencies] 28 | clap = { version = "3.1.8", features = ["cargo"] } 29 | clap_complete = "3.1" 30 | 31 | [dev-dependencies] 32 | assert_cmd = "2.0" 33 | pty = '0.2' 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2022 ea contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PANDOC ?= pandoc 2 | 3 | .PHONY: check 4 | check: 5 | @cargo check 6 | @cargo clippy -- -D warnings 7 | 8 | .PHONY: build 9 | build: 10 | @SHELL_COMPLETIONS_DIR=scripts/completion cargo build 11 | @git diff --exit-code 12 | 13 | .PHONY: manual 14 | manual: 15 | @$(PANDOC) --standalone --to man docs/UserManual.md -o docs/ea.1 16 | 17 | .PHONY: test 18 | test: 19 | @cargo test 20 | 21 | .PHONY: cargo-publish 22 | cargo-publish: 23 | @cargo publish --dry-run 24 | 25 | .PHONY: check-version 26 | check-version: 27 | @scripts/check-version.py 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ea: Making CLI Outputs Actionable 2 | 3 | `ea` remembers file paths from CLI output for later use. 4 | 5 | ## Intro 6 | 7 | CLI programs like `find`, `rg`, `clang` etc, print out file paths (and line/columns). `ea` lets you act on 8 | them. Here's how: 9 | 10 | ![Aliasing In Action](demo.gif) 11 | 12 | By running your command through `ea`, each path in its output get marked with a number in the front. This 13 | number can be used to retrieve its corresponding path later for your purposes. Combined with some shell 14 | configurations, this can provide a powerful experience. 15 | 16 | ## Install 17 | 18 | #### Via a package manager 19 | 20 | `ea` is available in the following package managers. 21 | 22 | | Manager / OS | Command | 23 | | ------------ | -------------------------------- | 24 | | Cargo | `cargo install ea-command` | 25 | | Homebrew | `brew install dduan/formulae/ea` | 26 | | AUR / Arch | `yay -S ea-command` | 27 | | Nix flake | Use `github:dduan/ea` | 28 | 29 | ### Pre-built executable 30 | 31 | Choose an pre-built executable from the [release page][] that fits your 32 | platform to download. Unpack it somewhere you'd like to run it from. 33 | 34 | [release page]: https://github.com/dduan/ea/releases 35 | 36 | ## Documentation 37 | 38 | * [User Manual](docs/UserManual.md) 39 | * [Tutorial](docs/UserManual.md#description) 40 | * [Formats](docs/UserManual.md#formats) 41 | * [Shell Integration](docs/UserManual.md#shell-integration) 42 | * [Editor Alias](docs/ShellIntegrations.md#editor-alias) 43 | * [Note to packagers](docs/Packaging.md) 44 | * [Developing ea](docs/Developing.md) 45 | 46 | ## Credits 47 | 48 | `ea` is inspired by [keith/tag][], which was inspired by [aykamko/tag][]. 49 | 50 | [keith/tag]: https://github.com/keith/tag 51 | [aykamko/tag]: https://github.com/aykamko/tag 52 | 53 | ## LICENSE 54 | 55 | See `LICENSE.txt`. 56 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use clap::CommandFactory; 2 | use clap_complete::{generate_to, Shell}; 3 | use std::fs; 4 | use std::env; 5 | use Shell::*; 6 | 7 | include!("src/interface.rs"); 8 | 9 | fn main() { 10 | let outdir = env::var("SHELL_COMPLETIONS_DIR").or_else(|_| env::var("OUT_DIR")).unwrap(); 11 | fs::create_dir_all(&outdir).unwrap(); 12 | let mut cmd = Interface::command(); 13 | for shell in [Bash, PowerShell, Fish, Elvish, Zsh] { 14 | generate_to(shell, &mut cmd, "ea", &outdir).unwrap(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dduan/ea/5849d96cf9fd7306153eb4fc20ba4fce52ca53d7/demo.gif -------------------------------------------------------------------------------- /docs/Developing.md: -------------------------------------------------------------------------------- 1 | # Devoloping ea 2 | 3 | Although not required, we recommend using Nix flake as part of `ea`'s development process: to replicate the exact development environment as the maintainers, run `nix shell`, or adding `use flake` in an [direnv][] `.envrc` file. 4 | 5 | If you update `Cargo.toml` in any way, you will have to update `flake.nix`, this is enforced by checks in continuous integration. 6 | 7 | If you don't use Nix, see `buildInputs` in `flake.nix` for the tools necassary to the development. 8 | 9 | [direnv]: https://direnv.net 10 | -------------------------------------------------------------------------------- /docs/Packaging.md: -------------------------------------------------------------------------------- 1 | # Packager Notes 2 | 3 | `ea` is a standard, Cargo-compatible Rust package. The Rust package produces has a single executable `ea`. 4 | 5 | Completion scripts are include for the following shells: 6 | 7 | - Zsh: scripts/completion/_ea 8 | - PowerShell: scripts/completion/_ea.ps1 9 | - Bash: scripts/completion/ea.bash 10 | - Elvish: scripts/completion/ea.elv 11 | - Fish: scripts/completion/ea.fish 12 | 13 | Unix manual is located at docs/ea.1 14 | -------------------------------------------------------------------------------- /docs/ShellIntegrations.md: -------------------------------------------------------------------------------- 1 | This file shows examples of shell functions that consumes output of `ea print`. It is recommended to include them as part of your `ea` workflow. 2 | 3 | 4 | # Editor Alias 5 | 6 | This shell function enables typing `e NUM` in shell to open the path corrsponding to `NUM` in your default editor. 7 | 8 | Find the shell/editor combination you need below! 9 | 10 | | | Bash | Zsh | Fish | 11 | | ---------- | ------------------------------- | ------------------------------ | ------------------------------- | 12 | | Vim/NeoVim | [link](#bash-and-vim-or-neovim) | [link](#zsh-and-vim-or-neovim) | [link](#fish-and-vim-or-neovim) | 13 | | VSCode | [link](#bash-and-vscode) | [link](#zsh-and-vscode) | [link](#fish-and-vscode) | 14 | | Emacs | [link](#bash-and-emacs) | [link](#zsh-and-emacs) | [link](#fish-and-emacs) | 15 | | TextMate | [link](#bash-and-textmate) | [link](#zsh-and-textmate) | [link](#fish-and-textmate) | 16 | 17 | ## Bash and Vim or NeoVim 18 | 19 | ```bash 20 | e() { 21 | eval $(ea p $1 "$EDITOR '{path}' '+call cursor({line}, {column})'") 22 | } 23 | ``` 24 | 25 | ## Zsh and Vim or NeoVim 26 | 27 | ```zsh 28 | function e { 29 | eval $(ea p $1 "$EDITOR '{path}' '+call cursor({line}, {column})'") 30 | } 31 | ``` 32 | 33 | ## Fish and Vim or NeoVim 34 | 35 | ```fish 36 | function e 37 | eval (ea p $argv '$EDITOR "{path}" "+call cursor({line}, {column})"') 38 | end 39 | ``` 40 | 41 | ## Bash and VSCode 42 | 43 | ```bash 44 | e() { 45 | eval $(ea p $1 "code --goto '{path}:{line}:{column}'") 46 | } 47 | ``` 48 | 49 | ## Zsh and VSCode 50 | 51 | ```zsh 52 | function e { 53 | eval $(ea p $1 "code --goto '{path}:{line}:{column}'") 54 | } 55 | ``` 56 | 57 | ## Fish and VSCode 58 | 59 | ```fish 60 | function e 61 | eval (ea p $argv "code --goto '{path}:{line}:{column}'") 62 | end 63 | ``` 64 | 65 | ## Bash and Emacs 66 | 67 | ```bash 68 | e() { 69 | eval $(ea p $1 "emacs +{line}:{column} '{path}'") 70 | } 71 | ``` 72 | 73 | ## Zsh and Emacs 74 | 75 | ```zsh 76 | function e { 77 | eval $(ea p $1 "emacs +{line}:{column} '{path}'") 78 | } 79 | ``` 80 | 81 | ## Fish and Emacs 82 | 83 | ```fish 84 | function e 85 | eval (ea p $argv "emacs +{line}:{column} '{path}'") 86 | end 87 | ``` 88 | 89 | ## Bash and TextMate 90 | 91 | ```bash 92 | e() { 93 | eval $(ea p $1 "mate -l {line}:{column} '{path}'") 94 | } 95 | ``` 96 | 97 | ## Zsh and TextMate 98 | 99 | ```zsh 100 | function e { 101 | eval $(ea p $1 "mate -l {line}:{column} '{path}'") 102 | } 103 | ``` 104 | 105 | ## Fish and TextMate 106 | 107 | ```fish 108 | function e 109 | eval (ea p $argv "mate -l {line}:{column} '{path}'") 110 | end 111 | ``` 112 | -------------------------------------------------------------------------------- /docs/UserManual.md: -------------------------------------------------------------------------------- 1 | % EA(1) Version 0.2.1 | EA MANUAL 2 | 3 | NAME 4 | ---- 5 | 6 | `ea` -- Making paths in CLI output actionable. 7 | 8 | SYNOPSIS 9 | -------- 10 | 11 | **ea** [_options_] **run** _style_ _executable_ [\-\- _argument_...] 12 | **ea** [_options_] [**list**] 13 | **ea** [_options_] **p[rint]** [_format_] 14 | 15 | DESCRIPTION 16 | ----------- 17 | 18 | Command-line tools often prints out file paths. Often, we want to follow up with some action on files in these paths, such as viewing, copying, editing, etc. When running these commands through `ea`, the paths in output will be stored in a local database, and become available for later use. 19 | 20 | Use `ea run` to invoke your command. For example: 21 | 22 | > **ea run** _grouped_ rg \-\- Vec src 23 | 24 | ... is how you run `rg Vec src` through `ea`. Note: any argument for `rg` comes after `--`. 25 | 26 | _grouped_ hints at the format of `rg`'s output' so that `ea` knows how to find paths from `rg`'s output. This hint is necessary for `ea` to work with any many arbitrary commands as possible. See FORMATS to learn more. 27 | 28 | You'll see that file locations in the original command are now prefixed with a number: 29 | 30 | > src/something.rs 31 | > [1] 23: let list: Vec 32 | > [2] 41: fn build() -> Vec 33 | > ... 34 | 35 | `ea list`, or simply `ea`, will print out these locations along with their numbers again: 36 | 37 | > [1] src/something.rs:23 38 | > [2] src/something.rs:41 39 | > ... 40 | 41 | With the numbers, `ea` can retrieve a corresponding path. In our example, `ea print` _2_ (or `ea p` _2_ in short) results in: 42 | > src/something.rs 43 | 44 | `ea print` takes an optional second argument _format_, making it possible to retrieve the location info mixed in this _format_ string. The sequences _{path}_, _{line}_, and _{column}_ appearing in _format_ get replaced by the location info. Running `ea p` _2_ _'{path} @ {line}'_ results in 45 | 46 | > src/something.rs @ 41 47 | 48 | `ea print`'s output is expected to be used as part of a longer command, such as `vim $(ea p 2)`. 49 | 50 | It is recommended to create shell aliases or functions for frequently used `ea` commands. 51 | 52 | FORMATS 53 | ------- 54 | 55 | `ea run`'s first argument _style_ is mandatory. It indicates how file locations will appear in the output. This argument must be one of the following: `grouped`, `linear`, `search`, `rust` `python`. This section will explain what each of them means. The command you run through `ea` should have a matching _format_ value. 56 | 57 | **grouped** indicates the command's output contains one or more sections, each section begins with a file path, and lines of line/column, and possibly more content, follows. An example of this _style_ of output is `ripgrep`'s default output: 58 | 59 | > src/archive.rs 60 | > 41: pub fn read() -> Vec { 61 | > 45: pub fn read_from(path: &Path) -> Vec { 62 | > 63 | > src/interface.rs 64 | > 53: arguments: Vec, 65 | 66 | **linear** indicates each line in the command's output is a location. A location can be a file path, optionally followed by line, and, optionally, column number separated by ":" (colon). This is the default output from commands such as _find_ or _fd_: 67 | 68 | > src/archive.rs:41 69 | > src/archive.rs:45 70 | > src/interface.rs:53 71 | 72 | 73 | **search** means the output is almost arbitrary, except every now and then, an location appears at the beginning of a line. This is common in error messages from compilers like _clang_ or _swift_: 74 | 75 | > Sources/Critic/DocProblem.swift:5:26: error: cannot find type 'Stringx' in scope 76 | 77 | **rust** means the format from Rust tools such as rustc, or clippy. 78 | 79 | **python** means Python or iPython's backtrace. 80 | 81 | If you need `ea` to support more formats, please file an issue at https://github.com/dduan/ea 82 | 83 | SHELL INTEGRATION 84 | ----------------- 85 | 86 | Some shell aliases, and functions makes using `ea` more effective. 87 | 88 | First, you'll want alias your normal command to the `ea` version: 89 | 90 | ``` 91 | alias fd 'ea run linear fd --' 92 | ``` 93 | 94 | Then, optionally, make a shell function that consume a path from `ea`'s output. The following example makes `e 6` opens the 6th paths known to `ea` in your default editor in zsh/bash: 95 | 96 | ``` 97 | e() { 98 | eval $(ea p $1 "$EDITOR {path}") 99 | } 100 | ``` 101 | 102 | For more examples, see documentation at https://github.com/dduan/ea/blob/main/docs/UserManual.md 103 | 104 | AUTHOR 105 | ------ 106 | 107 | Daniel Duan 108 | -------------------------------------------------------------------------------- /docs/ea.1: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pandoc 2.17.1.1 2 | .\" 3 | .\" Define V font for inline verbatim, using C font in formats 4 | .\" that render this, and otherwise B font. 5 | .ie "\f[CB]x\f[]"x" \{\ 6 | . ftr V B 7 | . ftr VI BI 8 | . ftr VB B 9 | . ftr VBI BI 10 | .\} 11 | .el \{\ 12 | . ftr V CR 13 | . ftr VI CI 14 | . ftr VB CB 15 | . ftr VBI CBI 16 | .\} 17 | .TH "EA" "1" "" "Version 0.2.1" "EA MANUAL" 18 | .hy 19 | .SS NAME 20 | .PP 21 | \f[V]ea\f[R] \[en] Making paths in CLI output actionable. 22 | .SS SYNOPSIS 23 | .PP 24 | \f[B]ea\f[R] [\f[I]options\f[R]] \f[B]run\f[R] \f[I]style\f[R] 25 | \f[I]executable\f[R] [-- \f[I]argument\f[R]\&...] 26 | .PD 0 27 | .P 28 | .PD 29 | \f[B]ea\f[R] [\f[I]options\f[R]] [\f[B]list\f[R]] 30 | .PD 0 31 | .P 32 | .PD 33 | \f[B]ea\f[R] [\f[I]options\f[R]] \f[B]p[rint]\f[R] [\f[I]format\f[R]] 34 | .SS DESCRIPTION 35 | .PP 36 | Command-line tools often prints out file paths. 37 | Often, we want to follow up with some action on files in these paths, 38 | such as viewing, copying, editing, etc. 39 | When running these commands through \f[V]ea\f[R], the paths in output 40 | will be stored in a local database, and become available for later use. 41 | .PP 42 | Use \f[V]ea run\f[R] to invoke your command. 43 | For example: 44 | .RS 45 | .PP 46 | \f[B]ea run\f[R] \f[I]grouped\f[R] rg -- Vec src 47 | .RE 48 | .PP 49 | \&... 50 | is how you run \f[V]rg Vec src\f[R] through \f[V]ea\f[R]. 51 | Note: any argument for \f[V]rg\f[R] comes after \f[V]--\f[R]. 52 | .PP 53 | \f[I]grouped\f[R] hints at the format of \f[V]rg\f[R]`s output' so that 54 | \f[V]ea\f[R] knows how to find paths from \f[V]rg\f[R]\[cq]s output. 55 | This hint is necessary for \f[V]ea\f[R] to work with any many arbitrary 56 | commands as possible. 57 | See FORMATS to learn more. 58 | .PP 59 | You\[cq]ll see that file locations in the original command are now 60 | prefixed with a number: 61 | .RS 62 | .PP 63 | src/something.rs 64 | .PD 0 65 | .P 66 | .PD 67 | [1] 23: let list: Vec 68 | .PD 0 69 | .P 70 | .PD 71 | [2] 41: fn build() -> Vec 72 | .PD 0 73 | .P 74 | .PD 75 | \&... 76 | .RE 77 | .PP 78 | \f[V]ea list\f[R], or simply \f[V]ea\f[R], will print out these 79 | locations along with their numbers again: 80 | .RS 81 | .PP 82 | [1] src/something.rs:23 83 | .PD 0 84 | .P 85 | .PD 86 | [2] src/something.rs:41 87 | .PD 0 88 | .P 89 | .PD 90 | \&... 91 | .RE 92 | .PP 93 | With the numbers, \f[V]ea\f[R] can retrieve a corresponding path. 94 | In our example, \f[V]ea print\f[R] \f[I]2\f[R] (or \f[V]ea p\f[R] 95 | \f[I]2\f[R] in short) results in: > src/something.rs 96 | .PP 97 | \f[V]ea print\f[R] takes an optional second argument \f[I]format\f[R], 98 | making it possible to retrieve the location info mixed in this 99 | \f[I]format\f[R] string. 100 | The sequences \f[I]{path}\f[R], \f[I]{line}\f[R], and \f[I]{column}\f[R] 101 | appearing in \f[I]format\f[R] get replaced by the location info. 102 | Running \f[V]ea p\f[R] \f[I]2\f[R] \f[I]`{path} \[at] {line}'\f[R] 103 | results in 104 | .RS 105 | .PP 106 | src/something.rs \[at] 41 107 | .RE 108 | .PP 109 | \f[V]ea print\f[R]\[cq]s output is expected to be used as part of a 110 | longer command, such as \f[V]vim $(ea p 2)\f[R]. 111 | .PP 112 | It is recommended to create shell aliases or functions for frequently 113 | used \f[V]ea\f[R] commands. 114 | .SS FORMATS 115 | .PP 116 | \f[V]ea run\f[R]\[cq]s first argument \f[I]style\f[R] is mandatory. 117 | It indicates how file locations will appear in the output. 118 | This argument must be one of the following: \f[V]grouped\f[R], 119 | \f[V]linear\f[R], \f[V]search\f[R], \f[V]rust\f[R] \f[V]python\f[R]. 120 | This section will explain what each of them means. 121 | The command you run through \f[V]ea\f[R] should have a matching 122 | \f[I]format\f[R] value. 123 | .PP 124 | \f[B]grouped\f[R] indicates the command\[cq]s output contains one or 125 | more sections, each section begins with a file path, and lines of 126 | line/column, and possibly more content, follows. 127 | An example of this \f[I]style\f[R] of output is \f[V]ripgrep\f[R]\[cq]s 128 | default output: 129 | .RS 130 | .PP 131 | src/archive.rs 132 | .PD 0 133 | .P 134 | .PD 135 | 41: pub fn read() -> Vec { 136 | .PD 0 137 | .P 138 | .PD 139 | 45: pub fn read_from(path: &Path) -> Vec { 140 | .PP 141 | src/interface.rs 142 | .PD 0 143 | .P 144 | .PD 145 | 53: arguments: Vec, 146 | .RE 147 | .PP 148 | \f[B]linear\f[R] indicates each line in the command\[cq]s output is a 149 | location. 150 | A location can be a file path, optionally followed by line, and, 151 | optionally, column number separated by \[lq]:\[rq] (colon). 152 | This is the default output from commands such as \f[I]find\f[R] or 153 | \f[I]fd\f[R]: 154 | .RS 155 | .PP 156 | src/archive.rs:41 157 | .PD 0 158 | .P 159 | .PD 160 | src/archive.rs:45 161 | .PD 0 162 | .P 163 | .PD 164 | src/interface.rs:53 165 | .RE 166 | .PP 167 | \f[B]search\f[R] means the output is almost arbitrary, except every now 168 | and then, an location appears at the beginning of a line. 169 | This is common in error messages from compilers like \f[I]clang\f[R] or 170 | \f[I]swift\f[R]: 171 | .RS 172 | .PP 173 | Sources/Critic/DocProblem.swift:5:26: error: cannot find type `Stringx' 174 | in scope 175 | .RE 176 | .PP 177 | \f[B]rust\f[R] means the format from Rust tools such as rustc, or 178 | clippy. 179 | .PP 180 | \f[B]python\f[R] means Python or iPython\[cq]s backtrace. 181 | .PP 182 | If you need \f[V]ea\f[R] to support more formats, please file an issue 183 | at https://github.com/dduan/ea 184 | .SS SHELL INTEGRATION 185 | .PP 186 | Some shell aliases, and functions makes using \f[V]ea\f[R] more 187 | effective. 188 | .PP 189 | First, you\[cq]ll want alias your normal command to the \f[V]ea\f[R] 190 | version: 191 | .IP 192 | .nf 193 | \f[C] 194 | alias fd \[aq]ea run linear fd --\[aq] 195 | \f[R] 196 | .fi 197 | .PP 198 | Then, optionally, make a shell function that consume a path from 199 | \f[V]ea\f[R]\[cq]s output. 200 | The following example makes \f[V]e 6\f[R] opens the 6th paths known to 201 | \f[V]ea\f[R] in your default editor in zsh/bash: 202 | .IP 203 | .nf 204 | \f[C] 205 | e() { 206 | eval $(ea p $1 \[dq]$EDITOR {path}\[dq]) 207 | } 208 | \f[R] 209 | .fi 210 | .PP 211 | For more examples, see documentation at 212 | https://github.com/dduan/ea/blob/main/docs/UserManual.md 213 | .SS AUTHOR 214 | .PP 215 | Daniel Duan 216 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1648297722, 6 | "narHash": "sha256-W+qlPsiZd8F3XkzXOzAoR+mpFqzm3ekQkJNa+PIh1BQ=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "0f8662f1319ad6abf89b3380dd2722369fc51ade", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nixpkgs": { 19 | "locked": { 20 | "lastModified": 1649541735, 21 | "narHash": "sha256-JdOywA2jcdGCxNgu0dJA7ZNtaV7sS0HwuZg9YaXd94c=", 22 | "owner": "nixos", 23 | "repo": "nixpkgs", 24 | "rev": "c2b6e029cd1efa0efd37daab89264ef040ae5669", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "nixos", 29 | "repo": "nixpkgs", 30 | "type": "github" 31 | } 32 | }, 33 | "root": { 34 | "inputs": { 35 | "flake-utils": "flake-utils", 36 | "nixpkgs": "nixpkgs" 37 | } 38 | } 39 | }, 40 | "root": "root", 41 | "version": 7 42 | } 43 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Tree command, improved."; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils }: 10 | flake-utils.lib.eachDefaultSystem (system: 11 | with nixpkgs.legacyPackages.${system}; 12 | let 13 | info = (fromTOML (builtins.readFile ./Cargo.toml)).package; 14 | in 15 | rec { 16 | packages = flake-utils.lib.flattenTree { 17 | ea = rustPlatform.buildRustPackage rec { 18 | pname = info.name; 19 | version = info.version; 20 | src = ./.; 21 | cargoSha256 = "sha256-1x9LQ1MkdNZLga+pAahkwaI6tlbdT41nwB+D1XVLBwY="; 22 | lockFile = ./Cargo.lock; 23 | nativeBuildInputs = [ installShellFiles pandoc ]; 24 | preFixup = '' 25 | make manual 26 | installManPage docs/ea.1 27 | ''; 28 | }; 29 | }; 30 | defaultPackage = packages.ea; 31 | devShell = pkgs.mkShell { 32 | buildInputs = [ 33 | pandoc 34 | cargo 35 | clippy 36 | rust-analyzer 37 | rustc 38 | ] ++ pkgs.lib.lists.optionals stdenv.isDarwin [ 39 | libiconv 40 | ]; 41 | }; 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/grouped.in.txt: -------------------------------------------------------------------------------- 1 | src/interface.rs 2 | 20: arguments: Vec, 3 | 4 | src/archive.rs 5 | 35:pub fn write(list: &Vec) -> io::Result<()> { 6 | 36: let data: Vec = bincode::serialize(list).unwrap_or(vec![]); 7 | 40:pub fn read() -> Vec { 8 | 41: let data: Vec = fs::read(ARCHIVE_PATH.as_path()).unwrap_or(vec![]); 9 | 10 | src/parsers.rs 11 | 6:pub fn ripgrep(input: &str) -> (Vec, Vec) { 12 | 13 | src/commands/run.rs 14 | 11:pub fn run(style: &Style, executable: &str, arguments: &Vec) { 15 | 15:fn execute(executable: &str, arguments: &Vec) -> Vec { 16 | 19: let mut output = Vec::new(); 17 | 41: let locations: Vec; 18 | 42: let display: Vec; 19 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/grouped.out.txt: -------------------------------------------------------------------------------- 1 | src/interface.rs 2 | [1] 20: arguments: Vec, 3 | 4 | src/archive.rs 5 | [2] 35:pub fn write(list: &Vec) -> io::Result<()> { 6 | [3] 36: let data: Vec = bincode::serialize(list).unwrap_or(vec![]); 7 | [4] 40:pub fn read() -> Vec { 8 | [5] 41: let data: Vec = fs::read(ARCHIVE_PATH.as_path()).unwrap_or(vec![]); 9 | 10 | src/parsers.rs 11 | [6] 6:pub fn ripgrep(input: &str) -> (Vec, Vec) { 12 | 13 | src/commands/run.rs 14 | [7] 11:pub fn run(style: &Style, executable: &str, arguments: &Vec) { 15 | [8] 15:fn execute(executable: &str, arguments: &Vec) -> Vec { 16 | [9] 19: let mut output = Vec::new(); 17 | [10] 41: let locations: Vec; 18 | [11] 42: let display: Vec; 19 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/grouped2.in.txt: -------------------------------------------------------------------------------- 1 | /Users/dduan/src/Clue/Sources/Clue/StoreInitializationError.swift 2 | 6:10: case couldNotInferStoreLocation 3 | 30:15: case .couldNotInferStoreLocation: 4 | 5 | /Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift 6 | 135:40: throw StoreInitializationError.couldNotInferStoreLocation 7 | 8 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/grouped2.out.txt: -------------------------------------------------------------------------------- 1 | /Users/dduan/src/Clue/Sources/Clue/StoreInitializationError.swift 2 | [1] 6:10: case couldNotInferStoreLocation 3 | [2] 30:15: case .couldNotInferStoreLocation: 4 | 5 | /Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift 6 | [3] 135:40: throw StoreInitializationError.couldNotInferStoreLocation 7 | 8 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/grouped2_locations.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dduan/ea/5849d96cf9fd7306153eb4fc20ba4fce52ca53d7/resources/fixtures/parsers/grouped2_locations.bin -------------------------------------------------------------------------------- /resources/fixtures/parsers/grouped3.in.txt: -------------------------------------------------------------------------------- 1 | src/parsers.rs 2 | 10:pub fn grouped(input: &[u8]) -> (Vec, Vec) { 3 | 18: let mut locations: Vec = Vec::new(); 4 | 50: let output_data: Vec = output.as_bytes().to_owned(); 5 | 54:pub fn linear(input: &[u8]) -> (Vec, Vec) { 6 | 56: let mut locations: Vec = Vec::new(); 7 | 77: let output_data: Vec = output.as_bytes().to_owned(); 8 | 9 | src/interface.rs 10 | 22: arguments: Vec, 11 | 12 | src/archive.rs 13 | 35:pub fn write(list: &Vec) -> io::Result<()> { 14 | 36: let data: Vec = bincode::serialize(list).unwrap_or(vec![]); 15 | 40:pub fn read() -> Vec { 16 | 41: let data: Vec = fs::read(ARCHIVE_PATH.as_path()).unwrap_or(vec![]); 17 | 18 | src/commands/run.rs 19 | 11:pub fn run(style: &Style, executable: &str, arguments: &Vec, debug: Option) { 20 | 26:fn execute(executable: &str, arguments: &Vec) -> Vec { 21 | 29: let mut output = Vec::new(); 22 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/grouped3.out.txt: -------------------------------------------------------------------------------- 1 | src/parsers.rs 2 | [1] 10:pub fn grouped(input: &[u8]) -> (Vec, Vec) { 3 | [2] 18: let mut locations: Vec = Vec::new(); 4 | [3] 50: let output_data: Vec = output.as_bytes().to_owned(); 5 | [4] 54:pub fn linear(input: &[u8]) -> (Vec, Vec) { 6 | [5] 56: let mut locations: Vec = Vec::new(); 7 | [6] 77: let output_data: Vec = output.as_bytes().to_owned(); 8 | 9 | src/interface.rs 10 | [7] 22: arguments: Vec, 11 | 12 | src/archive.rs 13 | [8] 35:pub fn write(list: &Vec) -> io::Result<()> { 14 | [9] 36: let data: Vec = bincode::serialize(list).unwrap_or(vec![]); 15 | [10] 40:pub fn read() -> Vec { 16 | [11] 41: let data: Vec = fs::read(ARCHIVE_PATH.as_path()).unwrap_or(vec![]); 17 | 18 | src/commands/run.rs 19 | [12] 11:pub fn run(style: &Style, executable: &str, arguments: &Vec, debug: Option) { 20 | [13] 26:fn execute(executable: &str, arguments: &Vec) -> Vec { 21 | [14] 29: let mut output = Vec::new(); 22 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/grouped3_locations.bin: -------------------------------------------------------------------------------- 1 | src/parsers.rs 2 | src/parsers.rssrc/parsers.rs2src/parsers.rs6src/parsers.rs8src/parsers.rsMsrc/interface.rssrc/archive.rs#src/archive.rs$src/archive.rs(src/archive.rs)src/commands/run.rs src/commands/run.rssrc/commands/run.rs -------------------------------------------------------------------------------- /resources/fixtures/parsers/grouped_locations.bin: -------------------------------------------------------------------------------- 1 | src/interface.rssrc/archive.rs#src/archive.rs$src/archive.rs(src/archive.rs)src/parsers.rssrc/commands/run.rs src/commands/run.rssrc/commands/run.rssrc/commands/run.rs)src/commands/run.rs* -------------------------------------------------------------------------------- /resources/fixtures/parsers/ipython3.in.txt: -------------------------------------------------------------------------------- 1 |  File "/tmp/test.py", line 1 2 |  def f() 3 |  ^ 4 | SyntaxError: invalid syntax 5 | 6 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/ipython3.out.txt: -------------------------------------------------------------------------------- 1 |  File "[1] /tmp/test.py", line 1 2 |  def f() 3 |  ^ 4 | SyntaxError: invalid syntax 5 | 6 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/ipython3_locations.bin: -------------------------------------------------------------------------------- 1 |  /tmp/test.py -------------------------------------------------------------------------------- /resources/fixtures/parsers/linear.in.txt: -------------------------------------------------------------------------------- 1 | ./tests/test_parsers.rs 2 | ./src/parsers.rs 3 | ./src/interface.rs 4 | ./src/lib.rs 5 | ./src/commands.rs 6 | ./src/main.rs 7 | ./src/commands/list.rs 8 | ./src/commands/run.rs 9 | ./src/archive.rs 10 | ./src/location.rs 11 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/linear.out.txt: -------------------------------------------------------------------------------- 1 | [1] ./tests/test_parsers.rs 2 | [2] ./src/parsers.rs 3 | [3] ./src/interface.rs 4 | [4] ./src/lib.rs 5 | [5] ./src/commands.rs 6 | [6] ./src/main.rs 7 | [7] ./src/commands/list.rs 8 | [8] ./src/commands/run.rs 9 | [9] ./src/archive.rs 10 | [10] ./src/location.rs 11 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/linear_colored.in.txt: -------------------------------------------------------------------------------- 1 | src/archive.rs 2 | src/commands/list.rs 3 | src/commands/run.rs 4 | src/commands.rs 5 | src/interface.rs 6 | src/lib.rs 7 | src/location.rs 8 | src/main.rs 9 | src/parsers.rs 10 | tests/test_parsers.rs 11 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/linear_colored.out.txt: -------------------------------------------------------------------------------- 1 | [1] src/archive.rs 2 | [2] src/commands/list.rs 3 | [3] src/commands/run.rs 4 | [4] src/commands.rs 5 | [5] src/interface.rs 6 | [6] src/lib.rs 7 | [7] src/location.rs 8 | [8] src/main.rs 9 | [9] src/parsers.rs 10 | [10] tests/test_parsers.rs 11 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/linear_colored_locations.bin: -------------------------------------------------------------------------------- 1 | 2 | src/archive.rssrc/commands/list.rssrc/commands/run.rssrc/commands.rssrc/interface.rs 3 | src/lib.rssrc/location.rs src/main.rssrc/parsers.rstests/test_parsers.rs -------------------------------------------------------------------------------- /resources/fixtures/parsers/linear_locations.bin: -------------------------------------------------------------------------------- 1 | 2 | ./tests/test_parsers.rs./src/parsers.rs./src/interface.rs ./src/lib.rs./src/commands.rs ./src/main.rs./src/commands/list.rs./src/commands/run.rs./src/archive.rs./src/location.rs -------------------------------------------------------------------------------- /resources/fixtures/parsers/python3.in.txt: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "/tmp/test2.py", line 1, in 3 | import test 4 | File "/private/tmp/test.py", line 1 5 | def f() 6 | ^ 7 | SyntaxError: invalid syntax 8 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/python3.out.txt: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "[1] /tmp/test2.py", line 1, in 3 | import test 4 | File "[2] /private/tmp/test.py", line 1 5 | def f() 6 | ^ 7 | SyntaxError: invalid syntax 8 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/python3_locations.bin: -------------------------------------------------------------------------------- 1 |  /tmp/test2.py/private/tmp/test.py -------------------------------------------------------------------------------- /resources/fixtures/parsers/rust.in.txt: -------------------------------------------------------------------------------- 1 | warning: literal with an empty format string 2 |  --> src/parsers.rs:28:25 3 |  | 4 | 28 |  write!(f, "{}", "Could not decode input as UTF-8 string") 5 |  |  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 |  | 7 |  = note: `#[warn(clippy::write_literal)]` on by default 8 |  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#write_literal 9 | help: try this 10 |  | 11 | 28 -  write!(f, "{}", "Could not decode input as UTF-8 string") 12 | 28 +  write!(f, "Could not decode input as UTF-8 string") 13 |  |  14 | 15 | warning: using `eprintln!("")` 16 |  --> src/commands/run.rs:50:9 17 |  | 18 | 50 |  eprintln!(""); 19 |  |  ^^^^^^^^^^^^^ help: replace it with: `eprintln!()` 20 |  | 21 |  = note: `#[warn(clippy::println_empty_string)]` on by default 22 |  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#println_empty_string 23 | 24 | warning: `ea-command` (lib) generated 2 warnings 25 |  Finished dev [unoptimized + debuginfo] target(s) in 0.03s 26 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/rust.out.txt: -------------------------------------------------------------------------------- 1 | warning: literal with an empty format string 2 |  --> [1] src/parsers.rs:28:25 3 |  | 4 | 28 |  write!(f, "{}", "Could not decode input as UTF-8 string") 5 |  |  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 |  | 7 |  = note: `#[warn(clippy::write_literal)]` on by default 8 |  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#write_literal 9 | help: try this 10 |  | 11 | 28 -  write!(f, "{}", "Could not decode input as UTF-8 string") 12 | 28 +  write!(f, "Could not decode input as UTF-8 string") 13 |  |  14 | 15 | warning: using `eprintln!("")` 16 |  --> [2] src/commands/run.rs:50:9 17 |  | 18 | 50 |  eprintln!(""); 19 |  |  ^^^^^^^^^^^^^ help: replace it with: `eprintln!()` 20 |  | 21 |  = note: `#[warn(clippy::println_empty_string)]` on by default 22 |  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#println_empty_string 23 | 24 | warning: `ea-command` (lib) generated 2 warnings 25 |  Finished dev [unoptimized + debuginfo] target(s) in 0.03s 26 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/rust_locations.bin: -------------------------------------------------------------------------------- 1 | src/parsers.rssrc/commands/run.rs2 -------------------------------------------------------------------------------- /resources/fixtures/parsers/search.in.txt: -------------------------------------------------------------------------------- 1 |  Building for debugging... 2 |  error: emit-module command failed with exit code 1 (use -v to see invocation) [1/3] Compiling Clue ClueEngine.swift /Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift:5:13: error: cannot find type 'IndexStoreDBx' in scope 3 |  let db: IndexStoreDBx 4 |  ^~~~~~~~~~~~~ 5 | /Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift:53:50: error: reference to member 'implicit' cannot be resolved without a contextual type 6 |  .filter { !$0.roles.isSuperset(of: [.implicit, .definition]) } 7 |  ^ 8 | /Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift:53:61: error: reference to member 'definition' cannot be resolved without a contextual type 9 |  .filter { !$0.roles.isSuperset(of: [.implicit, .definition]) } 10 |  ^ 11 | /Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift:82:42: error: cannot infer contextual base in reference to member 'definition' 12 |  .filter { $0.roles.contains(.definition) && $0.location.isSystem == isSystem } 13 |  ~^~~~~~~~~~ 14 | /Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift:99:65: error: cannot infer contextual base in reference to member 'definition' 15 |  let candidates = db.occurrences(ofUSR: usr, roles: .definition) 16 |  ~^~~~~~~~~~ 17 |  [2/3] Emitting module Clue /Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift:5:13: error: cannot find type 'IndexStoreDBx' in scope 18 |  let db: IndexStoreDBx 19 |  ^~~~~~~~~~~~~ 20 |  [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue 21 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/search.out.txt: -------------------------------------------------------------------------------- 1 |  Building for debugging... 2 |  error: emit-module command failed with exit code 1 (use -v to see invocation) [1/3] Compiling Clue ClueEngine.swift [1] /Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift:5:13: error: cannot find type 'IndexStoreDBx' in scope 3 |  let db: IndexStoreDBx 4 |  ^~~~~~~~~~~~~ 5 | [2] /Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift:53:50: error: reference to member 'implicit' cannot be resolved without a contextual type 6 |  .filter { !$0.roles.isSuperset(of: [.implicit, .definition]) } 7 |  ^ 8 | [3] /Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift:53:61: error: reference to member 'definition' cannot be resolved without a contextual type 9 |  .filter { !$0.roles.isSuperset(of: [.implicit, .definition]) } 10 |  ^ 11 | [4] /Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift:82:42: error: cannot infer contextual base in reference to member 'definition' 12 |  .filter { $0.roles.contains(.definition) && $0.location.isSystem == isSystem } 13 |  ~^~~~~~~~~~ 14 | [5] /Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift:99:65: error: cannot infer contextual base in reference to member 'definition' 15 |  let candidates = db.occurrences(ofUSR: usr, roles: .definition) 16 |  ~^~~~~~~~~~ 17 |  [2/3] Emitting module Clue [6] /Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift:5:13: error: cannot find type 'IndexStoreDBx' in scope 18 |  let db: IndexStoreDBx 19 |  ^~~~~~~~~~~~~ 20 |  [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue [2/3] Emitting module Clue 21 | -------------------------------------------------------------------------------- /resources/fixtures/parsers/search_locations.bin: -------------------------------------------------------------------------------- 1 | 3/Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift 3/Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift523/Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift5=3/Users/dduan/src/Clue/Sources/Clue/ClueEngine.swiftR*3/Users/dduan/src/Clue/Sources/Clue/ClueEngine.swiftcA3/Users/dduan/src/Clue/Sources/Clue/ClueEngine.swift -------------------------------------------------------------------------------- /scripts/check-version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import sys 5 | 6 | expected = re.search(r'name = "ea-command"\nversion = "(.+)"', open('Cargo.toml').read(), re.M).group(1) 7 | versions = {} 8 | versions['Cargo.lock'] = re.search(r'name = "ea-command"\nversion = "(.+)"', open('Cargo.lock').read(), re.M).group(1) 9 | versions['CHANGELOG.md'] = re.search(r'# main(?:\n.*)+?\n#\s*(.+)', open('CHANGELOG.md').read(), re.M).group(1) 10 | versions['docs/UserManual.md'] = re.search(r'% EA\(1\) Version ([0-9.]+)', open("docs/UserManual.md").read()).group(1) 11 | versions['docs/ea.1'] = re.search(r'.TH "EA" "1" "" "Version ([0-9.]+)"', open('docs/ea.1').read()).group(1) 12 | 13 | for file in versions: 14 | if expected != versions[file]: 15 | print(f"version mismatch: expected {expected}; found {versions[file]} in {file}", file=sys.stderr) 16 | exit(1) 17 | -------------------------------------------------------------------------------- /scripts/completion/_ea: -------------------------------------------------------------------------------- 1 | #compdef ea 2 | 3 | autoload -U is-at-least 4 | 5 | _ea() { 6 | typeset -A opt_args 7 | typeset -a _arguments_options 8 | local ret=1 9 | 10 | if is-at-least 5.2; then 11 | _arguments_options=(-s -S -C) 12 | else 13 | _arguments_options=(-s -C) 14 | fi 15 | 16 | local context curcontext="$curcontext" state line 17 | _arguments "${_arguments_options[@]}" \ 18 | '-h[Print help information]' \ 19 | '--help[Print help information]' \ 20 | '-V[Print version information]' \ 21 | '--version[Print version information]' \ 22 | ":: :_ea_commands" \ 23 | "*::: :->ea-command" \ 24 | && ret=0 25 | case $state in 26 | (ea-command) 27 | words=($line[1] "${words[@]}") 28 | (( CURRENT += 1 )) 29 | curcontext="${curcontext%:*:*}:ea-command-$line[1]:" 30 | case $line[1] in 31 | (run) 32 | _arguments "${_arguments_options[@]}" \ 33 | '--debug=[Write debug info at ]:debug_files_base_name:_files' \ 34 | '-h[Print help information]' \ 35 | '--help[Print help information]' \ 36 | ':style -- Format of output from EXECUTABLE. ea looks for file paths, lines, and columns within the file at the path. A file path can have one or more "locations". A location has at least a file path, and a line number, with an optional column:(grouped linear search rust py)' \ 37 | ':executable -- The command to execute:_command_names -e' \ 38 | '*::arguments -- Arguments for EXECUTABLE. Must be separated from EXECUTABLE with `--` (two dashes):' \ 39 | && ret=0 40 | ;; 41 | (list) 42 | _arguments "${_arguments_options[@]}" \ 43 | '-h[Print help information]' \ 44 | '--help[Print help information]' \ 45 | && ret=0 46 | ;; 47 | (print) 48 | _arguments "${_arguments_options[@]}" \ 49 | '-h[Print help information]' \ 50 | '--help[Print help information]' \ 51 | ':number -- The number associated with a file location from the latest `ea run` output:' \ 52 | '::format -- A string representing the format of the location to be printed. `{path}`, `{line}`, and `{column}` in this string will be replaced with corresponding values within the location. For example, '\''L{line}C{column} @ {path}'\'' might print out '\''L23C11 @ path/to/file'\''. If line or column info isn'\''t available, they'\''ll be filled with '\''0'\'':' \ 53 | && ret=0 54 | ;; 55 | (help) 56 | _arguments "${_arguments_options[@]}" \ 57 | '*::subcommand -- The subcommand whose help message to display:' \ 58 | && ret=0 59 | ;; 60 | esac 61 | ;; 62 | esac 63 | } 64 | 65 | (( $+functions[_ea_commands] )) || 66 | _ea_commands() { 67 | local commands; commands=( 68 | 'run:Run EXECUTABLE through `ea`. Expecting its output to be the format of STYLE. Arguments for EXECUTABLE must come after `--`. For example, `rg Vec src` becomes:' \ 69 | 'list:List locations found from the latest `ea run` output. This is the default subcommand. Running `ea` is the same as running `ea list`' \ 70 | 'print:Print the location info associated with NUMBER. Optionally, customize the output FORMAT. Also availble as the shorthand `ea p ...`' \ 71 | 'help:Print this message or the help of the given subcommand(s)' \ 72 | ) 73 | _describe -t commands 'ea commands' commands "$@" 74 | } 75 | (( $+functions[_ea__help_commands] )) || 76 | _ea__help_commands() { 77 | local commands; commands=() 78 | _describe -t commands 'ea help commands' commands "$@" 79 | } 80 | (( $+functions[_ea__list_commands] )) || 81 | _ea__list_commands() { 82 | local commands; commands=() 83 | _describe -t commands 'ea list commands' commands "$@" 84 | } 85 | (( $+functions[_ea__print_commands] )) || 86 | _ea__print_commands() { 87 | local commands; commands=() 88 | _describe -t commands 'ea print commands' commands "$@" 89 | } 90 | (( $+functions[_ea__run_commands] )) || 91 | _ea__run_commands() { 92 | local commands; commands=() 93 | _describe -t commands 'ea run commands' commands "$@" 94 | } 95 | 96 | _ea "$@" 97 | -------------------------------------------------------------------------------- /scripts/completion/_ea.ps1: -------------------------------------------------------------------------------- 1 | 2 | using namespace System.Management.Automation 3 | using namespace System.Management.Automation.Language 4 | 5 | Register-ArgumentCompleter -Native -CommandName 'ea' -ScriptBlock { 6 | param($wordToComplete, $commandAst, $cursorPosition) 7 | 8 | $commandElements = $commandAst.CommandElements 9 | $command = @( 10 | 'ea' 11 | for ($i = 1; $i -lt $commandElements.Count; $i++) { 12 | $element = $commandElements[$i] 13 | if ($element -isnot [StringConstantExpressionAst] -or 14 | $element.StringConstantType -ne [StringConstantType]::BareWord -or 15 | $element.Value.StartsWith('-') -or 16 | $element.Value -eq $wordToComplete) { 17 | break 18 | } 19 | $element.Value 20 | }) -join ';' 21 | 22 | $completions = @(switch ($command) { 23 | 'ea' { 24 | [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') 25 | [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') 26 | [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') 27 | [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') 28 | [CompletionResult]::new('run', 'run', [CompletionResultType]::ParameterValue, 'Run EXECUTABLE through `ea`. Expecting its output to be the format of STYLE. Arguments for EXECUTABLE must come after `--`. For example, `rg Vec src` becomes:') 29 | [CompletionResult]::new('list', 'list', [CompletionResultType]::ParameterValue, 'List locations found from the latest `ea run` output. This is the default subcommand. Running `ea` is the same as running `ea list`') 30 | [CompletionResult]::new('print', 'print', [CompletionResultType]::ParameterValue, 'Print the location info associated with NUMBER. Optionally, customize the output FORMAT. Also availble as the shorthand `ea p ...`') 31 | [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') 32 | break 33 | } 34 | 'ea;run' { 35 | [CompletionResult]::new('--debug', 'debug', [CompletionResultType]::ParameterName, 'Write debug info at ') 36 | [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') 37 | [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') 38 | break 39 | } 40 | 'ea;list' { 41 | [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') 42 | [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') 43 | break 44 | } 45 | 'ea;print' { 46 | [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information') 47 | [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') 48 | break 49 | } 50 | 'ea;help' { 51 | break 52 | } 53 | }) 54 | 55 | $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | 56 | Sort-Object -Property ListItemText 57 | } 58 | -------------------------------------------------------------------------------- /scripts/completion/ea.bash: -------------------------------------------------------------------------------- 1 | _ea() { 2 | local i cur prev opts cmds 3 | COMPREPLY=() 4 | cur="${COMP_WORDS[COMP_CWORD]}" 5 | prev="${COMP_WORDS[COMP_CWORD-1]}" 6 | cmd="" 7 | opts="" 8 | 9 | for i in ${COMP_WORDS[@]} 10 | do 11 | case "${i}" in 12 | "$1") 13 | cmd="ea" 14 | ;; 15 | help) 16 | cmd+="__help" 17 | ;; 18 | list) 19 | cmd+="__list" 20 | ;; 21 | print) 22 | cmd+="__print" 23 | ;; 24 | run) 25 | cmd+="__run" 26 | ;; 27 | *) 28 | ;; 29 | esac 30 | done 31 | 32 | case "${cmd}" in 33 | ea) 34 | opts="-h -V --help --version run list print help" 35 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then 36 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 37 | return 0 38 | fi 39 | case "${prev}" in 40 | *) 41 | COMPREPLY=() 42 | ;; 43 | esac 44 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 45 | return 0 46 | ;; 47 | ea__help) 48 | opts="..." 49 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then 50 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 51 | return 0 52 | fi 53 | case "${prev}" in 54 | *) 55 | COMPREPLY=() 56 | ;; 57 | esac 58 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 59 | return 0 60 | ;; 61 | ea__list) 62 | opts="-h --help" 63 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then 64 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 65 | return 0 66 | fi 67 | case "${prev}" in 68 | *) 69 | COMPREPLY=() 70 | ;; 71 | esac 72 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 73 | return 0 74 | ;; 75 | ea__print) 76 | opts="-h --help " 77 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then 78 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 79 | return 0 80 | fi 81 | case "${prev}" in 82 | *) 83 | COMPREPLY=() 84 | ;; 85 | esac 86 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 87 | return 0 88 | ;; 89 | ea__run) 90 | opts="-h --debug --help grouped linear search rust py ..." 91 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then 92 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 93 | return 0 94 | fi 95 | case "${prev}" in 96 | --debug) 97 | COMPREPLY=($(compgen -f "${cur}")) 98 | return 0 99 | ;; 100 | *) 101 | COMPREPLY=() 102 | ;; 103 | esac 104 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 105 | return 0 106 | ;; 107 | esac 108 | } 109 | 110 | complete -F _ea -o bashdefault -o default ea 111 | -------------------------------------------------------------------------------- /scripts/completion/ea.elv: -------------------------------------------------------------------------------- 1 | 2 | use builtin; 3 | use str; 4 | 5 | set edit:completion:arg-completer[ea] = {|@words| 6 | fn spaces {|n| 7 | builtin:repeat $n ' ' | str:join '' 8 | } 9 | fn cand {|text desc| 10 | edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc 11 | } 12 | var command = 'ea' 13 | for word $words[1..-1] { 14 | if (str:has-prefix $word '-') { 15 | break 16 | } 17 | set command = $command';'$word 18 | } 19 | var completions = [ 20 | &'ea'= { 21 | cand -h 'Print help information' 22 | cand --help 'Print help information' 23 | cand -V 'Print version information' 24 | cand --version 'Print version information' 25 | cand run 'Run EXECUTABLE through `ea`. Expecting its output to be the format of STYLE. Arguments for EXECUTABLE must come after `--`. For example, `rg Vec src` becomes:' 26 | cand list 'List locations found from the latest `ea run` output. This is the default subcommand. Running `ea` is the same as running `ea list`' 27 | cand print 'Print the location info associated with NUMBER. Optionally, customize the output FORMAT. Also availble as the shorthand `ea p ...`' 28 | cand help 'Print this message or the help of the given subcommand(s)' 29 | } 30 | &'ea;run'= { 31 | cand --debug 'Write debug info at ' 32 | cand -h 'Print help information' 33 | cand --help 'Print help information' 34 | } 35 | &'ea;list'= { 36 | cand -h 'Print help information' 37 | cand --help 'Print help information' 38 | } 39 | &'ea;print'= { 40 | cand -h 'Print help information' 41 | cand --help 'Print help information' 42 | } 43 | &'ea;help'= { 44 | } 45 | ] 46 | $completions[$command] 47 | } 48 | -------------------------------------------------------------------------------- /scripts/completion/ea.fish: -------------------------------------------------------------------------------- 1 | complete -c ea -n "__fish_use_subcommand" -s h -l help -d 'Print help information' 2 | complete -c ea -n "__fish_use_subcommand" -s V -l version -d 'Print version information' 3 | complete -c ea -n "__fish_use_subcommand" -f -a "run" -d 'Run EXECUTABLE through `ea`. Expecting its output to be the format of STYLE. Arguments for EXECUTABLE must come after `--`. For example, `rg Vec src` becomes:' 4 | complete -c ea -n "__fish_use_subcommand" -f -a "list" -d 'List locations found from the latest `ea run` output. This is the default subcommand. Running `ea` is the same as running `ea list`' 5 | complete -c ea -n "__fish_use_subcommand" -f -a "print" -d 'Print the location info associated with NUMBER. Optionally, customize the output FORMAT. Also availble as the shorthand `ea p ...`' 6 | complete -c ea -n "__fish_use_subcommand" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' 7 | complete -c ea -n "__fish_seen_subcommand_from run" -l debug -d 'Write debug info at ' -r -F 8 | complete -c ea -n "__fish_seen_subcommand_from run" -s h -l help -d 'Print help information' 9 | complete -c ea -n "__fish_seen_subcommand_from list" -s h -l help -d 'Print help information' 10 | complete -c ea -n "__fish_seen_subcommand_from print" -s h -l help -d 'Print help information' 11 | -------------------------------------------------------------------------------- /scripts/pandoc-docker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo $@ 3 | docker run --platform linux/amd64/v8 --rm -v "$(pwd):/data" -u $(id -u):$(id -g) pandoc/minimal $@ 4 | -------------------------------------------------------------------------------- /src/archive.rs: -------------------------------------------------------------------------------- 1 | use crate::Location; 2 | use lazy_static::lazy_static; 3 | use std::{env, fs, io}; 4 | use std::path::{PathBuf, Path}; 5 | 6 | #[cfg(not(target_os = "windows"))] 7 | lazy_static! { 8 | static ref ARCHIVE_PATH: PathBuf = [ 9 | "/tmp".to_string(), 10 | format!( 11 | "ea_aliases_{}.bin", 12 | env::var("USERNAME").unwrap_or_else(|_| "".to_string()) 13 | ), 14 | ] 15 | .iter() 16 | .collect(); 17 | } 18 | 19 | #[cfg(target_os = "windows")] 20 | lazy_static! { 21 | static ref ARCHIVE_PATH: PathBuf = [ 22 | env::var("TEMP").unwrap_or(env::var("HOME").unwrap_or_else(|_| r".".to_string())), 23 | format!( 24 | "ea_{}.bin", 25 | env::var("USERNAME").unwrap_or_else(|_| "".to_string()) 26 | ), 27 | ] 28 | .iter() 29 | .collect(); 30 | } 31 | 32 | pub fn write(list: &[Location]) -> io::Result<()> { 33 | write_to(ARCHIVE_PATH.as_path(), list) 34 | } 35 | 36 | pub fn write_to(path: &Path, list: &[Location]) -> io::Result<()> { 37 | let data: Vec = bincode::serialize(list).unwrap_or_default(); 38 | fs::write(path, &data) 39 | } 40 | 41 | pub fn read() -> Vec { 42 | read_from(ARCHIVE_PATH.as_path()) 43 | } 44 | 45 | pub fn read_from(path: &Path) -> Vec { 46 | let data: Vec = fs::read(path).unwrap_or_default(); 47 | bincode::deserialize(&data).unwrap_or_default() 48 | } 49 | -------------------------------------------------------------------------------- /src/bin/ea.rs: -------------------------------------------------------------------------------- 1 | use ea_command::interface::{self, Commands}; 2 | use ea_command::commands; 3 | use clap::Parser; 4 | 5 | fn main() { 6 | let args = interface::Interface::parse(); 7 | match args.subcommand { 8 | Some(Commands::Run { 9 | style, 10 | executable, 11 | arguments, 12 | debug, 13 | }) => { 14 | commands::run::run(&style, &executable, &arguments, debug); 15 | } 16 | Some(Commands::List) | None => { 17 | commands::list::list(); 18 | } 19 | Some(Commands::Print { number, format }) => { 20 | commands::print::print(number, &format); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/bin/printfile.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs; 3 | use std::io::{self, Write}; 4 | 5 | fn main() { 6 | let file = env::args().nth(1).unwrap(); 7 | _ = io::stdout().write(&fs::read(file).unwrap()); 8 | } 9 | -------------------------------------------------------------------------------- /src/commands.rs: -------------------------------------------------------------------------------- 1 | pub mod list; 2 | pub mod print; 3 | pub mod run; 4 | -------------------------------------------------------------------------------- /src/commands/list.rs: -------------------------------------------------------------------------------- 1 | use atty; 2 | use crate::archive; 3 | use crate::Location; 4 | 5 | fn colored_location(index: &usize, location: &Location) -> String { 6 | let mut output = format!("[\x1b[0m\x1b[31m{}\x1b[0m] {}", index + 1, location.path); 7 | 8 | if let Some(line_number) = location.line { 9 | output = format!("{}\x1b[2m:\x1b[0m\x1b[32m{}\x1b[0m", output, &line_number); 10 | if let Some(column_number) = location.column { 11 | output = format!("{}\x1b[2m:{}\x1b[0m", output, column_number); 12 | } 13 | } 14 | 15 | output 16 | } 17 | 18 | fn uncolored_location(index: &usize, location: &Location) -> String { 19 | let mut output = format!("[{}] {}", index + 1, location.path); 20 | if let Some(line_number) = location.line { 21 | output = format!("{}:{}", output, &line_number); 22 | if let Some(column_number) = location.column { 23 | output = format!("{}:{}", output, column_number); 24 | } 25 | } 26 | 27 | output 28 | } 29 | 30 | pub fn list() { 31 | for (idx, location) in archive::read().iter().enumerate() { 32 | if atty::is(atty::Stream::Stdout) { 33 | println!("{}", colored_location(&idx, location)); 34 | } else { 35 | println!("{}", uncolored_location(&idx, location)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/print.rs: -------------------------------------------------------------------------------- 1 | use crate::archive; 2 | 3 | pub fn print(number: usize, format: &str) { 4 | let mut result = format.to_string(); 5 | let list = archive::read(); 6 | if number <= list.len() { 7 | let location = &list[number - 1]; 8 | result = result.replace("{path}", &location.path); 9 | result = result.replace("{line}", &location.line.unwrap_or(1).to_string()); 10 | result = result.replace("{column}", &location.column.unwrap_or(1).to_string()); 11 | 12 | println!("{}", &result); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/run.rs: -------------------------------------------------------------------------------- 1 | use crate::archive; 2 | use crate::interface::Style; 3 | use crate::parsers; 4 | use atty; 5 | use pty::fork::Fork; 6 | use std; 7 | use std::error; 8 | use std::fmt; 9 | use std::fs; 10 | use std::io::{self, Read, Write}; 11 | use std::process; 12 | 13 | #[derive(Debug)] 14 | enum RunError { 15 | CouldNotExecuteCommand(String), 16 | CommandEncounteredError(String), 17 | CommandWasInterrupted(String), 18 | } 19 | 20 | impl RunError { 21 | fn new(code: u8, command: &str) -> Option { 22 | match code { 23 | 0 => Some(Self::CouldNotExecuteCommand(command.to_string())), 24 | 1 => Some(Self::CommandEncounteredError(command.to_string())), 25 | 2 => Some(Self::CommandWasInterrupted(command.to_string())), 26 | _ => None, 27 | } 28 | } 29 | 30 | fn could_not_execute_command() -> u8 { 0 } 31 | fn command_encountered_error() -> u8 { 1 } 32 | fn command_was_interrupted() -> u8 { 2 } 33 | } 34 | 35 | impl fmt::Display for RunError { 36 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 37 | match self { 38 | RunError::CouldNotExecuteCommand(command) => write!(f, "could not execute {}", command), 39 | RunError::CommandEncounteredError(command) => write!(f, "{} encountered an error", command), 40 | RunError::CommandWasInterrupted(command) => write!(f, "{} was interrupted by signal", command), 41 | } 42 | } 43 | } 44 | 45 | impl error::Error for RunError {} 46 | 47 | // deliberately mis-spelled 48 | static ERROR_SIGNAL: [u8; 4] = [0xde, 0xad, 0xbe, 0xaf]; 49 | 50 | fn format_error(is_tty: bool, error: Box) -> String { 51 | if is_tty { 52 | format!("\x1b[0m\x1b[31m[ea]: {}\x1b[0m\n", error) 53 | } else { 54 | format!("[ea]: {}\n", error) 55 | } 56 | } 57 | 58 | pub fn run(style: &Style, executable: &str, arguments: &[String], debug: Option) { 59 | let is_tty = atty::is(atty::Stream::Stdout); 60 | let mut output = execute(is_tty, executable, arguments); 61 | let output_len = output.len(); 62 | let mut error_exit_code: Option = None; 63 | if output_len >= 4 && output[(output_len - 4)..] == ERROR_SIGNAL { 64 | _ = io::stderr().write(&output); 65 | let error = RunError::new(output[output_len - 5], executable).expect("Error synthesized"); 66 | _ = io::stderr().write(format_error(is_tty, Box::new(error)).as_bytes()); 67 | error_exit_code = Some(output[output_len - 6] as i32); 68 | output = output[0..(output_len - 6)].to_vec(); 69 | } 70 | 71 | let parsed = match style { 72 | Style::Grouped => parsers::grouped::grouped, 73 | Style::Linear => parsers::linear::linear, 74 | Style::Search => parsers::search::search, 75 | Style::Rust => parsers::rust::rust, 76 | Style::Py => parsers::python::python, 77 | }(&output); 78 | 79 | let (display, locations) = match parsed { 80 | Ok(result) => result, 81 | Err(error) => { 82 | _ = io::stdout().write(&output); 83 | _ = io::stderr().write(format_error(is_tty, Box::new(error)).as_bytes()); 84 | return; 85 | } 86 | }; 87 | 88 | if error_exit_code.is_none() || !&locations.is_empty() { 89 | _ = io::stdout().write(&display); 90 | _ = archive::write(&locations); 91 | } // else we would already let the executable print its errors, and we have nothing to add 92 | 93 | if let Some(debug_path) = debug { 94 | _ = fs::write( 95 | format!("{}.args", debug_path), 96 | format!("{:?}\n{}\n{:?}", style, executable, arguments), 97 | ); 98 | _ = fs::write(format!("{}.in", debug_path), output); 99 | _ = fs::write(format!("{}.out", debug_path), &display); 100 | } 101 | 102 | if let Some(code) = error_exit_code { 103 | process::exit(code) 104 | } 105 | } 106 | 107 | fn execute_simple(executable: &str, arguments: &[String], output: &mut Vec) -> i32 { 108 | // We must run with .status() as opposed to .output() because we might be in a pty. 109 | // Running .output() would convince the process it's not in a pty! 110 | let execution_result = process::Command::new(executable).args(arguments).status(); 111 | 112 | let error_code: u8; 113 | let exit: u8; 114 | match execution_result { 115 | Ok(exit_status) => { 116 | if let Some(exit_code) = exit_status.code() { 117 | exit = exit_code as u8; 118 | // if exit is 0, this following error won't really be used later. 119 | error_code = RunError::command_encountered_error(); 120 | } else { 121 | exit = 1; 122 | error_code = RunError::command_was_interrupted(); 123 | } 124 | } 125 | Err(error) => { 126 | output.extend_from_slice(error.to_string().as_bytes()); 127 | exit = error.raw_os_error().unwrap_or(1) as u8; 128 | error_code = RunError::could_not_execute_command(); 129 | } 130 | } 131 | 132 | // We are in a child process, whose entire output will be read by the parent. To signal to the 133 | // parent that something went wrong, we print out a special sequence at the end of the output, 134 | // proceeded by an error code, proceeded by an exit status. Any error produced by the external 135 | // command is attached after the error signal sequence. 136 | if exit != 0 { 137 | output.push(exit); 138 | output.push(error_code); 139 | output.extend_from_slice(&ERROR_SIGNAL); 140 | } 141 | 142 | exit as i32 143 | } 144 | 145 | fn execute(is_tty: bool, executable: &str, arguments: &[String]) -> Vec { 146 | let mut output = Vec::new(); 147 | if is_tty { 148 | let fork = Fork::from_ptmx().unwrap(); 149 | if let Ok(mut parent) = fork.is_parent() { 150 | _ = parent.read_to_end(&mut output); 151 | } else { 152 | let code = execute_simple(executable, arguments, &mut output); 153 | if code != 0 { 154 | _ = io::stderr().write(&output); 155 | } 156 | process::exit(code); 157 | } 158 | } else { 159 | _ = execute_simple(executable, arguments, &mut output); 160 | } 161 | output 162 | } 163 | -------------------------------------------------------------------------------- /src/interface.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgEnum, Parser, Subcommand, ValueHint}; 2 | 3 | /// ea - Making command line output actionable 4 | /// 5 | /// This tool captures paths from command line tools, and let you follow them up with actions. 6 | /// When you run a command line tool through `ea run`, its output containing file paths will be 7 | /// prefixed with a NUMBER. Afterwards, `ea print NUMBER` will output the associated path so that 8 | /// you can use it as part of other commands (e.g. `vim $(ea p 42)` to open path #42 in vim). 9 | /// 10 | /// For more details, see `man ea`, or documentation at https://github.com/dduan/ea 11 | #[derive(Debug, Parser)] 12 | #[clap(author, version, about)] 13 | pub struct Interface { 14 | #[clap(subcommand)] 15 | pub subcommand: Option, 16 | } 17 | 18 | #[derive(ArgEnum, Clone, Debug)] 19 | pub enum Style { 20 | Grouped, 21 | Linear, 22 | Search, 23 | Rust, 24 | Py, 25 | } 26 | 27 | #[derive(Debug, Subcommand)] 28 | pub enum Commands { 29 | /// Run EXECUTABLE through `ea`. Expecting its output to be the format of STYLE. Arguments for 30 | /// EXECUTABLE must come after `--`. For example, `rg Vec src` becomes: 31 | /// 32 | /// ea run grouped rg -- Vec src 33 | /// 34 | /// (rg's default output is in the "grouped" STYLE). 35 | Run { 36 | /// Format of output from EXECUTABLE. ea looks for file paths, lines, and columns within 37 | /// the file at the path. A file path can have one or more "locations". A location has at 38 | /// least a file path, and a line number, with an optional column. 39 | /// 40 | /// [grouped]: A file path followed by a list of locations, then the next file path and its 41 | /// locations, etc. Example: ripgrep's default output format. 42 | /// 43 | /// [linear]: An location on each line. Example: fd/find's default output format. 44 | /// 45 | /// [search]: Locations at the start of line, with additional content on the same line, 46 | /// followed by lots of other content, followed by another location. Example: 47 | /// clang/swift's default format. 48 | /// 49 | /// For more explanation, see `man ea`, or documentation at http://github.com/dduan/ea 50 | #[clap(arg_enum)] 51 | style: Style, 52 | #[clap(value_hint = ValueHint::CommandName)] 53 | /// The command to execute. 54 | executable: String, 55 | #[clap(last = true)] 56 | /// Arguments for EXECUTABLE. Must be separated from EXECUTABLE with `--` (two dashes). 57 | arguments: Vec, 58 | /// Write debug info at 59 | #[clap(long, value_name = "debug_files_base_name", value_hint = ValueHint::FilePath)] 60 | debug: Option, 61 | }, 62 | 63 | /// List locations found from the latest `ea run` output. This is the default subcommand. 64 | /// Running `ea` is the same as running `ea list`. 65 | List, 66 | 67 | /// Print the location info associated with NUMBER. Optionally, customize the output FORMAT. 68 | /// Also availble as the shorthand `ea p ...` 69 | #[clap(alias("p"))] 70 | Print { 71 | /// The number associated with a file location from the latest `ea run` output. 72 | #[clap(required = true)] 73 | number: usize, 74 | /// A string representing the format of the location to be printed. `{path}`, `{line}`, and `{column}` 75 | /// in this string will be replaced with corresponding values within the location. For 76 | /// example, 'L{line}C{column} @ {path}' might print out 'L23C11 @ path/to/file'. If line 77 | /// or column info isn't available, they'll be filled with '0'. 78 | #[clap(default_value = "{path}")] 79 | format: String, 80 | }, 81 | } 82 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod archive; 2 | mod parsers; 3 | pub mod commands; 4 | pub mod interface; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | use std::option::Option; 8 | 9 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd)] 10 | pub struct Location { 11 | pub path: String, 12 | pub line: Option, 13 | pub column: Option, 14 | } 15 | -------------------------------------------------------------------------------- /src/parsers.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use regex::Regex; 3 | use std::{fmt, error}; 4 | use guard::guard; 5 | use crate::Location; 6 | 7 | pub mod grouped; 8 | pub mod linear; 9 | pub mod search; 10 | pub mod rust; 11 | pub mod python; 12 | 13 | lazy_static! { 14 | static ref RE_ANSI_CODE: Regex = Regex::new(r#"(\x1b\[[0-9;]*m|\x1b\[[0-9;]*K)"#).unwrap(); 15 | } 16 | 17 | pub fn search_pattern(pattern: &Regex, input: &[u8]) -> Result<(Vec, Vec), ParseError> { 18 | let mut output = String::new(); 19 | let mut start: usize = 0; 20 | let mut locations: Vec = Vec::new(); 21 | guard!(let Ok(input_str) = std::str::from_utf8(input) else { 22 | return Result::Err(ParseError::FailedEncoding); 23 | }); 24 | for captures in pattern.captures_iter(input_str) { 25 | let path_match = captures.name("path").unwrap(); 26 | let line = captures.name("line").and_then(|x| x.as_str().parse::().ok()).unwrap_or(1); 27 | let column = captures.name("column").and_then(|x| x.as_str().parse::().ok()); 28 | locations.push(Location { 29 | path: path_match.as_str().to_string(), 30 | line: Some(line), 31 | column, 32 | }); 33 | output = format!( 34 | "{}{}\x1b[0m[\x1b[31m{}\x1b[0m] ", 35 | output, 36 | &input_str[start..path_match.start()], 37 | locations.len() 38 | ); 39 | start = path_match.start(); 40 | } 41 | 42 | output = format!("{}{}", output, &input_str[start..]); 43 | 44 | let output_data: Vec = output.as_bytes().to_owned(); 45 | Ok((output_data, locations)) 46 | } 47 | 48 | fn append_line(output: &mut String, location_number: usize, line: &str) { 49 | *output = format!( 50 | "{}\x1b[0m[\x1b[31m{}\x1b[0m] {}\n", 51 | output, location_number, line 52 | ); 53 | } 54 | 55 | #[derive(Debug)] 56 | pub enum ParseError { 57 | FailedEncoding, 58 | } 59 | 60 | impl error::Error for ParseError {} 61 | impl fmt::Display for ParseError { 62 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 63 | write!(f, "Could not decode input as UTF-8 string") 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | pub mod tests { 69 | use lazy_static::lazy_static; 70 | use std::path::PathBuf; 71 | use std::str; 72 | 73 | lazy_static! { 74 | static ref FIXTURES: PathBuf = [ 75 | env!("CARGO_MANIFEST_DIR"), 76 | "resources", 77 | "fixtures", 78 | "parsers" 79 | ] 80 | .iter() 81 | .collect(); 82 | } 83 | 84 | pub fn fixture(path: &str) -> PathBuf { 85 | let mut result = FIXTURES.clone(); 86 | result.push(path); 87 | result 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/parsers/grouped.rs: -------------------------------------------------------------------------------- 1 | use crate::parsers::{append_line, ParseError, RE_ANSI_CODE}; 2 | use crate::Location; 3 | use guard::guard; 4 | use lazy_static::lazy_static; 5 | use regex::Regex; 6 | 7 | pub fn grouped(input: &[u8]) -> Result<(Vec, Vec), ParseError> { 8 | lazy_static! { 9 | static ref RE_LINE: Regex = Regex::new(r#"^(\d+):(?:(\d+):)?.+?"#).unwrap(); 10 | } 11 | 12 | let mut output = String::new(); 13 | guard!(let Ok(input_str) = std::str::from_utf8(input) else { 14 | return Result::Err(ParseError::FailedEncoding); 15 | }); 16 | 17 | let mut locations: Vec = Vec::new(); 18 | let mut file: Option = None; 19 | let mut striped: String; 20 | for line in input_str.lines() { 21 | striped = RE_ANSI_CODE.replace_all(line, "").to_string(); 22 | if let Some(line_match) = RE_LINE.captures(&striped) { 23 | if let Ok(line_number) = line_match.get(1).unwrap().as_str().parse::() { 24 | if let Some(current_file) = &file { 25 | let column: Option = line_match 26 | .get(2) 27 | .and_then(|x| x.as_str().parse::().ok()); 28 | let new_location = Location { 29 | path: current_file.to_string(), 30 | line: Some(line_number), 31 | column, 32 | }; 33 | 34 | locations.push(new_location); 35 | } 36 | 37 | append_line(&mut output, locations.len(), line); 38 | } 39 | continue; 40 | } else if !striped.is_empty() { 41 | file = Some(striped); 42 | } 43 | output = format!("{}{}\n", output, line); 44 | } 45 | 46 | let output_data: Vec = output.as_bytes().to_owned(); 47 | Ok((output_data, locations)) 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use super::grouped; 53 | use crate::archive::read_from; 54 | use crate::parsers::tests::fixture; 55 | use crate::Location; 56 | use std::fs; 57 | 58 | #[test] 59 | fn test_grouped_output() { 60 | let input = fs::read(fixture("grouped.in.txt")).expect("input file"); 61 | let expected_output = fs::read(fixture("grouped.out.txt")).expect("output file"); 62 | let output = grouped(&input); 63 | assert_eq!(output.expect("grouped output").0, expected_output); 64 | } 65 | 66 | #[test] 67 | fn test_grouped_locations() { 68 | let input = fs::read(fixture("grouped.in.txt")).expect("input file"); 69 | let expected_locations: Vec = read_from(&fixture("grouped_locations.bin")); 70 | let output = grouped(&input); 71 | assert_eq!(output.expect("grouped output").1, expected_locations); 72 | } 73 | 74 | #[test] 75 | fn test_grouped_output2() { 76 | let input = fs::read(fixture("grouped2.in.txt")).expect("input file"); 77 | let expected_output = fs::read(fixture("grouped2.out.txt")).expect("output file"); 78 | let output = grouped(&input); 79 | assert_eq!(output.expect("grouped output").0, expected_output); 80 | } 81 | 82 | #[test] 83 | fn test_grouped_locations2() { 84 | let input = fs::read(fixture("grouped2.in.txt")).expect("input file"); 85 | let expected_locations: Vec = read_from(&fixture("grouped2_locations.bin")); 86 | let output = grouped(&input); 87 | assert_eq!(output.expect("grouped output").1, expected_locations); 88 | } 89 | 90 | #[test] 91 | fn test_grouped_output3() { 92 | let input = fs::read(fixture("grouped3.in.txt")).expect("input file"); 93 | let expected_output = fs::read(fixture("grouped3.out.txt")).expect("output file"); 94 | let output = grouped(&input); 95 | assert_eq!(output.expect("grouped output").0, expected_output); 96 | } 97 | 98 | #[test] 99 | fn test_grouped_locations3() { 100 | let input = fs::read(fixture("grouped3.in.txt")).expect("input file"); 101 | let expected_locations: Vec = read_from(&fixture("grouped3_locations.bin")); 102 | let output = grouped(&input); 103 | assert_eq!(output.expect("grouped output").1, expected_locations); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/parsers/linear.rs: -------------------------------------------------------------------------------- 1 | use crate::parsers::{append_line, ParseError, RE_ANSI_CODE}; 2 | use crate::Location; 3 | use guard::guard; 4 | 5 | pub fn linear(input: &[u8]) -> Result<(Vec, Vec), ParseError> { 6 | let mut output = String::new(); 7 | let mut locations: Vec = Vec::new(); 8 | guard!(let Ok(input_str) = std::str::from_utf8(input) else { 9 | return Result::Err(ParseError::FailedEncoding); 10 | }); 11 | for line in input_str.lines() { 12 | if line.is_empty() { 13 | continue; 14 | } 15 | 16 | let striped = RE_ANSI_CODE.replace_all(line, ""); 17 | let parts: Vec<&str> = striped.split(':').collect(); 18 | let path = parts[0].to_string(); 19 | let line_number = if parts.len() > 1 { 20 | parts[1].parse::().ok() 21 | } else { 22 | None 23 | }; 24 | let column_number = if parts.len() > 2 { 25 | parts[2].parse::().ok() 26 | } else { 27 | None 28 | }; 29 | 30 | locations.push(Location { 31 | path, 32 | line: line_number, 33 | column: column_number, 34 | }); 35 | 36 | append_line(&mut output, locations.len(), line); 37 | } 38 | let output_data: Vec = output.as_bytes().to_owned(); 39 | Ok((output_data, locations)) 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use super::linear; 45 | use crate::archive::read_from; 46 | use crate::parsers::tests::fixture; 47 | use crate::Location; 48 | use std::fs; 49 | 50 | #[test] 51 | fn test_linear_colored_output() { 52 | let input = fs::read(fixture("linear_colored.in.txt")).expect("input file"); 53 | let expected_output = fs::read(fixture("linear_colored.out.txt")).expect("output file"); 54 | let output = linear(&input); 55 | assert_eq!(output.expect("linear output").0, expected_output); 56 | } 57 | 58 | #[test] 59 | fn test_linear_colored_locations() { 60 | let input = fs::read(fixture("linear_colored.in.txt")).expect("input file"); 61 | let expected_locations: Vec = read_from(&fixture("linear_colored_locations.bin")); 62 | let output = linear(&input); 63 | assert_eq!(output.expect("linear output").1, expected_locations); 64 | } 65 | 66 | #[test] 67 | fn test_linear_output() { 68 | let input = fs::read(fixture("linear.in.txt")).expect("input file"); 69 | let expected_output = fs::read(fixture("linear.out.txt")).expect("output file"); 70 | let output = linear(&input); 71 | assert_eq!(output.expect("linear output").0, expected_output); 72 | } 73 | 74 | #[test] 75 | fn test_linear_locations() { 76 | let input = fs::read(fixture("linear.in.txt")).expect("input file"); 77 | let expected_locations: Vec = read_from(&fixture("linear_locations.bin")); 78 | let output = linear(&input); 79 | assert_eq!(output.expect("linear output").1, expected_locations); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/parsers/python.rs: -------------------------------------------------------------------------------- 1 | use crate::Location; 2 | use lazy_static::lazy_static; 3 | use regex::Regex; 4 | use crate::parsers::{ParseError, search_pattern}; 5 | 6 | lazy_static! { 7 | static ref RE_LINE: Regex = 8 | Regex::new(r#"(\x1b\[[0-9;]*m?)? File (\x1b\[[0-9;]*m?)*"(?P[^:\n\r]+)"(\x1b\[[0-9;]*m?)*, line (?P\x1b\[[0-9;]*m?)*(\d+)"#).unwrap(); 9 | } 10 | 11 | pub fn python(input: &[u8]) -> Result<(Vec, Vec), ParseError> { 12 | search_pattern(&RE_LINE, input) 13 | } 14 | 15 | #[cfg(test)] 16 | mod tests { 17 | use super::python; 18 | use crate::archive::read_from; 19 | use crate::{parsers::tests::fixture, Location}; 20 | use std::fs; 21 | 22 | #[test] 23 | fn test_python_output() { 24 | let input = fs::read(fixture("python3.in.txt")).expect("input file"); 25 | let expected_output = fs::read(fixture("python3.out.txt")).expect("output file"); 26 | let output = python(&input); 27 | assert_eq!(output.expect("python3 output").0, expected_output); 28 | } 29 | 30 | #[test] 31 | fn test_python_locations() { 32 | let input = fs::read(fixture("python3.in.txt")).expect("input file"); 33 | let expected_locations: Vec = read_from(&fixture("python3_locations.bin")); 34 | let output = python(&input); 35 | assert_eq!(output.expect("python3 output").1, expected_locations); 36 | } 37 | 38 | #[test] 39 | fn test_ipython_output() { 40 | let input = fs::read(fixture("ipython3.in.txt")).expect("input file"); 41 | let expected_output = fs::read(fixture("ipython3.out.txt")).expect("output file"); 42 | let output = python(&input); 43 | assert_eq!(output.expect("python output").0, expected_output); 44 | } 45 | 46 | #[test] 47 | fn test_ipython_locations() { 48 | let input = fs::read(fixture("ipython3.in.txt")).expect("input file"); 49 | let expected_locations: Vec = read_from(&fixture("ipython3_locations.bin")); 50 | let output = python(&input); 51 | assert_eq!(output.expect("python output").1, expected_locations); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/parsers/rust.rs: -------------------------------------------------------------------------------- 1 | use crate::Location; 2 | use lazy_static::lazy_static; 3 | use regex::Regex; 4 | use crate::parsers::{ParseError, search_pattern}; 5 | 6 | lazy_static! { 7 | static ref RE_LINE: Regex = 8 | Regex::new(r#" (\x1b\[[0-9;]*m?)*--> (\x1b\[[0-9;]*m?)*(?P[^:\n\r]+):(?P\d+)(:(?P\d+))?"#).unwrap(); 9 | } 10 | 11 | pub fn rust(input: &[u8]) -> Result<(Vec, Vec), ParseError> { 12 | search_pattern(&RE_LINE, input) 13 | } 14 | 15 | #[cfg(test)] 16 | mod tests { 17 | use super::rust; 18 | use crate::archive::read_from; 19 | use crate::{parsers::tests::fixture, Location}; 20 | use std::fs; 21 | 22 | #[test] 23 | fn test_rust_output() { 24 | let input = fs::read(fixture("rust.in.txt")).expect("input file"); 25 | let expected_output = fs::read(fixture("rust.out.txt")).expect("output file"); 26 | let output = rust(&input); 27 | assert_eq!(output.expect("rust output").0, expected_output); 28 | } 29 | 30 | #[test] 31 | fn test_rust_locations() { 32 | let input = fs::read(fixture("rust.in.txt")).expect("input file"); 33 | let expected_locations: Vec = read_from(&fixture("rust_locations.bin")); 34 | let output = rust(&input); 35 | assert_eq!(output.expect("rust output").1, expected_locations); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/parsers/search.rs: -------------------------------------------------------------------------------- 1 | use crate::Location; 2 | use lazy_static::lazy_static; 3 | use regex::Regex; 4 | use crate::parsers::{ParseError, search_pattern}; 5 | 6 | lazy_static! { 7 | static ref RE_LINE: Regex = 8 | Regex::new(r#"(\r|\n)(\x1b\[[0-9;]*m?)*(?P[^:\n\r ]+):(?P\d+)(:(?P\d+))?"#).unwrap(); 9 | } 10 | 11 | pub fn search(input: &[u8]) -> Result<(Vec, Vec), ParseError> { 12 | search_pattern(&RE_LINE, input) 13 | } 14 | 15 | #[cfg(test)] 16 | mod tests { 17 | use super::search; 18 | use crate::archive::read_from; 19 | use crate::parsers::tests::fixture; 20 | use crate::Location; 21 | use std::fs; 22 | 23 | #[test] 24 | fn test_search_output() { 25 | let input = fs::read(fixture("search.in.txt")).expect("input file"); 26 | let expected_output = fs::read(fixture("search.out.txt")).expect("output file"); 27 | let output = search(&input); 28 | assert_eq!(output.expect("search output").0, expected_output); 29 | } 30 | 31 | #[test] 32 | fn test_search_locations() { 33 | let input = fs::read(fixture("search.in.txt")).expect("input file"); 34 | let expected_locations: Vec = read_from(&fixture("search_locations.bin")); 35 | let output = search(&input); 36 | assert_eq!(output.expect("search output").1, expected_locations); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/intergation_tests.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::prelude::CommandCargoExt; 2 | use std::path::PathBuf; 3 | use std::process; 4 | use std::io::Read; 5 | use pty::fork::Fork; 6 | 7 | fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option { 8 | haystack.windows(needle.len()).position(|window| window == needle) 9 | } 10 | 11 | #[test] 12 | // Run `ea run` in a terminal with a sample app. Check output is reasonably modified by `ea`. 13 | fn invoke_subprocess_via_pty() -> Result<(), Box> { 14 | let mut ea = process::Command::cargo_bin("ea")?; 15 | let printer_cmd = process::Command::cargo_bin("printfile")?; 16 | let printer = printer_cmd.get_program(); 17 | let input_path: PathBuf = [ 18 | env!("CARGO_MANIFEST_DIR"), 19 | "resources", 20 | "fixtures", 21 | "parsers", 22 | "grouped.in.txt" 23 | ].iter().collect(); 24 | 25 | let fork = Fork::from_ptmx().unwrap(); 26 | let mut output = Vec::new(); 27 | if let Ok(mut parent) = fork.is_parent() { 28 | _ = parent.read_to_end(&mut output); 29 | } else { 30 | ea 31 | .args(["run", "grouped"]) 32 | .arg(printer) 33 | .arg("--") 34 | .arg(input_path) 35 | .status() 36 | .expect("execution fails"); 37 | process::exit(0) 38 | } 39 | assert!(find_subsequence(&output, b"[1] 20:").is_some()); 40 | Ok(()) 41 | } 42 | 43 | #[test] 44 | fn subprocess_command_error() -> Result<(), Box> { 45 | let mut ea = process::Command::cargo_bin("ea")?; 46 | let fork = Fork::from_ptmx().unwrap(); 47 | let mut output = Vec::new(); 48 | if let Ok(mut parent) = fork.is_parent() { 49 | _ = parent.read_to_end(&mut output); 50 | } else { 51 | ea 52 | .args(["run", "rust", "cargo", "--", "clippppy"]) 53 | .status() 54 | .expect("execution fails"); 55 | process::exit(0) 56 | } 57 | assert!(find_subsequence(&output, b"[ea]: cargo encountered an error").is_some()); 58 | Ok(()) 59 | } 60 | 61 | #[test] 62 | fn subprocess_execution_fail() -> Result<(), Box> { 63 | let mut ea = process::Command::cargo_bin("ea")?; 64 | let fork = Fork::from_ptmx().unwrap(); 65 | let mut output = Vec::new(); 66 | if let Ok(mut parent) = fork.is_parent() { 67 | _ = parent.read_to_end(&mut output); 68 | } else { 69 | ea 70 | .args(["run", "rust", "/lmao/i/do/not/exist"]) 71 | .status() 72 | .expect("execution fails"); 73 | process::exit(0) 74 | } 75 | assert!(find_subsequence(&output, b"[ea]: could not execute /lmao/i/do/not/exist").is_some()); 76 | Ok(()) 77 | } 78 | --------------------------------------------------------------------------------