diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..8f689a84 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: ogham diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..b2dad65a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,8 @@ +--- +name: exa is unmaintained +about: Please use the active fork eza instead. +--- + +exa is unmaintained, please use the active fork eza instead. + +--- diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 00000000..12816dab --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,46 @@ +name: Unit tests + +on: + push: + branches: [ master ] + paths: + - '.github/workflows/*' + - 'src/**' + - 'Cargo.*' + - build.rs + pull_request: + branches: [ master ] + paths: + - '.github/workflows/*' + - 'src/**' + - 'Cargo.*' + - build.rs + +env: + CARGO_TERM_COLOR: always + +jobs: + unit-tests: + runs-on: ${{ matrix.os }} + + continue-on-error: ${{ matrix.rust == 'nightly' }} + + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + rust: [1.66.1, stable, beta, nightly] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + + - name: Install cargo-hack + run: cargo install cargo-hack@0.5.27 + + - name: Run unit tests + run: cargo hack test --feature-powerset diff --git a/.gitignore b/.gitignore index ede6ac03..cb7baef0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ target # Vagrant stuff .vagrant -ubuntu-xenial-16.04-cloudimg-console.log +*.log # Compiled artifacts # (see devtools/*-package-for-*.sh) diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 00000000..c7ad93ba --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +disable_all_formatting = true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 917b2d0f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -before_install: - - sudo add-apt-repository --yes ppa:kubuntu-ppa/backports - - sudo apt-get update -qq - - sudo apt-get install cmake -sudo: true -language: rust -rust: - - stable -script: - - cargo build --verbose - - cargo test --verbose - - cargo build --verbose --no-default-features - - cargo test --verbose --no-default-features diff --git a/Cargo.lock b/Cargo.lock index 8323b908..5ee181df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,537 +1,396 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -[[package]] -name = "aho-corasick" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", -] +version = 3 [[package]] name = "ansi_term" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "atty" -version = "0.2.13" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi", ] [[package]] name = "autocfg" -version = "0.1.5" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bitflags" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "byteorder" -version = "1.3.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cc" -version = "1.0.37" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +dependencies = [ + "jobserver", +] [[package]] name = "cfg-if" -version = "0.1.9" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "datetime" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "iso8601 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", - "locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", - "pad 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "env_logger" -version = "0.6.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c3f7a77f3e57fedf80e09136f2d8777ebf621207306f6d96d610af048354bc" dependencies = [ - "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", - "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.1.9 (registry+https://github.com/rust-lang/crates.io-index)", - "termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "locale", + "pad", + "redox_syscall", + "winapi", ] [[package]] name = "exa" -version = "0.9.0" +version = "0.10.1" +dependencies = [ + "ansi_term", + "datetime", + "git2", + "glob", + "lazy_static", + "libc", + "locale", + "log", + "natord", + "num_cpus", + "number_prefix", + "scoped_threadpool", + "term_grid", + "terminal_size", + "unicode-width", + "users", + "zoneinfo_compiled", +] + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ - "ansi_term 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", - "datetime 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "git2 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", - "locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "natord 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)", - "number_prefix 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", - "term_grid 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "users 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", - "zoneinfo_compiled 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "matches", + "percent-encoding", ] [[package]] name = "git2" -version = "0.9.1" +version = "0.13.20" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9831e983241f8c5591ed53f17d874833e2fa82cac2625f3888c50cbfe136cba" dependencies = [ - "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", - "libgit2-sys 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.9.48 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-sys", + "url", ] [[package]] name = "glob" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] -name = "humantime" -version = "1.2.0" +name = "hermit-abi" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" dependencies = [ - "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", ] [[package]] name = "idna" -version = "0.1.5" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "matches", + "unicode-bidi", + "unicode-normalization", ] [[package]] -name = "iso8601" -version = "0.1.1" +name = "jobserver" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "972f5ae5d1cb9c6ae417789196c803205313edde988685da5e3aae0827b9e7fd" dependencies = [ - "nom 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "kernel32-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", ] [[package]] name = "lazy_static" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.60" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" [[package]] name = "libgit2-sys" -version = "0.8.1" +version = "0.12.21+1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86271bacd72b2b9e854c3dcfb82efd538f15f870e4c11af66900effb462f6825" dependencies = [ - "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", - "libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "cc", + "libc", + "libz-sys", + "pkg-config", ] [[package]] name = "libz-sys" -version = "1.0.25" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" dependencies = [ - "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", - "vcpkg 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "cc", + "libc", + "pkg-config", + "vcpkg", ] [[package]] name = "locale" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd" dependencies = [ - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", ] [[package]] name = "log" -version = "0.4.7" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", ] [[package]] name = "matches" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "memchr" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" [[package]] name = "natord" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "nom" -version = "1.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "num-traits" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "num-traits" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" [[package]] name = "num_cpus" -version = "1.10.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" dependencies = [ - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", + "hermit-abi", + "libc", ] [[package]] name = "number_prefix" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "openssl-src" -version = "111.3.0+1.1.1c" +version = "111.15.0+1.1.1k" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a5f6ae2ac04393b217ea9f700cd04fa9bf3d93fae2872069f3d15d908af70a" dependencies = [ - "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", + "cc", ] [[package]] name = "openssl-sys" -version = "0.9.48" +version = "0.9.61" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313752393519e876837e09e1fa183ddef0be7735868dced3196f4472d536277f" dependencies = [ - "autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-src 111.3.0+1.1.1c (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", - "vcpkg 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", ] [[package]] name = "pad" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3" dependencies = [ - "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width", ] [[package]] name = "percent-encoding" -version = "1.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pkg-config" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "quick-error" -version = "1.2.2" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" [[package]] name = "redox_syscall" -version = "0.1.56" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "regex" -version = "1.1.9" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "aho-corasick 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", - "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "utf8-ranges 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "regex-syntax" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "scoped_threadpool" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "smallvec" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" [[package]] name = "term_grid" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "term_size" -version = "0.3.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c9eb7705cb3f0fd71d3955b23db6d372142ac139e8c473952c93bf3c3dc4b7" dependencies = [ - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width", ] [[package]] -name = "termcolor" -version = "1.0.5" +name = "terminal_size" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406" dependencies = [ - "wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "winapi", ] [[package]] -name = "thread_local" -version = "0.3.6" +name = "tinyvec" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" dependencies = [ - "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tinyvec_macros", ] [[package]] -name = "ucd-util" -version = "0.1.3" +name = "tinyvec_macros" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "unicode-bidi" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" dependencies = [ - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "matches", ] [[package]] name = "unicode-normalization" -version = "0.1.8" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" dependencies = [ - "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" [[package]] name = "url" -version = "1.7.2" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" dependencies = [ - "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "form_urlencoded", + "idna", + "matches", + "percent-encoding", ] [[package]] name = "users" -version = "0.9.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" dependencies = [ - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "log", ] -[[package]] -name = "utf8-ranges" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "vcpkg" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "winapi" -version = "0.2.8" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d" [[package]] name = "winapi" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "winapi-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "wincolor" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "zoneinfo_compiled" -version = "0.4.8" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fbebe65e899530f43bd760b23fda8f141118f4db49952b02998cbd0907a5de" dependencies = [ - "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "datetime 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder", + "datetime", ] - -[metadata] -"checksum aho-corasick 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "36b7aa1ccb7d7ea3f437cf025a2ab1c47cc6c1bc9fc84918ff449def12f5e282" -"checksum ansi_term 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eaa72766c3585a1f812a3387a7e2c6cab780f899c2f43ff6ea06c8d071fcbb36" -"checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" -"checksum autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "22130e92352b948e7e82a49cdb0aa94f2211761117f29e052dd397c1ac33542b" -"checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd" -"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" -"checksum cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "39f75544d7bbaf57560d2168f28fd649ff9c76153874db88bdbdfd839b1a7e7d" -"checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" -"checksum datetime 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "5c44b6c112860e38412e0c4732172d723458d40db906ee4b9ce87544f022a7b9" -"checksum env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" -"checksum git2 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "924b2e7d2986e625dcad89e8a429a7b3adee3c3d71e585f4a66c4f7e78715e31" -"checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" -"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114" -"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" -"checksum iso8601 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "11dc464f8c6f17595d191447c9c6559298b2d023d6f846a4a23ac7ea3c46c477" -"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" -"checksum libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)" = "d44e80633f007889c7eff624b709ab43c92d708caad982295768a7b13ca3b5eb" -"checksum libgit2-sys 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "941a41e23f77323b8c9d2ee118aec9ee39dfc176078c18b4757d3bad049d9ff7" -"checksum libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe" -"checksum locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd" -"checksum log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c275b6ad54070ac2d665eef9197db647b32239c9d244bfb6f041a766d00da5b3" -"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" -"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" -"checksum natord 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" -"checksum nom 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" -"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" -"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" -"checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273" -"checksum number_prefix 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a" -"checksum openssl-src 111.3.0+1.1.1c (registry+https://github.com/rust-lang/crates.io-index)" = "53ed5f31d294bdf5f7a4ba0a206c2754b0f60e9a63b7e3076babc5317873c797" -"checksum openssl-sys 0.9.48 (registry+https://github.com/rust-lang/crates.io-index)" = "b5ba300217253bcc5dc68bed23d782affa45000193866e025329aa8a7a9f05b8" -"checksum pad 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9b8de33465981073e32e1d75bb89ade49062bb853e7c97ec2c13439095563a" -"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" -"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" -"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" -"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" -"checksum regex 1.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "d9d8297cc20bbb6184f8b45ff61c8ee6a9ac56c156cec8e38c3e5084773c44ad" -"checksum regex-syntax 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "9b01330cce219c1c6b2e209e5ed64ccd587ae5c67bed91c0b49eecf02ae40e21" -"checksum scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" -"checksum smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7" -"checksum term_grid 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "230d3e804faaed5a39b08319efb797783df2fd9671b39b7596490cb486d702cf" -"checksum term_size 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9e5b9a66db815dcfd2da92db471106457082577c3c278d4138ab3e3b4e189327" -"checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e" -"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" -"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86" -"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" -"checksum unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "141339a08b982d942be2ca06ff8b076563cbe223d1befd5450716790d44e2426" -"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" -"checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" -"checksum users 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c72f4267aea0c3ec6d07eaabea6ead7c5ddacfafc5e22bcf8d186706851fb4cf" -"checksum utf8-ranges 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9d50aa7650df78abf942826607c62468ce18d9019673d4a2ebe1865dbb96ffde" -"checksum vcpkg 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "33dd455d0f96e90a75803cfeb7f948768c08d70a6de9a8d2362461935698bf95" -"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" -"checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" -"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" -"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" -"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -"checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba" -"checksum zoneinfo_compiled 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6e136f8e8905dcf086773dbb987be12b149e240bf4039dce9d068774780ad52e" diff --git a/Cargo.toml b/Cargo.toml index b683c860..447b7885 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,73 +1,72 @@ [package] name = "exa" -version = "0.9.0" -authors = [ "Benjamin Sago " ] -build = "build.rs" -edition = "2018" - description = "A modern replacement for ls" -homepage = "https://the.exa.website/" -repository = "https://github.com/ogham/exa" -documentation = "https://github.com/ogham/exa" - -readme = "README.md" +authors = ["Benjamin Sago "] categories = ["command-line-utilities"] -keywords = ["ls", "files", "command-line"] +edition = "2021" +rust-version = "1.66.1" +exclude = ["/devtools/*", "/Justfile", "/Vagrantfile", "/screenshots.png"] +readme = "README.md" +homepage = "https://the.exa.website/" license = "MIT" -exclude = ["/devtools/*", "/Makefile", "/Vagrantfile", "/screenshots.png"] +repository = "https://github.com/ogham/exa" +version = "0.10.1" [[bin]] name = "exa" -path = "src/bin/main.rs" -doc = false - -[lib] -name = "exa" -path = "src/exa.rs" [dependencies] -ansi_term = "0.12.0" -datetime = "0.4.7" -env_logger = "0.6.1" -glob = "0.3.0" -lazy_static = "1.3.0" -libc = "0.2.51" -locale = "0.2.2" -log = "0.4.6" -natord = "1.0.9" -num_cpus = "1.10.0" -number_prefix = "0.3.0" -scoped_threadpool = "0.1.9" -term_grid = "0.1.7" -term_size = "0.3.1" -unicode-width = "0.1.5" -users = "0.9.1" -zoneinfo_compiled = "0.4.8" +ansi_term = "0.12" +glob = "0.3" +lazy_static = "1.3" +libc = "0.2" +locale = "0.2" +log = "0.4" +natord = "1.0" +num_cpus = "1.10" +number_prefix = "0.4" +scoped_threadpool = "0.1" +term_grid = "0.2.0" +terminal_size = "0.1.16" +unicode-width = "0.1" +zoneinfo_compiled = "0.5.1" + +[target.'cfg(unix)'.dependencies] +users = "0.11" + +[dependencies.datetime] +version = "0.5.2" +default-features = false +features = ["format"] [dependencies.git2] -version = "0.9.1" +version = "0.13" optional = true default-features = false -[build-dependencies] -datetime = "0.4.7" +[build-dependencies.datetime] +version = "0.5.2" +default-features = false [features] default = [ "git" ] git = [ "git2" ] vendored-openssl = ["git2/vendored-openssl"] -[profile.release] -opt-level = 3 + +# make dev builds faster by excluding debug symbols +[profile.dev] debug = false + +# use LTO for smaller binaries (that take longer to build) +[profile.release] lto = true -panic = "abort" [package.metadata.deb] -license-file = [ "LICENCE" ] +license-file = [ "LICENCE", "4" ] depends = "$auto" extended-description = """ exa is a replacement for ls written in Rust. @@ -76,6 +75,9 @@ section = "utils" priority = "optional" assets = [ [ "target/release/exa", "/usr/bin/exa", "0755" ], - [ "contrib/man/exa.1", "/usr/share/man/man1/exa.1", "0644" ], - [ "contrib/completions.bash", "/etc/bash_completion.d/exa", "0644" ], + [ "target/release/../man/exa.1", "/usr/share/man/man1/exa.1", "0644" ], + [ "target/release/../man/exa_colors.5", "/usr/share/man/man5/exa_colors.5", "0644" ], + [ "completions/bash/exa", "/usr/share/bash-completion/completions/exa", "0644" ], + [ "completions/zsh/_exa", "/usr/share/zsh/site-functions/_exa", "0644" ], + [ "completions/fish/exa.fish", "/usr/share/fish/vendor_completions.d/exa.fish", "0644" ], ] diff --git a/Justfile b/Justfile new file mode 100644 index 00000000..867fc80c --- /dev/null +++ b/Justfile @@ -0,0 +1,110 @@ +all: build test +all-release: build-release test-release + + +#----------# +# building # +#----------# + +# compile the exa binary +@build: + cargo build + +# compile the exa binary (in release mode) +@build-release: + cargo build --release --verbose + +# produce an HTML chart of compilation timings +@build-time: + cargo +nightly clean + cargo +nightly build -Z timings + +# check that the exa binary can compile +@check: + cargo check + + +#---------------# +# running tests # +#---------------# + +# run unit tests +@test: + cargo test --workspace -- --quiet + +# run unit tests (in release mode) +@test-release: + cargo test --workspace --release --verbose + + +#------------------------# +# running extended tests # +#------------------------# + +# run extended tests +@xtests: + xtests/run.sh + +# run extended tests (using the release mode exa) +@xtests-release: + xtests/run.sh --release + +# display the number of extended tests that get run +@count-xtests: + grep -F '[[cmd]]' -R xtests | wc -l + + +#-----------------------# +# code quality and misc # +#-----------------------# + +# lint the code +@clippy: + touch src/main.rs + cargo clippy + +# update dependency versions, and checks for outdated ones +@update-deps: + cargo update + command -v cargo-outdated >/dev/null || (echo "cargo-outdated not installed" && exit 1) + cargo outdated + +# list unused dependencies +@unused-deps: + command -v cargo-udeps >/dev/null || (echo "cargo-udeps not installed" && exit 1) + cargo +nightly udeps + +# check that every combination of feature flags is successful +@check-features: + command -v cargo-hack >/dev/null || (echo "cargo-hack not installed" && exit 1) + cargo hack check --feature-powerset + +# build exa and run extended tests with features disabled +@feature-checks *args: + cargo build --no-default-features + specsheet xtests/features/none.toml -shide {{args}} \ + -O cmd.target.exa="${CARGO_TARGET_DIR:-../../target}/debug/exa" + +# print versions of the necessary build tools +@versions: + rustc --version + cargo --version + + +#---------------# +# documentation # +#---------------# + +# build the man pages +@man: + mkdir -p "${CARGO_TARGET_DIR:-target}/man" + pandoc --standalone -f markdown -t man man/exa.1.md > "${CARGO_TARGET_DIR:-target}/man/exa.1" + pandoc --standalone -f markdown -t man man/exa_colors.5.md > "${CARGO_TARGET_DIR:-target}/man/exa_colors.5" + +# build and preview the main man page (exa.1) +@man-1-preview: man + man "${CARGO_TARGET_DIR:-target}/man/exa.1" + +# build and preview the colour configuration man page (exa_colors.5) +@man-5-preview: man + man "${CARGO_TARGET_DIR:-target}/man/exa_colors.5" diff --git a/Makefile b/Makefile deleted file mode 100644 index b9abb502..00000000 --- a/Makefile +++ /dev/null @@ -1,86 +0,0 @@ -DESTDIR = -PREFIX = /usr/local - -override define compdir -ifndef $(1) -$(1) := $$(or $$(shell pkg-config --variable=completionsdir $(2) 2>/dev/null),$(3)) -endif -endef - -$(eval $(call compdir,BASHDIR,bash-completion,$(PREFIX)/etc/bash_completion.d)) -$(eval $(call compdir,ZSHDIR,zsh,/usr/share/zsh/vendor_completions.d)) -$(eval $(call compdir,FISHDIR,fish,$(PREFIX)/share/fish/vendor_completions.d)) - -FEATURES ?= default -CARGO_OPTS := --no-default-features --features "$(FEATURES)" - -all: target/release/exa -build: target/release/exa - -target/release/exa: - cargo build --release $(CARGO_OPTS) - -install: install-exa install-man - -install-exa: target/release/exa - install -m755 -- target/release/exa "$(DESTDIR)$(PREFIX)/bin/" - -install-man: - install -dm755 -- "$(DESTDIR)$(PREFIX)/bin/" "$(DESTDIR)$(PREFIX)/share/man/man1/" - install -m644 -- contrib/man/exa.1 "$(DESTDIR)$(PREFIX)/share/man/man1/" - -install-bash-completions: - install -m644 -- contrib/completions.bash "$(DESTDIR)$(BASHDIR)/exa" - -install-zsh-completions: - install -m644 -- contrib/completions.zsh "$(DESTDIR)$(ZSHDIR)/_exa" - -install-fish-completions: - install -m644 -- contrib/completions.fish "$(DESTDIR)$(FISHDIR)/exa.fish" - -test: target/release/exa - cargo test --release $(CARGO_OPTS) - -check: test - -uninstall: - -rm -f -- "$(DESTDIR)$(PREFIX)/share/man/man1/exa.1" - -rm -f -- "$(DESTDIR)$(PREFIX)/bin/exa" - -rm -f -- "$(DESTDIR)$(BASHDIR)/exa" - -rm -f -- "$(DESTDIR)$(ZSHDIR)/_exa" - -rm -f -- "$(DESTDIR)$(FISHDIR)/exa.fish" - -clean: - cargo clean - -preview-man: - man contrib/man/exa.1 - -help: - @echo 'Available make targets:' - @echo ' all - build exa (default)' - @echo ' build - build exa' - @echo ' clean - run `cargo clean`' - @echo ' install - build and install exa and manpage' - @echo ' install-exa - build and install exa' - @echo ' install-man - install the manpage' - @echo ' test - run `cargo test`' - @echo ' uninstall - uninstall fish, manpage, and completions' - @echo ' preview-man - preview the manpage without installing' - @echo ' help - print this help' - @echo - @echo ' install-bash-completions - install bash completions into $$BASHDIR' - @echo ' install-zsh-completions - install zsh completions into $$ZSHDIR' - @echo ' install-fish-completions - install fish completions into $$FISHDIR' - @echo - @echo 'Variables:' - @echo ' DESTDIR - A path that'\''s prepended to installation paths (default: "")' - @echo ' PREFIX - The installation prefix for everything except zsh completions (default: /usr/local)' - @echo ' BASHDIR - The directory to install bash completions in (default: $$PREFIX/etc/bash_completion.d)' - @echo ' ZSHDIR - The directory to install zsh completions in (default: /usr/share/zsh/vendor-completions)' - @echo ' FISHDIR - The directory to install fish completions in (default: $$PREFIX/share/fish/vendor_completions.d)' - @echo ' FEATURES - The cargo feature flags to use. Set to an empty string to disable git support' - -.PHONY: all build target/release/exa install-exa install-man preview-man \ - install-bash-completions install-zsh-completions install-fish-completions \ - clean uninstall help diff --git a/README.md b/README.md index 505254c6..3f8c1e52 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,42 @@ -# exa [![Build status](https://travis-ci.org/ogham/exa.svg)](https://travis-ci.org/ogham/exa) +# exa is unmaintained, use the [fork eza](https://github.com/eza-community/eza) instead. -[exa](https://the.exa.website/) is a replacement for `ls` written in Rust. +(This repository isn’t archived because the only person with the rights to do so is unreachable). -## Rationale +--- -**exa** is a modern replacement for the command-line program `ls` that ships with Unix and Linux operating systems, with more features and better defaults. It uses colours to distinguish file types and metadata. It knows about symlinks, extended attributes, and Git. And it’s **small**, **fast**, and just one **single binary**. +
-By deliberately making some decisions differently, exa attempts to be a more featureful, more user-friendly version of `ls`. +# exa + +[exa](https://the.exa.website/) is a modern replacement for _ls_. -## Screenshots +**README Sections:** [Options](#options) — [Installation](#installation) — [Development](#development) + +[![Unit tests](https://github.com/ogham/exa/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/ogham/exa/actions/workflows/unit-tests.yml) +
![Screenshots of exa](screenshots.png) +--- + +**exa** is a modern replacement for the venerable file-listing command-line program `ls` that ships with Unix and Linux operating systems, giving it more features and better defaults. +It uses colours to distinguish file types and metadata. +It knows about symlinks, extended attributes, and Git. +And it’s **small**, **fast**, and just **one single binary**. + +By deliberately making some decisions differently, exa attempts to be a more featureful, more user-friendly version of `ls`. +For more information, see [exa’s website](https://the.exa.website/). + + +--- -## Options + +

Command-line options

+
-exa’s options are almost, but not quite, entirely unlike `ls`'s. +exa’s options are almost, but not quite, entirely unlike `ls`’s. -### Display Options +### Display options - **-1**, **--oneline**: display one entry per line - **-G**, **--grid**: display entries as a grid (default) @@ -29,8 +48,9 @@ exa’s options are almost, but not quite, entirely unlike `ls`'s. - **--colo[u]r**: when to use terminal colours - **--colo[u]r-scale**: highlight levels of file sizes distinctly - **--icons**: display icons +- **--no-icons**: don't display icons (always overrides --icons) -### Filtering Options +### Filtering options - **-a**, **--all**: show hidden and 'dot' files - **-d**, **--list-dirs**: list directories like regular files @@ -44,91 +64,197 @@ exa’s options are almost, but not quite, entirely unlike `ls`'s. Pass the `--all` option twice to also show the `.` and `..` directories. -### Long View Options +### Long view options -These options are available when running with --long (`-l`): +These options are available when running with `--long` (`-l`): - **-b**, **--binary**: list file sizes with binary prefixes - **-B**, **--bytes**: list file sizes in bytes, without any prefixes -- **-g**, **--group**: list each file's group +- **-g**, **--group**: list each file’s group - **-h**, **--header**: add a header row to each column -- **-H**, **--links**: list each file's number of hard links -- **-i**, **--inode**: list each file's inode number +- **-H**, **--links**: list each file’s number of hard links +- **-i**, **--inode**: list each file’s inode number - **-m**, **--modified**: use the modified timestamp field -- **-S**, **--blocks**: list each file's number of file system blocks +- **-S**, **--blocks**: list each file’s number of file system blocks - **-t**, **--time=(field)**: which timestamp field to use - **-u**, **--accessed**: use the accessed timestamp field - **-U**, **--created**: use the created timestamp field -- **-@**, **--extended**: list each file's extended attributes and sizes +- **-@**, **--extended**: list each file’s extended attributes and sizes - **--changed**: use the changed timestamp field -- **--git**: list each file's Git status, if tracked or ignored +- **--git**: list each file’s Git status, if tracked or ignored - **--time-style**: how to format timestamps - **--no-permissions**: suppress the permissions field +- **--octal-permissions**: list each file's permission in octal format - **--no-filesize**: suppress the filesize field - **--no-user**: suppress the user field - **--no-time**: suppress the time field +Some of the options accept parameters: + - Valid **--color** options are **always**, **automatic**, and **never**. - Valid sort fields are **accessed**, **changed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter sort uppercase before lowercase. The modified field has the aliases **date**, **time**, and **newest**, while its reverse has the aliases **age** and **oldest**. - Valid time fields are **modified**, **changed**, **accessed**, and **created**. - Valid time styles are **default**, **iso**, **long-iso**, and **full-iso**. -## Installation +--- -exa is written in [Rust](http://www.rust-lang.org). You will need rustc version 1.35.0 or higher. The recommended way to install Rust is from the official download page. -Once you have it set up, a simple `make install` will compile exa and install it into `/usr/local/bin`. + +

Installation

+
-exa depends on [libgit2](https://github.com/alexcrichton/git2-rs) for certain features. -If you’re unable to compile libgit2, you can opt out of Git support by running `cargo build --release --no-default-features`. +exa is available for macOS and Linux. +More information on how to install exa is available on [the Installation page](https://the.exa.website/install). -If you intend to compile for musl you will need to use the flag vendored-openssl if you want to get the Git feature working: `cargo build --release --target=x86_64-unknown-linux-musl --features vendored-openssl,git` +### Alpine Linux -### Cargo Install +On Alpine Linux, [enable community repository](https://wiki.alpinelinux.org/wiki/Enable_Community_Repository) and install the [`exa`](https://pkgs.alpinelinux.org/package/edge/community/x86_64/exa) package. -If you’re using a recent version of Cargo (0.5.0 or higher), you can use the `cargo install` command: + apk add exa - cargo install exa +### Arch Linux -or: +On Arch, install the [`exa`](https://www.archlinux.org/packages/community/x86_64/exa/) package. - cargo install --no-default-features exa + pacman -S exa -Cargo will build the `exa` binary and place it in `$HOME/.cargo` (this location can be overridden by setting the `--root` option). +### Android / Termux -### Homebrew +On Android / Termux, install the [`exa`](https://github.com/termux/termux-packages/tree/master/packages/exa) package. -If you're using [homebrew](https://brew.sh/), you can use the `brew install` command: + pkg install exa - brew install exa +### Debian -or: +On Debian, install the [`exa`](https://packages.debian.org/stable/exa) package. - brew install exa --without-git - -[Formulae](https://github.com/Homebrew/homebrew-core/blob/master/Formula/exa.rb) + apt install exa ### Fedora -You can install the `exa` package from the official Fedora repositories by running: +On Fedora, install the [`exa`](https://src.fedoraproject.org/modules/exa) package. dnf install exa +### Gentoo + +On Gentoo, install the [`sys-apps/exa`](https://packages.gentoo.org/packages/sys-apps/exa) package. + + emerge sys-apps/exa + +### Homebrew + +If you’re using [Homebrew](https://brew.sh/) on macOS, install the [`exa`](http://formulae.brew.sh/formula/exa) formula. + + brew install exa + +### MacPorts + +If you're using [MacPorts](https://www.macports.org/) on macOS, install the [`exa`](https://ports.macports.org/port/exa/summary) port. + + port install exa + ### Nix -`exa` is also installable through [the derivation](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/misc/exa/default.nix) using the [nix package manager](https://nixos.org/nix/) by running: +On nixOS, install the [`exa`](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/misc/exa/default.nix) package. nix-env -i exa -## Testing with Vagrant +### openSUSE + +On openSUSE, install the [`exa`](https://software.opensuse.org/package/exa) package. + + zypper install exa + +### Ubuntu + +On Ubuntu 20.10 (Groovy Gorilla) and later, install the [`exa`](https://packages.ubuntu.com/jammy/exa) package. + + sudo apt install exa + +### Void Linux + +On Void Linux, install the [`exa`](https://github.com/void-linux/void-packages/blob/master/srcpkgs/exa/template) package. + + xbps-install -S exa + +### Manual installation from GitHub + +Compiled binary versions of exa are uploaded to GitHub when a release is made. +You can install exa manually by [downloading a release](https://github.com/ogham/exa/releases), extracting it, and copying the binary to a directory in your `$PATH`, such as `/usr/local/bin`. + +For more information, see the [Manual Installation page](https://the.exa.website/install/linux#manual). + +### Cargo + +If you already have a Rust environment set up, you can use the `cargo install` command: + + cargo install exa + +Cargo will build the `exa` binary and place it in `$HOME/.cargo`. + +To build without Git support, run `cargo install --no-default-features exa` is also available, if the requisite dependencies are not installed. + + +--- + + +

Development + + + Rust 1.66.1+ + + + + MIT Licence + +

+ +exa is written in [Rust](https://www.rust-lang.org/). +You will need rustc version 1.66.1 or higher. +The recommended way to install Rust for development is from the [official download page](https://www.rust-lang.org/tools/install), using rustup. + +Once Rust is installed, you can compile exa with Cargo: + + cargo build + cargo test + +- The [just](https://github.com/casey/just) command runner can be used to run some helpful development commands, in a manner similar to `make`. +Run `just --list` to get an overview of what’s available. + +- If you are compiling a copy for yourself, be sure to run `cargo build --release` or `just build-release` to benefit from release-mode optimisations. +Copy the resulting binary, which will be in the `target/release` directory, into a folder in your `$PATH`. +`/usr/local/bin` is usually a good choice. + +- To compile and install the manual pages, you will need [pandoc](https://pandoc.org/). +The `just man` command will compile the Markdown into manual pages, which it will place in the `target/man` directory. +To use them, copy them into a directory that `man` will read. +`/usr/local/share/man` is usually a good choice. + +- exa depends on [libgit2](https://github.com/rust-lang/git2-rs) for certain features. +If you’re unable to compile libgit2, you can opt out of Git support by running `cargo build --no-default-features`. + +- If you intend to compile for musl, you will need to use the flag `vendored-openssl` if you want to get the Git feature working. +The full command is `cargo build --release --target=x86_64-unknown-linux-musl --features vendored-openssl,git`. + +For more information, see the [Building from Source page](https://the.exa.website/install/source). + + +### Testing with Vagrant exa uses [Vagrant][] to configure virtual machines for testing. -Programs such as exa that are basically interfaces to the system are [notoriously difficult to test][testing]. Although the internal components have unit tests, it’s impossible to do a complete end-to-end test without mandating the current user’s name, the time zone, the locale, and directory structure to test. (And yes, these tests are worth doing. I have missed an edge case on more than one occasion.) +Programs such as exa that are basically interfaces to the system are [notoriously difficult to test][testing]. +Although the internal components have unit tests, it’s impossible to do a complete end-to-end test without mandating the current user’s name, the time zone, the locale, and directory structure to test. +(And yes, these tests are worth doing. I have missed an edge case on many an occasion.) -The initial attempt to solve the problem was just to create a directory of “awkward” test cases, run exa on it, and make sure it produced the correct output. But even this output would change if, say, the user’s locale formats dates in a different way. These can be mocked inside the code, but at the cost of making that code more complicated to read and understand. +The initial attempt to solve the problem was just to create a directory of “awkward” test cases, run exa on it, and make sure it produced the correct output. +But even this output would change if, say, the user’s locale formats dates in a different way. +These can be mocked inside the code, but at the cost of making that code more complicated to read and understand. -An alternative solution is to fake *everything*: create a virtual machine with a known state and run the tests on *that*. This is what Vagrant does. Although it takes a while to download and set up, it gives everyone the same development environment to test for any obvious regressions. +An alternative solution is to fake *everything*: create a virtual machine with a known state and run the tests on *that*. +This is what Vagrant does. +Although it takes a while to download and set up, it gives everyone the same development environment to test for any obvious regressions. [Vagrant]: https://www.vagrantup.com/ [testing]: https://eev.ee/blog/2016/08/22/testing-for-people-who-hate-testing/#troublesome-cases @@ -137,7 +263,8 @@ First, initialise the VM: host$ vagrant up -The first command downloads the virtual machine image, and then runs our provisioning script, which installs Rust, exa’s dependencies, configures the environment, and generates some awkward files and folders to use as test cases. This takes some time, but it does write to output occasionally. Once this is done, you can SSH in, and build and test: +The first command downloads the virtual machine image, and then runs our provisioning script, which installs Rust and exa’s build-time dependencies, configures the environment, and generates some awkward files and folders to use as test cases. +Once this is done, you can SSH in, and build and test: host$ vagrant ssh vm$ cd /vagrant @@ -145,7 +272,6 @@ The first command downloads the virtual machine image, and then runs our provisi vm$ ./xtests/run All the tests passed! - -### Running without Vagrant - -Of course, the drawback of having a standard development environment is that you stop noticing bugs that occur outside of it. For this reason, Vagrant isn’t a *necessary* development step — it’s there if you’d like to use it, but exa still gets used and tested on other platforms. It can still be built and compiled on any target triple that it supports, VM or no VM, with `cargo build` and `cargo test`. +Of course, the drawback of having a standard development environment is that you stop noticing bugs that occur outside of it. +For this reason, Vagrant isn’t a *necessary* development step — it’s there if you’d like to use it, but exa still gets used and tested on other platforms. +It can still be built and compiled on any target triple that it supports, VM or no VM, with `cargo build` and `cargo test`. diff --git a/Vagrantfile b/Vagrantfile index a8b4c596..b1b4288c 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,22 +1,19 @@ -require 'date' - Vagrant.configure(2) do |config| # We use Ubuntu instead of Debian because the image comes with two-way # shared folder support by default. - UBUNTU = 'bento/ubuntu-16.04' + UBUNTU = 'hashicorp/bionic64' - # The main VM is the one used for development and testing. - config.vm.define(:exa, primary: true) do |config| + config.vm.define(:exa) do |config| config.vm.provider :virtualbox do |v| v.name = 'exa' v.memory = 2048 - v.cpus = 2 + v.cpus = `nproc`.chomp.to_i end config.vm.provider :vmware_desktop do |v| v.vmx['memsize'] = '2048' - v.vmx['numvcpus'] = '2' + v.vmx['numvcpus'] = `nproc`.chomp end config.vm.box = UBUNTU @@ -32,20 +29,19 @@ Vagrant.configure(2) do |config| # Install the dependencies needed for exa to build, as quietly as # apt can do. config.vm.provision :shell, privileged: true, inline: <<-EOF - set -xe - apt-get update - apt-get install -qq -o=Dpkg::Use-Pty=0 -y \ - git cmake curl attr libgit2-dev zip \ - fish zsh bash bash-completion + if hash fish &>/dev/null; then + echo "Tools are already installed" + else + trap 'exit' ERR + echo "Installing tools" + apt-get update -qq + apt-get install -qq -o=Dpkg::Use-Pty=0 \ + git gcc curl attr libgit2-dev zip \ + fish zsh bash bash-completion + fi EOF - # Guarantee that the timezone is UTC -- some of the tests - # depend on this (for now). - config.vm.provision :shell, privileged: true, inline: - %[timedatectl set-timezone UTC] - - # Install Rust. # This is done as vagrant, not root, because it’s vagrant # who actually uses it. Sent to /dev/null because the progress @@ -54,25 +50,46 @@ Vagrant.configure(2) do |config| if hash rustc &>/dev/null; then echo "Rust is already installed" else - set -xe - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + trap 'exit' ERR + echo "Installing Rust" + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --profile minimal --component rustc,rust-std,cargo,clippy -y > /dev/null + source $HOME/.cargo/env + echo "Installing cargo-hack" + cargo install -q cargo-hack + echo "Installing specsheet" + cargo install -q --git https://github.com/ogham/specsheet fi EOF - # Use a different ‘target’ directory on the VM than on the host. - # By default it just uses the one in /vagrant/target, which can - # cause problems if it has different permissions than the other - # directories, or contains object files compiled for the host. + # Privileged installation and setup scripts. config.vm.provision :shell, privileged: true, inline: <<-EOF + + # Install Just, the command runner. + if hash just &>/dev/null; then + echo "just is already installed" + else + trap 'exit' ERR + echo "Installing just" + wget -q "https://github.com/casey/just/releases/download/v0.8.3/just-v0.8.3-x86_64-unknown-linux-musl.tar.gz" + tar -xf "just-v0.8.3-x86_64-unknown-linux-musl.tar.gz" + cp just /usr/local/bin + fi + + # Guarantee that the timezone is UTC — some of the tests + # depend on this (for now). + timedatectl set-timezone UTC + + + # Use a different ‘target’ directory on the VM than on the host. + # By default it just uses the one in /vagrant/target, which can + # cause problems if it has different permissions than the other + # directories, or contains object files compiled for the host. echo 'PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/#{developer}/.cargo/bin"' > /etc/environment echo 'CARGO_TARGET_DIR="/home/#{developer}/target"' >> /etc/environment - EOF - # Create a variety of misc scripts. - config.vm.provision :shell, privileged: true, inline: <<-EOF - set -xe + # Create a variety of misc scripts. ln -sf /vagrant/devtools/dev-run-debug.sh /usr/bin/exa ln -sf /vagrant/devtools/dev-run-release.sh /usr/bin/rexa @@ -80,7 +97,7 @@ Vagrant.configure(2) do |config| echo -e "#!/bin/sh\ncargo build --manifest-path /vagrant/Cargo.toml \\$@" > /usr/bin/build-exa ln -sf /usr/bin/build-exa /usr/bin/b - echo -e "#!/bin/sh\ncargo test --manifest-path /vagrant/Cargo.toml --lib \\$@ -- --quiet" > /usr/bin/test-exa + echo -e "#!/bin/sh\ncargo test --manifest-path /vagrant/Cargo.toml \\$@ -- --quiet" > /usr/bin/test-exa ln -sf /usr/bin/test-exa /usr/bin/t echo -e "#!/bin/sh\n/vagrant/xtests/run.sh" > /usr/bin/run-xtests @@ -93,22 +110,12 @@ Vagrant.configure(2) do |config| echo -e "#!/bin/sh\ncat /etc/motd" > /usr/bin/halp chmod +x /usr/bin/{exa,rexa,b,t,x,c,build-exa,test-exa,run-xtests,compile-exa,package-exa,halp} - EOF - # This fix is applied by changing the VM rather than changing the - # Cargo.toml file so it works for everyone because it’s such a niche - # build issue, it’s not worth specifying a non-crates.io dependency - # and losing the ability to `cargo publish` the exa crate there! - # It also isolates the hackiness to the one place I can test it - # actually works. - - - # Configure the welcoming text that gets shown. - config.vm.provision :shell, privileged: true, inline: <<-EOF - rm -f /etc/update-motd.d/* + # Configure the welcoming text that gets shown: # Capture the help text so it gets displayed first + rm -f /etc/update-motd.d/* bash /vagrant/devtools/dev-help.sh > /etc/motd # Tell bash to execute a bunch of stuff when a session starts @@ -118,438 +125,29 @@ Vagrant.configure(2) do |config| # Disable last login date in sshd sed -i '/PrintLastLog yes/c\PrintLastLog no' /etc/ssh/sshd_config systemctl restart sshd - EOF - # Link the completion files so they’re “installed”. - config.vm.provision :shell, privileged: true, inline: <<-EOF - set -xe + # Link the completion files so they’re “installed”: + # bash test -h /etc/bash_completion.d/exa \ || ln -s /vagrant/contrib/completions.bash /etc/bash_completion.d/exa + # zsh test -h /usr/share/zsh/vendor-completions/_exa \ || ln -s /vagrant/contrib/completions.zsh /usr/share/zsh/vendor-completions/_exa + # fish test -h /usr/share/fish/completions/exa.fish \ || ln -s /vagrant/contrib/completions.fish /usr/share/fish/completions/exa.fish EOF - # We create two users that own the test files. - # The first one just owns the ordinary ones, because we don’t want the - # test outputs to depend on “vagrant” or “ubuntu” existing. - user = "cassowary" - config.vm.provision :shell, privileged: true, inline: - %[id -u #{user} &>/dev/null || useradd #{user}] - - - # The second one has a long name, to test that the file owner column - # widens correctly. The benefit of Vagrant is that we don’t need to - # set this up on the *actual* system! - longuser = "antidisestablishmentarienism" - config.vm.provision :shell, privileged: true, inline: - %[id -u #{longuser} &>/dev/null || useradd #{longuser}] - - - # Because the timestamps are formatted differently depending on whether - # they’re in the current year or not (see `details.rs`), we have to make - # sure that the files are created in the current year, so they get shown - # in the format we expect. - current_year = Date.today.year - some_date = "#{current_year}01011234.56" # 1st January, 12:34:56 - - - # We also need an UID and a GID that are guaranteed to not exist, to - # test what happen when they don’t. - invalid_uid = 666 - invalid_gid = 616 - - - # Delete old testcases if they exist already, then create a - # directory to house new ones. - test_dir = "/testcases" - config.vm.provision :shell, privileged: true, inline: <<-EOF - set -xe - rm -rfv #{test_dir} - mkdir #{test_dir} - chmod 777 #{test_dir} - EOF - - - # Awkward file size testcases. - # This needs sudo to set the files’ users at the very end. - config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe - mkdir "#{test_dir}/files" - for i in {1..13}; do - fallocate -l "$i" "#{test_dir}/files/$i"_bytes - fallocate -l "$i"KiB "#{test_dir}/files/$i"_KiB - fallocate -l "$i"MiB "#{test_dir}/files/$i"_MiB - done - - touch -t #{some_date} "#{test_dir}/files/"* - chmod 644 "#{test_dir}/files/"* - sudo chown #{user}:#{user} "#{test_dir}/files/"* - EOF - - - # File name extension testcases. - # These aren’t tested in details view, but we set timestamps on them to - # test that various sort options work. - config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe - mkdir "#{test_dir}/file-names-exts" - - touch "#{test_dir}/file-names-exts/Makefile" - - touch "#{test_dir}/file-names-exts/IMAGE.PNG" - touch "#{test_dir}/file-names-exts/image.svg" - - touch "#{test_dir}/file-names-exts/VIDEO.AVI" - touch "#{test_dir}/file-names-exts/video.wmv" - - touch "#{test_dir}/file-names-exts/music.mp3" - touch "#{test_dir}/file-names-exts/MUSIC.OGG" - - touch "#{test_dir}/file-names-exts/lossless.flac" - touch "#{test_dir}/file-names-exts/lossless.wav" - - touch "#{test_dir}/file-names-exts/crypto.asc" - touch "#{test_dir}/file-names-exts/crypto.signature" - - touch "#{test_dir}/file-names-exts/document.pdf" - touch "#{test_dir}/file-names-exts/DOCUMENT.XLSX" - - touch "#{test_dir}/file-names-exts/COMPRESSED.ZIP" - touch "#{test_dir}/file-names-exts/compressed.tar.gz" - touch "#{test_dir}/file-names-exts/compressed.tgz" - touch "#{test_dir}/file-names-exts/compressed.tar.xz" - touch "#{test_dir}/file-names-exts/compressed.txz" - touch "#{test_dir}/file-names-exts/compressed.deb" - - touch "#{test_dir}/file-names-exts/backup~" - touch "#{test_dir}/file-names-exts/#SAVEFILE#" - touch "#{test_dir}/file-names-exts/file.tmp" - - touch "#{test_dir}/file-names-exts/compiled.class" - touch "#{test_dir}/file-names-exts/compiled.o" - touch "#{test_dir}/file-names-exts/compiled.js" - touch "#{test_dir}/file-names-exts/compiled.coffee" - EOF - - - # File name testcases. - # bash really doesn’t want you to create a file with escaped characters - # in its name, so we have to resort to the echo builtin and touch! - # - # The double backslashes are not strictly necessary; without them, Ruby - # will interpolate them instead of bash, but because Vagrant prints out - # each command it runs, your *own* terminal will go “ding” from the alarm! - config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe - mkdir "#{test_dir}/file-names" - - echo -ne "#{test_dir}/file-names/ascii: hello" | xargs -0 touch - echo -ne "#{test_dir}/file-names/emoji: [🆒]" | xargs -0 touch - echo -ne "#{test_dir}/file-names/utf-8: pâté" | xargs -0 touch - - echo -ne "#{test_dir}/file-names/bell: [\\a]" | xargs -0 touch - echo -ne "#{test_dir}/file-names/backspace: [\\b]" | xargs -0 touch - echo -ne "#{test_dir}/file-names/form-feed: [\\f]" | xargs -0 touch - echo -ne "#{test_dir}/file-names/new-line: [\\n]" | xargs -0 touch - echo -ne "#{test_dir}/file-names/return: [\\r]" | xargs -0 touch - echo -ne "#{test_dir}/file-names/tab: [\\t]" | xargs -0 touch - echo -ne "#{test_dir}/file-names/vertical-tab: [\\v]" | xargs -0 touch - - echo -ne "#{test_dir}/file-names/escape: [\\033]" | xargs -0 touch - echo -ne "#{test_dir}/file-names/ansi: [\\033[34mblue\\033[0m]" | xargs -0 touch - - echo -ne "#{test_dir}/file-names/invalid-utf8-1: [\\xFF]" | xargs -0 touch - echo -ne "#{test_dir}/file-names/invalid-utf8-2: [\\xc3\\x28]" | xargs -0 touch - echo -ne "#{test_dir}/file-names/invalid-utf8-3: [\\xe2\\x82\\x28]" | xargs -0 touch - echo -ne "#{test_dir}/file-names/invalid-utf8-4: [\\xf0\\x28\\x8c\\x28]" | xargs -0 touch - - echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]" | xargs -0 mkdir - echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/subfile" | xargs -0 touch - echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/another: [\\n]" | xargs -0 touch - echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/broken" | xargs -0 touch - - mkdir "#{test_dir}/file-names/links" - ln -s "#{test_dir}/file-names/new-line-dir"*/* "#{test_dir}/file-names/links" - - echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/broken" | xargs -0 rm - EOF - - - # Special file testcases. - config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe - mkdir "#{test_dir}/specials" - - sudo mknod "#{test_dir}/specials/block-device" b 3 60 - sudo mknod "#{test_dir}/specials/char-device" c 14 40 - sudo mknod "#{test_dir}/specials/named-pipe" p - - sudo touch -t #{some_date} "#{test_dir}/specials/"* - EOF - - - # Awkward symlink testcases. - config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe - mkdir "#{test_dir}/links" - - ln -s / "#{test_dir}/links/root" - ln -s /usr "#{test_dir}/links/usr" - ln -s nowhere "#{test_dir}/links/broken" - ln -s /proc/1/root "#{test_dir}/links/forbidden" - - touch "#{test_dir}/links/some_file" - ln -s "#{test_dir}/links/some_file" "#{test_dir}/links/some_file_absolute" - (cd "#{test_dir}/links"; ln -s "some_file" "some_file_relative") - (cd "#{test_dir}/links"; ln -s "." "current_dir") - (cd "#{test_dir}/links"; ln -s ".." "parent_dir") - (cd "#{test_dir}/links"; ln -s "itself" "itself") - EOF - - - # Awkward passwd testcases. - # sudo is needed for these because we technically aren’t a member - # of the groups (because they don’t exist), and chown and chgrp - # are smart enough to disallow it! - config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe - mkdir "#{test_dir}/passwd" - - touch -t #{some_date} "#{test_dir}/passwd/unknown-uid" - chmod 644 "#{test_dir}/passwd/unknown-uid" - sudo chown #{invalid_uid}:#{user} "#{test_dir}/passwd/unknown-uid" - - touch -t #{some_date} "#{test_dir}/passwd/unknown-gid" - chmod 644 "#{test_dir}/passwd/unknown-gid" - sudo chown #{user}:#{invalid_gid} "#{test_dir}/passwd/unknown-gid" - EOF - - - # Awkward permission testcases. - # Differences in the way ‘chmod’ handles setting ‘setuid’ and ‘setgid’ - # when you don’t already own the file mean that we need to use ‘sudo’ - # to change permissions to those. - config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe - mkdir "#{test_dir}/permissions" - - mkdir "#{test_dir}/permissions/forbidden-directory" - chmod 000 "#{test_dir}/permissions/forbidden-directory" - touch -t #{some_date} "#{test_dir}/permissions/forbidden-directory" - sudo chown #{user}:#{user} "#{test_dir}/permissions/forbidden-directory" - - for perms in 000 001 002 004 010 020 040 100 200 400 644 755 777 1000 1001 2000 2010 4000 4100 7666 7777; do - touch "#{test_dir}/permissions/$perms" - sudo chown #{user}:#{user} "#{test_dir}/permissions/$perms" - sudo chmod $perms "#{test_dir}/permissions/$perms" - sudo touch -t #{some_date} "#{test_dir}/permissions/$perms" - done - EOF - - old = '200303030000.00' - med = '200606152314.29' # the june gets used for fr_FR locale tests - new = '200912221038.53' # and the december for ja_JP local tests - - # Awkward date and time testcases. - config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe - mkdir "#{test_dir}/dates" - - # there's no way to touch the created date of a file... - # so we have to do this the old-fashioned way! - # (and make sure these don't actually get listed) - touch -t #{old} "#{test_dir}/dates/peach"; sleep 1 - touch -t #{med} "#{test_dir}/dates/plum"; sleep 1 - touch -t #{new} "#{test_dir}/dates/pear" - - # modified dates - touch -t #{old} -m "#{test_dir}/dates/pear" - touch -t #{med} -m "#{test_dir}/dates/peach" - touch -t #{new} -m "#{test_dir}/dates/plum" - - # accessed dates - touch -t #{old} -a "#{test_dir}/dates/plum" - touch -t #{med} -a "#{test_dir}/dates/pear" - touch -t #{new} -a "#{test_dir}/dates/peach" - - sudo chown #{user}:#{user} -R "#{test_dir}/dates" - EOF - - - # Awkward extended attribute testcases. - # We need to test combinations of various numbers of files *and* - # extended attributes in directories. Turns out, the easiest way to - # do this is to generate all combinations of files with “one-xattr” - # or “two-xattrs” in their name and directories with “empty” or - # “one-file” in their name, then just give the right number of - # xattrs and children to those. - config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe - mkdir "#{test_dir}/attributes" - - mkdir "#{test_dir}/attributes/files" - touch "#{test_dir}/attributes/files/"{no-xattrs,one-xattr,two-xattrs}{,_forbidden} - - mkdir "#{test_dir}/attributes/dirs" - mkdir "#{test_dir}/attributes/dirs/"{no-xattrs,one-xattr,two-xattrs}_{empty,one-file,two-files}{,_forbidden} - - setfattr -n user.greeting -v hello "#{test_dir}/attributes"/**/*{one-xattr,two-xattrs}* - setfattr -n user.another_greeting -v hi "#{test_dir}/attributes"/**/*two-xattrs* - - for dir in "#{test_dir}/attributes/dirs/"*one-file*; do - touch $dir/file-in-question - done - - for dir in "#{test_dir}/attributes/dirs/"*two-files*; do - touch $dir/this-file - touch $dir/that-file - done - - find "#{test_dir}/attributes" -exec touch {} -t #{some_date} \\; - - # I want to use the following to test, - # but it only works on macos: - #chmod +a "#{user} deny readextattr" "#{test_dir}/attributes"/**/*_forbidden - - sudo chmod 000 "#{test_dir}/attributes"/**/*_forbidden - sudo chown #{user}:#{user} -R "#{test_dir}/attributes" - EOF - - - # A sample Git repository - # This uses cd because it's easier than telling Git where to go each time - config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe - mkdir "#{test_dir}/git" - cd "#{test_dir}/git" - git init - - mkdir edits additions moves - - echo "original content" | tee edits/{staged,unstaged,both} - echo "this file gets moved" > moves/hither - - git add edits moves - git config --global user.email "exa@exa.exa" - git config --global user.name "Exa Exa" - git commit -m "Automated test commit" - - echo "modifications!" | tee edits/{staged,both} - touch additions/{staged,edited} - mv moves/{hither,thither} - - git add edits moves additions - echo "more modifications!" | tee edits/unstaged edits/both additions/edited - touch additions/unstaged - - find "#{test_dir}/git" -exec touch {} -t #{some_date} \\; - sudo chown #{user}:#{user} -R "#{test_dir}/git" - EOF - - - # A second Git repository - # for testing two at once - config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe - mkdir -p "#{test_dir}/git2/deeply/nested/directory" - cd "#{test_dir}/git2" - git init - - touch "deeply/nested/directory/upd8d" - git add "deeply/nested/directory/upd8d" - git commit -m "Automated test commit" - - echo "Now with contents" > "deeply/nested/directory/upd8d" - touch "deeply/nested/directory/l8st" - - echo -e "target\n*.mp3" > ".gitignore" - mkdir "ignoreds" - touch "ignoreds/music.mp3" - touch "ignoreds/music.m4a" - mkdir "ignoreds/nested" - touch "ignoreds/nested/70s grove.mp3" - touch "ignoreds/nested/funky chicken.m4a" - - mkdir "target" - touch "target/another ignored file" - - mkdir "deeply/nested/repository" - cd "deeply/nested/repository" - git init - touch subfile - - find "#{test_dir}/git2" -exec touch {} -t #{some_date} \\; - sudo chown #{user}:#{user} -R "#{test_dir}/git2" - EOF - - # A third Git repository - # Regression test for https://github.com/ogham/exa/issues/526 - config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe - mkdir -p "#{test_dir}/git3" - cd "#{test_dir}/git3" - git init - - # Create a symbolic link pointing to a non-existing file - ln -s aaa/aaa/a b - - find "#{test_dir}/git3" -exec touch {} -t #{some_date} \\; - sudo chown #{user}:#{user} -R "#{test_dir}/git3" - EOF - - # Hidden and dot file testcases. - # We need to set the permissions of `.` and `..` because they actually - # get displayed in the output here, so this has to come last. - config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe - shopt -u dotglob - GLOBIGNORE=".:.." - - mkdir "#{test_dir}/hiddens" - touch "#{test_dir}/hiddens/visible" - touch "#{test_dir}/hiddens/.hidden" - touch "#{test_dir}/hiddens/..extra-hidden" - - # ./hiddens/ - touch -t #{some_date} "#{test_dir}/hiddens/"* - chmod 644 "#{test_dir}/hiddens/"* - sudo chown #{user}:#{user} "#{test_dir}/hiddens/"* - - # . - touch -t #{some_date} "#{test_dir}/hiddens" - chmod 755 "#{test_dir}/hiddens" - sudo chown #{user}:#{user} "#{test_dir}/hiddens" - - # .. - sudo touch -t #{some_date} "#{test_dir}" - sudo chmod 755 "#{test_dir}" - sudo chown #{user}:#{user} "#{test_dir}" - EOF - - - # Set up some locales - config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe - - # uncomment these from the config file - sudo sed -i '/fr_FR.UTF-8/s/^# //g' /etc/locale.gen - sudo sed -i '/ja_JP.UTF-8/s/^# //g' /etc/locale.gen - sudo locale-gen - EOF - - # Install kcov for test coverage # This doesn’t run coverage over the xtests so it’s less useful for now if ENV.key?('INSTALL_KCOV') config.vm.provision :shell, privileged: false, inline: <<-EOF - set -xe + trap 'exit' ERR test -e ~/.cargo/bin/cargo-kcov \ || cargo install cargo-kcov @@ -561,48 +159,8 @@ Vagrant.configure(2) do |config| cargo kcov --print-install-kcov-sh | sudo sh EOF end - end - - # Remember that problem that exa had where the binary wasn’t actually - # self-contained? Or the problem where the Linux binary was actually the - # macOS binary in disguise? - # - # This is a “fresh” VM that intentionally downloads no dependencies and - # installs nothing so that we can check that exa still runs! - config.vm.define(:fresh) do |config| - config.vm.box = UBUNTU - config.vm.hostname = 'fresh' - - config.vm.provider :virtualbox do |v| - v.name = 'exa-fresh' - v.memory = 384 - v.cpus = 1 - end - - # Well, we do need *one* dependency... - config.vm.provision :shell, privileged: true, inline: <<-EOF - set -xe - apt-get install -qq -o=Dpkg::Use-Pty=0 -y unzip - EOF - - # This thing also has its own welcoming text. - config.vm.provision :shell, privileged: true, inline: <<-EOF - rm -f /etc/update-motd.d/* - - # Capture the help text so it gets displayed first - bash /vagrant/devtools/dev-help-testvm.sh > /etc/motd - - # Disable last login date in sshd - sed -i '/PrintLastLog yes/c\PrintLastLog no' /etc/ssh/sshd_config - systemctl restart sshd - EOF - - # Make the checker script a command. - config.vm.provision :shell, privileged: true, inline: <<-EOF - set -xe - echo -e "#!/bin/sh\nbash /vagrant/devtools/dev-download-and-check-release.sh \"\\$*\"" > /usr/bin/check-release - chmod +x /usr/bin/check-release - EOF + config.vm.provision :shell, privileged: true, path: 'devtools/dev-set-up-environment.sh' + config.vm.provision :shell, privileged: false, path: 'devtools/dev-create-test-filesystem.sh' end end diff --git a/build.rs b/build.rs index 79a01748..6cd6c722 100644 --- a/build.rs +++ b/build.rs @@ -10,10 +10,51 @@ /// - https://stackoverflow.com/q/43753491/3484614 /// - https://crates.io/crates/vergen -extern crate datetime; -use std::io::Result as IOResult; use std::env; +use std::fs::File; +use std::io::{self, Write}; +use std::path::PathBuf; +use datetime::{LocalDateTime, ISO}; + + +/// The build script entry point. +fn main() -> io::Result<()> { + #![allow(clippy::write_with_newline)] + + let tagline = "exa - list files on the command-line"; + let url = "https://the.exa.website/"; + + let ver = + if is_debug_build() { + format!("{}\nv{} \\1;31m(pre-release debug build!)\\0m\n\\1;4;34m{}\\0m", tagline, version_string(), url) + } + else if is_development_version() { + format!("{}\nv{} [{}] built on {} \\1;31m(pre-release!)\\0m\n\\1;4;34m{}\\0m", tagline, version_string(), git_hash(), build_date(), url) + } + else { + format!("{}\nv{}\n\\1;4;34m{}\\0m", tagline, version_string(), url) + }; + + // We need to create these files in the Cargo output directory. + let out = PathBuf::from(env::var("OUT_DIR").unwrap()); + let path = &out.join("version_string.txt"); + + // Bland version text + let mut f = File::create(path).unwrap_or_else(|_| { panic!("{}", path.to_string_lossy().to_string()) }); + writeln!(f, "{}", strip_codes(&ver))?; + + Ok(()) +} + +/// Removes escape codes from a string. +fn strip_codes(input: &str) -> String { + input.replace("\\0m", "") + .replace("\\1;31m", "") + .replace("\\1;4;34m", "") +} + +/// Retrieve the project’s current Git hash, as a string. fn git_hash() -> String { use std::process::Command; @@ -24,38 +65,59 @@ fn git_hash() -> String { .stdout).trim().to_string() } -fn main() { - write_statics().unwrap(); -} - +/// Whether we should show pre-release info in the version string. +/// +/// Both weekly releases and actual releases are --release releases, +/// but actual releases will have a proper version number. fn is_development_version() -> bool { - // Both weekly releases and actual releases are --release releases, - // but actual releases will have a proper version number cargo_version().ends_with("-pre") || env::var("PROFILE").unwrap() == "debug" } +/// Whether we are building in debug mode. +fn is_debug_build() -> bool { + env::var("PROFILE").unwrap() == "debug" +} + +/// Retrieves the [package] version in Cargo.toml as a string. fn cargo_version() -> String { env::var("CARGO_PKG_VERSION").unwrap() } -fn build_date() -> String { - use datetime::{LocalDateTime, ISO}; +/// Returns the version and build parameters string. +fn version_string() -> String { + let mut ver = cargo_version(); - let now = LocalDateTime::now(); - format!("{}", now.date().iso()) + let feats = nonstandard_features_string(); + if ! feats.is_empty() { + ver.push_str(&format!(" [{}]", &feats)); + } + + ver +} + +/// Finds whether a feature is enabled by examining the Cargo variable. +fn feature_enabled(name: &str) -> bool { + env::var(&format!("CARGO_FEATURE_{}", name)) + .map(|e| ! e.is_empty()) + .unwrap_or(false) } -fn write_statics() -> IOResult<()> { - use std::fs::File; - use std::io::Write; - use std::path::PathBuf; +/// A comma-separated list of non-standard feature choices. +fn nonstandard_features_string() -> String { + let mut s = Vec::new(); - let ver = match is_development_version() { - true => format!("exa v{} ({} built on {})", cargo_version(), git_hash(), build_date()), - false => format!("exa v{}", cargo_version()), - }; + if feature_enabled("GIT") { + s.push("+git"); + } + else { + s.push("-git"); + } - let out = PathBuf::from(env::var("OUT_DIR").unwrap()); - let mut f = File::create(&out.join("version_string.txt"))?; - write!(f, "{:?}", ver) + s.join(", ") +} + +/// Formats the current date as an ISO 8601 string. +fn build_date() -> String { + let now = LocalDateTime::now(); + format!("{}", now.date().iso()) } diff --git a/contrib/completions.bash b/completions/bash/exa similarity index 53% rename from contrib/completions.bash rename to completions/bash/exa index 4a370f37..d0447278 100644 --- a/contrib/completions.bash +++ b/completions/bash/exa @@ -8,6 +8,11 @@ _exa() return ;; + --colour) + COMPREPLY=( $( compgen -W 'always auto never' -- "$cur" ) ) + return + ;; + -L|--level) COMPREPLY=( $( compgen -W '{0..9}' -- "$cur" ) ) return @@ -19,19 +24,28 @@ _exa() ;; -t|--time) - COMPREPLY=( $( compgen -W 'modified changed accessed created --' -- $cur ) ) + COMPREPLY=( $( compgen -W 'modified changed accessed created --' -- "$cur" ) ) return ;; --time-style) - COMPREPLY=( $( compgen -W 'default iso long-iso full-iso --' -- $cur ) ) + COMPREPLY=( $( compgen -W 'default iso long-iso full-iso --' -- "$cur" ) ) return ;; esac case "$cur" in + # _parse_help doesn’t pick up short options when they are on the same line than long options + --*) + # colo[u]r isn’t parsed correctly so we filter these options out and add them by hand + parse_help=$( exa --help | grep -oE ' (\-\-[[:alnum:]@-]+)' | tr -d ' ' | grep -v '\-\-colo' ) + completions=$( echo '--color --colour --color-scale --colour-scale' $parse_help ) + COMPREPLY=( $( compgen -W "$completions" -- "$cur" ) ) + ;; + -*) - COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) ) + completions=$( exa --help | grep -oE ' (\-[[:alnum:]@])' | tr -d ' ' ) + COMPREPLY=( $( compgen -W "$completions" -- "$cur" ) ) ;; *) diff --git a/contrib/completions.fish b/completions/fish/exa.fish similarity index 79% rename from contrib/completions.fish rename to completions/fish/exa.fish index 41266072..023bd05b 100755 --- a/contrib/completions.fish +++ b/completions/fish/exa.fish @@ -10,20 +10,25 @@ complete -c exa -s 'x' -l 'across' -d "Sort the grid across, rather than d complete -c exa -s 'R' -l 'recurse' -d "Recurse into directories" complete -c exa -s 'T' -l 'tree' -d "Recurse into directories as a tree" complete -c exa -s 'F' -l 'classify' -d "Display type indicator by file names" -complete -c exa -l 'color' -d "When to use terminal colours" -complete -c exa -l 'colour' -d "When to use terminal colours" -complete -c exa -l 'color-scale' -d "Highlight levels of file sizes distinctly" -complete -c exa -l 'colour-scale' -d "Highlight levels of file sizes distinctly" +complete -c exa -l 'color' \ + -l 'colour' -d "When to use terminal colours" -x -a " + always\t'Always use colour' + auto\t'Use colour if standard output is a terminal' + never\t'Never use colour' +" +complete -c exa -l 'color-scale' \ + -l 'colour-scale' -d "Highlight levels of file sizes distinctly" complete -c exa -l 'icons' -d "Display icons" +complete -c exa -l 'no-icons' -d "Don't display icons" # Filtering and sorting options complete -c exa -l 'group-directories-first' -d "Sort directories before other files" complete -c exa -l 'git-ignore' -d "Ignore files mentioned in '.gitignore'" complete -c exa -s 'a' -l 'all' -d "Show hidden and 'dot' files" complete -c exa -s 'd' -l 'list-dirs' -d "List directories like regular files" -complete -c exa -s 'L' -l 'level' -d "Limit the depth of recursion" -a "1 2 3 4 5 6 7 8 9" +complete -c exa -s 'L' -l 'level' -d "Limit the depth of recursion" -x -a "1 2 3 4 5 6 7 8 9" complete -c exa -s 'r' -l 'reverse' -d "Reverse the sort order" -complete -c exa -s 's' -l 'sort' -x -d "Which field to sort by" -a " +complete -c exa -s 's' -l 'sort' -d "Which field to sort by" -x -a " accessed\t'Sort by file accessed time' age\t'Sort by file modified time (newest first)' changed\t'Sort by changed time' @@ -55,26 +60,28 @@ complete -c exa -s 'b' -l 'binary' -d "List file sizes with binary prefixes" complete -c exa -s 'B' -l 'bytes' -d "List file sizes in bytes, without any prefixes" complete -c exa -s 'g' -l 'group' -d "List each file's group" complete -c exa -s 'h' -l 'header' -d "Add a header row to each column" -complete -c exa -s 'h' -l 'links' -d "List each file's number of hard links" -complete -c exa -s 'g' -l 'group' -d "List each file's inode number" +complete -c exa -s 'H' -l 'links' -d "List each file's number of hard links" +complete -c exa -s 'i' -l 'inode' -d "List each file's inode number" complete -c exa -s 'S' -l 'blocks' -d "List each file's number of filesystem blocks" -complete -c exa -s 't' -l 'time' -x -d "Which timestamp field to list" -a " +complete -c exa -s 't' -l 'time' -d "Which timestamp field to list" -x -a " modified\t'Display modified time' changed\t'Display changed time' accessed\t'Display accessed time' created\t'Display created time' " complete -c exa -s 'm' -l 'modified' -d "Use the modified timestamp field" +complete -c exa -s 'n' -l 'numeric' -d "List numeric user and group IDs." complete -c exa -l 'changed' -d "Use the changed timestamp field" complete -c exa -s 'u' -l 'accessed' -d "Use the accessed timestamp field" complete -c exa -s 'U' -l 'created' -d "Use the created timestamp field" -complete -c exa -l 'time-style' -x -d "How to format timestamps" -a " +complete -c exa -l 'time-style' -d "How to format timestamps" -x -a " default\t'Use the default time style' iso\t'Display brief ISO timestamps' long-iso\t'Display longer ISO timestaps, up to the minute' full-iso\t'Display full ISO timestamps, up to the nanosecond' " complete -c exa -l 'no-permissions' -d "Suppress the permissions field" +complete -c exa -l 'octal-permissions' -d "List each file's permission in octal format" complete -c exa -l 'no-filesize' -d "Suppress the filesize field" complete -c exa -l 'no-user' -d "Suppress the user field" complete -c exa -l 'no-time' -d "Suppress the time field" diff --git a/contrib/completions.zsh b/completions/zsh/_exa similarity index 90% rename from contrib/completions.zsh rename to completions/zsh/_exa index a700d47c..b915a5d0 100644 --- a/contrib/completions.zsh +++ b/completions/zsh/_exa @@ -1,7 +1,7 @@ #compdef exa # Save this file as _exa in /usr/local/share/zsh/site-functions or in any -# other folder in $fpath. E. g. save it in a folder called ~/.zfunc and add a +# other folder in $fpath. E.g. save it in a folder called ~/.zfunc and add a # line containing `fpath=(~/.zfunc $fpath)` somewhere before `compinit` in your # ~/.zshrc. @@ -19,9 +19,10 @@ __exa() { {-R,--recurse}"[Recurse into directories]" \ {-T,--tree}"[Recurse into directories as a tree]" \ {-F,--classify}"[Display type indicator by file names]" \ - --colo{,u}r"[When to use terminal colours]" \ + --colo{,u}r="[When to use terminal colours]:(when):(always auto never)" \ --colo{,u}r-scale"[Highlight levels of file sizes distinctly]" \ --icons"[Display icons]" \ + --no-icons"[Hide icons]" \ --group-directories-first"[Sort directories before other files]" \ --git-ignore"[Ignore files mentioned in '.gitignore']" \ {-a,--all}"[Show hidden and 'dot' files]" \ @@ -39,10 +40,12 @@ __exa() { {-H,--links}"[List each file's number of hard links]" \ {-i,--inode}"[List each file's inode number]" \ {-m,--modified}"[Use the modified timestamp field]" \ + {-n,--numeric}"[List numeric user and group IDs.]" \ {-S,--blocks}"[List each file's number of filesystem blocks]" \ {-t,--time}="[Which time field to show]:(time field):(accessed changed created modified)" \ --time-style="[How to format timestamps]:(time style):(default iso long-iso full-iso)" \ --no-permissions"[Suppress the permissions field]" \ + --octal-permissions"[List each file's permission in octal format]" \ --no-filesize"[Suppress the filesize field]" \ --no-user"[Suppress the user field]" \ --no-time"[Suppress the time field]" \ diff --git a/contrib/man/exa.1 b/contrib/man/exa.1 deleted file mode 100644 index acc9ff10..00000000 --- a/contrib/man/exa.1 +++ /dev/null @@ -1,501 +0,0 @@ -.hy -.TH "exa" "1" "2019\-07\-15" "exa 0.9.0" "" -.SH NAME -.PP -exa \- a modern replacement for ls -.SH SYNOPSIS -.PP -exa [\f[I]options\f[]] [\f[I]files\f[]]... -.SH DESCRIPTION -.PP -\f[C]exa\f[] is a modern replacement for \f[C]ls\f[]. -It uses colours for information by default, helping you distinguish -between many types of files, such as whether you are the owner, or in -the owning group. -It also has extra features not present in the original \f[C]ls\f[], such -as viewing the Git status for a directory, or recursing into directories -with a tree view. -.SH DISPLAY OPTIONS -.TP -.B \-1, \-\-oneline -display one entry per line -.RS -.RE -.TP -.B \-G, \-\-grid -display entries as a grid (default) -.RS -.RE -.TP -.B \-l, \-\-long -display extended file metadata as a table -.RS -.RE -.TP -.B \-x, \-\-across -sort the grid across, rather than downwards -.RS -.RE -.TP -.B \-R, \-\-recurse -recurse into directories -.RS -.RE -.TP -.B \-T, \-\-tree -recurse into directories as a tree -.RS -.RE -.TP -.B \-F, \-\-classify -display type indicator by file names -.RS -.RE -.TP -.B \-\-color, \-\-colour=\f[I]WHEN\f[] -when to use terminal colours (always, automatic, never) -.RS -.RE -.TP -.B \-\-color-scale, \-\-colour-scale -highlight levels of file sizes distinctly -.RS -.RE -.TP -.B \-\-icons -display icons -.RS -.RE -.SH FILTERING AND SORTING OPTIONS -.TP -.B \-a, \-\-all -show hidden and \[aq]dot\[aq] files. -Use this twice to also show the \f[C].\f[] and \f[C]..\f[] directories. -.RS -.RE -.TP -.B \-d, \-\-list\-dirs -list directories like regular files -.RS -.RE -.TP -.B \-L, \-\-level=\f[I]DEPTH\f[] -limit the depth of recursion -.RS -.RE -.TP -.B \-r, \-\-reverse -reverse the sort order -.RS -.RE -.TP -.B \-s, \-\-sort=\f[I]SORT_FIELD\f[] -which field to sort by. -Valid fields are name, Name, extension, Extension, size, modified, changed, accessed, created, inode, type, and none. -The modified field has the aliases date, time, and newest, and its reverse order has the aliases age and oldest. -Fields starting with a capital letter will sort uppercase before lowercase: 'A' then 'B' then 'a' then 'b'. -Fields starting with a lowercase letter will mix them: 'A' then 'a' then 'B' then 'b'. -.RS -.RE -.TP -.B \-I, \-\-ignore\-glob=\f[I]GLOBS\f[] -Glob patterns, pipe-separated, of files to ignore -.RS -.RE -.TP -.B \-\-git\-ignore -ignore files mentioned in '.gitignore' -.RS -.RE -.TP -.B \-\-group\-directories\-first -list directories before other files -.RS -.RE -.TP -.B \-D, \-\-only\-dirs -list only directories -.RS -.RE -.SH LONG VIEW OPTIONS -.PP -These options are available when running with \f[C]\-\-long\f[] -(\f[C]\-l\f[]): -.TP -.B \-b, \-\-binary -list file sizes with binary prefixes -.RS -.RE -.TP -.B \-B, \-\-bytes -list file sizes in bytes, without any prefixes -.RS -.RE -.TP -.B \-\-changed -use the changed timestamp field -.RS -.RE -.TP -.B \-g, \-\-group -list each file\[aq]s group -.RS -.RE -.TP -.B \-h, \-\-header -add a header row to each column -.RS -.RE -.TP -.B \-H, \-\-links -list each file\[aq]s number of hard links -.RS -.RE -.TP -.B \-i, \-\-inode -list each file\[aq]s inode number -.RS -.RE -.TP -.B \-m, \-\-modified -use the modified timestamp field -.RS -.RE -.TP -.B \-S, \-\-blocks -list each file\[aq]s number of file system blocks -.RS -.RE -.TP -.B \-t, \-\-time=\f[I]WORD\f[] -which timestamp field to list (modified, changed, accessed, created) -.RS -.RE -.TP -.B \-\-time\-style=\f[I]STYLE\f[] -how to format timestamps (default, iso, long-iso, full-iso) -.RS -.RE -.TP -.B \-u, \-\-accessed -use the accessed timestamp field -.RS -.RE -.TP -.B \-U, \-\-created -use the created timestamp field -.RS -.RE -.TP -.B \-\-no\-permissions -suppress the permissions field -.RS -.RE -.TP -.B \-\-no\-filesize -suppress the filesize field -.RS -.RE -.TP -.B \-\-no\-user -suppress the user field -.RS -.RE -.TP -.B \-\-no\-time -suppress the time field -.RS -.RE -.TP -.B \-\@, \-\-extended -list each file\[aq]s extended attributes and sizes -.RS -.RE -.TP -.B \-\-git -list each file\[aq]s Git status, if tracked -.RS -.RE -.SH EXAMPLES -.PP -To display a list of files, with the largest at the top: -.IP -.nf -\f[C] -exa\ \-\-reverse\ \-\-sort=size -\f[] -.fi -.PP -To display a tree of files, three levels deep: -.IP -.nf -\f[C] -exa\ \-\-long\ \-\-tree\ \-\-level=3 -\f[] -.fi -.SH ENVIRONMENT VARIABLES -.PP -exa responds to the following environment variables: -.SS \f[C]COLUMNS\f[] -.PP -Overrides the width of the terminal, in characters. -For example, \f[C]COLUMNS=80\ exa\f[] will show a grid view with a -maximum width of 80 characters. -.PP -This option won\[aq]t do anything when exa\[aq]s output doesn\[aq]t -wrap, such as when using the \f[C]\-\-long\f[] view. -.SS \f[C]EXA_STRICT\f[] -.PP -Enables \f[I]strict mode\f[], which will make exa error when two -command\-line options are incompatible. -Usually, options can override each other going right\-to\-left on the -command line, so that exa can be given aliases: creating an alias -\f[C]exa=exa\ \-\-sort=ext\f[] then running \f[C]exa\ \-\-sort=size\f[] -with that alias will run \f[C]exa\ \-\-sort=ext\ \-\-sort=size\f[], and -the sorting specified by the user will override the sorting specified by -the alias. -In strict mode, the two options will not co\-operate, and exa will -error. -.PP -This option is intended for use with automated scripts and other -situations where you want to be \f[I]certain\f[] you\[aq]re typing in -the right command. -.SS \f[C]EXA_GRID_ROWS\f[] -.PP -Limits the grid\-details view (\f[C]exa\ \-\-grid\ \-\-long\f[]) so -it\[aq]s only activated when at least the given number of rows of output -would be generated. -With widescreen displays, it\[aq]s possible for the grid to look very -wide and sparse, on just one or two lines with none of the columns -lining up. -By specifying a minimum number of rows, you can only use the view if -it\[aq]s going to be worth using. -.SS \f[C]LS_COLORS\f[] and \f[C]EXA_COLORS\f[] -.PP -The \f[C]EXA_COLORS\f[] variable is the traditional way of customising -the colours used by \f[C]ls\f[]. -.PP -You can use the \f[C]dircolors\f[] program to generate a script that -sets the variable from an input file, or if you don\[aq]t mind editing -long strings of text, you can just type it out directly. -These variables have the following structure: -.IP \[bu] 2 -A list of key\-value pairs separated by \f[C]=\f[], such as -\f[C]*.txt=32\f[]. -.IP \[bu] 2 -Multiple ANSI formatting codes are separated by \f[C];\f[], such as -\f[C]*.txt=32;1;4\f[]. -.IP \[bu] 2 -Finally, multiple pairs are separated by \f[C]:\f[], such as -\f[C]*.txt=32:*.mp3=1;35\f[]. -.PP -The key half of the pair can either be a two\-letter code or a file -glob, and anything that\[aq]s not a valid code will be treated as a -glob, including keys that happen to be two letters long. -.PP -\f[C]LS_COLORS\f[] can use these ten codes: -.IP \[bu] 2 -\f[B]di\f[], directories -.IP \[bu] 2 -\f[B]ex\f[], executable files -.IP \[bu] 2 -\f[B]fi\f[], regular files -.IP \[bu] 2 -\f[B]pi\f[], named pipes -.IP \[bu] 2 -\f[B]so\f[], sockets -.IP \[bu] 2 -\f[B]bd\f[], block devices -.IP \[bu] 2 -\f[B]cd\f[], character devices -.IP \[bu] 2 -\f[B]ln\f[], symlinks -.IP \[bu] 2 -\f[B]or\f[], symlinks with no target -.PP -\f[C]EXA_COLORS\f[] can use many more: -.IP \[bu] 2 -\f[B]ur\f[], the user\-read permission bit -.IP \[bu] 2 -\f[B]uw\f[], the user\-write permission bit -.IP \[bu] 2 -\f[B]ux\f[], the user\-execute permission bit for regular files -.IP \[bu] 2 -\f[B]ue\f[], the user\-execute for other file kinds -.IP \[bu] 2 -\f[B]gr\f[], the group\-read permission bit -.IP \[bu] 2 -\f[B]gw\f[], the group\-write permission bit -.IP \[bu] 2 -\f[B]gx\f[], the group\-execute permission bit -.IP \[bu] 2 -\f[B]tr\f[], the others\-read permission bit -.IP \[bu] 2 -\f[B]tw\f[], the others\-write permission bit -.IP \[bu] 2 -\f[B]tx\f[], the others\-execute permission bit -.IP \[bu] 2 -\f[B]su\f[], setuid, setgid, and sticky permission bits for files -.IP \[bu] 2 -\f[B]sf\f[], setuid, setgid, and sticky for other file kinds -.IP \[bu] 2 -\f[B]xa\f[], the extended attribute indicator -.IP \[bu] 2 -\f[B]sn\f[], the numbers of a file\[aq]s size (sets nb, nk, nm, ng and nh) -.IP \[bu] 2 -\f[B]nb\f[], the numbers of a file\[aq]s size if it is lower than 1 KB/Kib -.IP \[bu] 2 -\f[B]nk\f[], the numbers of a file\[aq]s size if it is between 1 KB/KiB and 1 MB/MiB -.IP \[bu] 2 -\f[B]nm\f[], the numbers of a file\[aq]s size if it is between 1 MB/MiB and 1 GB/GiB -.IP \[bu] 2 -\f[B]ng\f[], the numbers of a file\[aq]s size if it is between 1 GB/GiB and 1 TB/TiB -.IP \[bu] 2 -\f[B]nt\f[], the numbers of a file\[aq]s size if it is 1 TB/TiB or higher -.IP \[bu] 2 -\f[B]sb\f[], the units of a file\[aq]s size (sets ub, uk, um, ug and uh) -.IP \[bu] 2 -\f[B]ub\f[], the units of a file\[aq]s size if it is lower than 1 KB/Kib -.IP \[bu] 2 -\f[B]uk\f[], the units of a file\[aq]s size if it is between 1 KB/KiB and 1 MB/MiB -.IP \[bu] 2 -\f[B]um\f[], the units of a file\[aq]s size if it is between 1 MB/MiB and 1 GB/GiB -.IP \[bu] 2 -\f[B]ug\f[], the units of a file\[aq]s size if it is between 1 GB/GiB and 1 TB/TiB -.IP \[bu] 2 -\f[B]ut\f[], the units of a file\[aq]s size if it is 1 TB/TiB or higher -.IP \[bu] 2 -\f[B]df\f[], a device\[aq]s major ID -.IP \[bu] 2 -\f[B]ds\f[], a device\[aq]s minor ID -.IP \[bu] 2 -\f[B]uu\f[], a user that\[aq]s you -.IP \[bu] 2 -\f[B]un\f[], a user that\[aq]s someone else -.IP \[bu] 2 -\f[B]gu\f[], a group that you belong to -.IP \[bu] 2 -\f[B]gn\f[], a group you aren\[aq]t a member of -.IP \[bu] 2 -\f[B]lc\f[], a number of hard links -.IP \[bu] 2 -\f[B]lm\f[], a number of hard links for a regular file with at least two -.IP \[bu] 2 -\f[B]ga\f[], a new flag in Git -.IP \[bu] 2 -\f[B]gm\f[], a modified flag in Git -.IP \[bu] 2 -\f[B]gd\f[], a deleted flag in Git -.IP \[bu] 2 -\f[B]gv\f[], a renamed flag in Git -.IP \[bu] 2 -\f[B]gt\f[], a modified metadata flag in Git -.IP \[bu] 2 -\f[B]xx\f[], "punctuation", including many background UI elements -.IP \[bu] 2 -\f[B]da\f[], a file\[aq]s date -.IP \[bu] 2 -\f[B]in\f[], a file\[aq]s inode number -.IP \[bu] 2 -\f[B]bl\f[], a file\[aq]s number of blocks -.IP \[bu] 2 -\f[B]hd\f[], the header row of a table -.IP \[bu] 2 -\f[B]lp\f[], the path of a symlink -.IP \[bu] 2 -\f[B]cc\f[], an escaped character in a filename -.IP \[bu] 2 -\f[B]bO\f[], the overlay style for broken symlink paths -.PP -Values in \f[C]EXA_COLORS\f[] override those given in -\f[C]LS_COLORS\f[], so you don\[aq]t need to re\-write an existing -\f[C]LS_COLORS\f[] variable with proprietary extensions. -.PP -Unlike some versions of \f[C]ls\f[], the given ANSI values must be valid -colour codes: exa won\[aq]t just print out whichever characters are -given. -The codes accepted by exa are: -.IP \[bu] 2 -\f[C]1\f[], for bold -.IP \[bu] 2 -\f[C]4\f[], for underline -.IP \[bu] 2 -\f[C]31\f[], for red text -.IP \[bu] 2 -\f[C]32\f[], for green text -.IP \[bu] 2 -\f[C]33\f[], for yellow text -.IP \[bu] 2 -\f[C]34\f[], for blue text -.IP \[bu] 2 -\f[C]35\f[], for purple text -.IP \[bu] 2 -\f[C]36\f[], for cyan text -.IP \[bu] 2 -\f[C]37\f[], for white text -.IP \[bu] 2 -\f[C]38;5;\f[]\f[I]\f[C]nnn\f[]\f[], for a colour from 0 to 255 (replace -the \f[I]nnn\f[] part) -.PP -Many terminals will treat bolded text as a different colour, or at least -provide the option to. -.PP -exa provides its own built\-in set of file extension mappings that cover -a large range of common file extensions, including documents, archives, -media, and temporary files. -Any mappings in the environment variables will override this default -set: running exa with \f[C]LS_COLORS="*.zip=32"\f[] will turn zip files -green but leave the colours of other compressed files alone. -.PP -You can also disable this built\-in set entirely by including a -\f[C]reset\f[] entry at the beginning of \f[C]EXA_COLORS\f[]. -So setting \f[C]EXA_COLORS="reset:*.txt=31"\f[] will highlight only text -files; setting \f[C]EXA_COLORS="reset"\f[] will highlight nothing. -.SS Examples -.IP \[bu] 2 -Disable the "current user" highlighting: \f[C]EXA_COLORS="uu=0:gu=0"\f[] -.IP \[bu] 2 -Turn the date column green: \f[C]EXA_COLORS="da=32"\f[] -.IP \[bu] 2 -Highlight Vagrantfiles: \f[C]EXA_COLORS="Vagrantfile=1;4;33"\f[] -.IP \[bu] 2 -Override the existing zip colour: \f[C]EXA_COLORS="*.zip=38;5;125"\f[] -.IP \[bu] 2 -Markdown files a shade of green, log files a shade of grey: -\f[C]EXA_COLORS="*.md=38;5;121:*.log=38;5;248"\f[] -.SS BUILT\-IN EXTENSIONS -.IP \[bu] 2 -"Immediate" files are the files you should look at when downloading and -building a project for the first time: READMEs, Makefiles, Cargo.toml, -and others. -They\[aq]re highlighted in yellow and underlined. -.IP \[bu] 2 -Images (png, jpeg, gif) are purple. -.IP \[bu] 2 -Videos (mp4, ogv, m2ts) are a slightly purpler purple. -.IP \[bu] 2 -Music (mp3, m4a, ogg) is a deeper purple. -.IP \[bu] 2 -Lossless music (flac, alac, wav) is deeper than \f[I]that\f[] purple. -In general, most media files are some shade of purple. -.IP \[bu] 2 -Cryptographic files (asc, enc, p12) are a faint blue. -.IP \[bu] 2 -Documents (pdf, doc, dvi) are a less faint blue. -.IP \[bu] 2 -Compressed files (zip, tgz, Z) are red. -.IP \[bu] 2 -Temporary files (tmp, swp, ~) are grey. -.IP \[bu] 2 -Compiled files (class, o, pyc) are faint orange. -A file is also counted as compiled if it uses a common extension and is -in the same directory as one of its source files: \[aq]styles.css\[aq] -will count as compiled when next to \[aq]styles.less\[aq] or -\[aq]styles.sass\[aq], and \[aq]scripts.js\[aq] when next to -\[aq]scripts.ts\[aq] or \[aq]scripts.coffee\[aq]. -.SH AUTHOR -.PP -\f[C]exa\f[] is maintained by Benjamin \[aq]ogham\[aq] Sago and many -other contributors. -You can view the full list at -. diff --git a/devtools/README.md b/devtools/README.md index 8136bdde..29f1e65d 100644 --- a/devtools/README.md +++ b/devtools/README.md @@ -1,5 +1,5 @@ -## exa development tools +## exa › development tools -These scripts deal with things like packaging release-worthy versions of exa and making sure the published versions actually work. +These scripts deal with things like packaging release-worthy versions of exa. -They are **not general-purpose scripts** that you’re able to run from your main computer! They’re intended to be run from the Vagrant machines — they have commands such as ‘package-exa’ or ‘check-release’ that execute them instead. +They are **not general-purpose scripts** that you’re able to run from your main computer! They’re intended to be run from the Vagrant machine. diff --git a/devtools/dev-bash.sh b/devtools/dev-bash.sh index e869ea2a..8a9073b4 100644 --- a/devtools/dev-bash.sh +++ b/devtools/dev-bash.sh @@ -11,35 +11,42 @@ bash /vagrant/devtools/dev-versions.sh # Configure the Cool Prompt™ (not actually trademarked). # The Cool Prompt tells you whether you’re in debug or strict mode, whether # you have colours configured, and whether your last command failed. -function nonzero_return() { RETVAL=$?; [ $RETVAL -ne 0 ] && echo "$RETVAL "; } -function debug_mode() { [ -n "$EXA_DEBUG" ] && echo "debug "; } -function strict_mode() { [ -n "$EXA_STRICT" ] && echo "strict "; } -function lsc_mode() { [ -n "$LS_COLORS" ] && echo "lsc "; } -function exac_mode() { [ -n "$EXA_COLORS" ] && echo "exac "; } +nonzero_return() { RETVAL=$?; [ "$RETVAL" -ne 0 ] && echo "$RETVAL "; } +debug_mode() { [ "$EXA_DEBUG" == "trace" ] && echo -n "trace-"; [ -n "$EXA_DEBUG" ] && echo "debug "; } +strict_mode() { [ -n "$EXA_STRICT" ] && echo "strict "; } +lsc_mode() { [ -n "$LS_COLORS" ] && echo "lsc "; } +exac_mode() { [ -n "$EXA_COLORS" ] && echo "exac "; } export PS1="\[\e[1;36m\]\h \[\e[32m\]\w \[\e[31m\]\`nonzero_return\`\[\e[35m\]\`debug_mode\`\[\e[32m\]\`lsc_mode\`\[\e[1;32m\]\`exac_mode\`\[\e[33m\]\`strict_mode\`\[\e[36m\]\\$\[\e[0m\] " # The ‘debug’ function lets you switch debug mode on and off. # Turn it on if you need to see exa’s debugging logs. -function debug () { - case "$1" in "on") export EXA_DEBUG=1 ;; - "off") export EXA_DEBUG= ;; - "") [ -n "$EXA_DEBUG" ] && echo "debug on" || echo "debug off" ;; - *) echo "Usage: debug on|off"; return 1 ;; esac; } +debug() { + case "$1" in + ""|"on") export EXA_DEBUG=1 ;; + "off") export EXA_DEBUG= ;; + "trace") export EXA_DEBUG=trace ;; + "status") [ -n "$EXA_DEBUG" ] && echo "debug on" || echo "debug off" ;; + *) echo "Usage: debug on|off|trace|status"; return 1 ;; + esac; +} # The ‘strict’ function lets you switch strict mode on and off. # Turn it on if you’d like exa’s command-line arguments checked. -function strict () { - case "$1" in "on") export EXA_STRICT=1 ;; +strict() { + case "$1" in + "on") export EXA_STRICT=1 ;; "off") export EXA_STRICT= ;; - "") [ -n "$EXA_STRICT" ] && echo "strict on" || echo "strict off" ;; - *) echo "Usage: strict on|off"; return 1 ;; esac; } + "") [ -n "$EXA_STRICT" ] && echo "strict on" || echo "strict off" ;; + *) echo "Usage: strict on|off"; return 1 ;; + esac; +} # The ‘colors’ function sets or unsets the ‘LS_COLORS’ and ‘EXA_COLORS’ # environment variables. There’s also a ‘hacker’ theme which turns everything # green, which is usually used for checking that all colour codes work, and # for looking cool while you phreak some mainframes or whatever. -function colors () { +colors() { case "$1" in "ls") export LS_COLORS="di=34:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43" @@ -53,4 +60,6 @@ function colors () { "") [ -n "$LS_COLORS" ] && echo "LS_COLORS=$LS_COLORS" || echo "ls-colors off" [ -n "$EXA_COLORS" ] && echo "EXA_COLORS=$EXA_COLORS" || echo "exa-colors off" ;; - *) echo "Usage: ls-colors ls|hacker|off"; return 1 ;; esac; } + *) echo "Usage: ls-colors ls|hacker|off"; return 1 ;; + esac; +} diff --git a/devtools/dev-create-test-filesystem.sh b/devtools/dev-create-test-filesystem.sh new file mode 100755 index 00000000..74d5e2ea --- /dev/null +++ b/devtools/dev-create-test-filesystem.sh @@ -0,0 +1,377 @@ +#!/bin/bash +# This script creates a bunch of awkward test case files. It gets +# automatically run as part of Vagrant provisioning. +trap 'exit' ERR + +if [[ ! -d "/vagrant" ]]; then + echo "This script should be run in the Vagrant environment" + exit 1 +fi + +source "/vagrant/devtools/dev-fixtures.sh" + + +# Delete old testcases if they exist already, then create a +# directory to house new ones. +if [[ -d "$TEST_ROOT" ]]; then + echo -e "\033[1m[ 0/13]\033[0m Deleting existing test cases directory" + sudo rm -rf "$TEST_ROOT" +fi + +sudo mkdir "$TEST_ROOT" +sudo chmod 777 "$TEST_ROOT" +sudo mkdir "$TEST_ROOT/empty" + + +# Awkward file size testcases. +# This needs sudo to set the files’ users at the very end. +mkdir "$TEST_ROOT/files" +echo -e "\033[1m[ 1/13]\033[0m Creating file size testcases" +for i in {1..13}; do + fallocate -l "$i" "$TEST_ROOT/files/$i"_bytes + fallocate -l "$i"KiB "$TEST_ROOT/files/$i"_KiB + fallocate -l "$i"MiB "$TEST_ROOT/files/$i"_MiB +done + +touch -t $FIXED_DATE "$TEST_ROOT/files/"* +touch -t $FIXED_DATE "$TEST_ROOT/files/" +chmod 644 "$TEST_ROOT/files/"* +sudo chown $FIXED_USER:$FIXED_USER "$TEST_ROOT/files/"* + + +# File name extension testcases. +# These aren’t tested in details view, but we set timestamps on them to +# test that various sort options work. +mkdir "$TEST_ROOT/file-names-exts" +echo -e "\033[1m[ 2/13]\033[0m Creating file name extension testcases" + +touch "$TEST_ROOT/file-names-exts/Makefile" + +touch "$TEST_ROOT/file-names-exts/IMAGE.PNG" +touch "$TEST_ROOT/file-names-exts/image.svg" + +touch "$TEST_ROOT/file-names-exts/VIDEO.AVI" +touch "$TEST_ROOT/file-names-exts/video.wmv" + +touch "$TEST_ROOT/file-names-exts/music.mp3" +touch "$TEST_ROOT/file-names-exts/MUSIC.OGG" + +touch "$TEST_ROOT/file-names-exts/lossless.flac" +touch "$TEST_ROOT/file-names-exts/lossless.wav" + +touch "$TEST_ROOT/file-names-exts/crypto.asc" +touch "$TEST_ROOT/file-names-exts/crypto.signature" + +touch "$TEST_ROOT/file-names-exts/document.pdf" +touch "$TEST_ROOT/file-names-exts/DOCUMENT.XLSX" + +touch "$TEST_ROOT/file-names-exts/COMPRESSED.ZIP" +touch "$TEST_ROOT/file-names-exts/compressed.tar.gz" +touch "$TEST_ROOT/file-names-exts/compressed.tgz" +touch "$TEST_ROOT/file-names-exts/compressed.tar.xz" +touch "$TEST_ROOT/file-names-exts/compressed.txz" +touch "$TEST_ROOT/file-names-exts/compressed.deb" + +touch "$TEST_ROOT/file-names-exts/backup~" +touch "$TEST_ROOT/file-names-exts/#SAVEFILE#" +touch "$TEST_ROOT/file-names-exts/file.tmp" + +touch "$TEST_ROOT/file-names-exts/compiled.class" +touch "$TEST_ROOT/file-names-exts/compiled.o" +touch "$TEST_ROOT/file-names-exts/compiled.js" +touch "$TEST_ROOT/file-names-exts/compiled.coffee" + + +# File name testcases. +# bash really doesn’t want you to create a file with escaped characters +# in its name, so we have to resort to the echo builtin and touch! +mkdir "$TEST_ROOT/file-names" +echo -e "\033[1m[ 3/13]\033[0m Creating file names testcases" + +echo -ne "$TEST_ROOT/file-names/ascii: hello" | xargs -0 touch +echo -ne "$TEST_ROOT/file-names/emoji: [🆒]" | xargs -0 touch +echo -ne "$TEST_ROOT/file-names/utf-8: pâté" | xargs -0 touch + +echo -ne "$TEST_ROOT/file-names/bell: [\a]" | xargs -0 touch +echo -ne "$TEST_ROOT/file-names/backspace: [\b]" | xargs -0 touch +echo -ne "$TEST_ROOT/file-names/form-feed: [\f]" | xargs -0 touch +echo -ne "$TEST_ROOT/file-names/new-line: [\n]" | xargs -0 touch +echo -ne "$TEST_ROOT/file-names/return: [\r]" | xargs -0 touch +echo -ne "$TEST_ROOT/file-names/tab: [\t]" | xargs -0 touch +echo -ne "$TEST_ROOT/file-names/vertical-tab: [\v]" | xargs -0 touch + +echo -ne "$TEST_ROOT/file-names/escape: [\033]" | xargs -0 touch +echo -ne "$TEST_ROOT/file-names/ansi: [\033[34mblue\033[0m]" | xargs -0 touch + +echo -ne "$TEST_ROOT/file-names/invalid-utf8-1: [\xFF]" | xargs -0 touch +echo -ne "$TEST_ROOT/file-names/invalid-utf8-2: [\xc3\x28]" | xargs -0 touch +echo -ne "$TEST_ROOT/file-names/invalid-utf8-3: [\xe2\x82\x28]" | xargs -0 touch +echo -ne "$TEST_ROOT/file-names/invalid-utf8-4: [\xf0\x28\x8c\x28]" | xargs -0 touch + +echo -ne "$TEST_ROOT/file-names/new-line-dir: [\n]" | xargs -0 mkdir +echo -ne "$TEST_ROOT/file-names/new-line-dir: [\n]/subfile" | xargs -0 touch +echo -ne "$TEST_ROOT/file-names/new-line-dir: [\n]/another: [\n]" | xargs -0 touch +echo -ne "$TEST_ROOT/file-names/new-line-dir: [\n]/broken" | xargs -0 touch + +mkdir "$TEST_ROOT/file-names/links" +ln -s "$TEST_ROOT/file-names/new-line-dir"*/* "$TEST_ROOT/file-names/links" + +echo -ne "$TEST_ROOT/file-names/new-line-dir: [\n]/broken" | xargs -0 rm + + +# Special file testcases. +mkdir "$TEST_ROOT/specials" +echo -e "\033[1m[ 4/13]\033[0m Creating special file kind testcases" + +sudo mknod "$TEST_ROOT/specials/block-device" b 3 60 +sudo mknod "$TEST_ROOT/specials/char-device" c 14 40 +sudo mknod "$TEST_ROOT/specials/named-pipe" p + +sudo touch -t $FIXED_DATE "$TEST_ROOT/specials/"* + + +# Awkward symlink testcases. +mkdir "$TEST_ROOT/links" +echo -e "\033[1m[ 5/13]\033[0m Creating symlink testcases" + +ln -s / "$TEST_ROOT/links/root" +ln -s /usr "$TEST_ROOT/links/usr" +ln -s nowhere "$TEST_ROOT/links/broken" +ln -s /proc/1/root "$TEST_ROOT/links/forbidden" + +touch "$TEST_ROOT/links/some_file" +ln -s "$TEST_ROOT/links/some_file" "$TEST_ROOT/links/some_file_absolute" +(cd "$TEST_ROOT/links"; ln -s "some_file" "some_file_relative") +(cd "$TEST_ROOT/links"; ln -s "." "current_dir") +(cd "$TEST_ROOT/links"; ln -s ".." "parent_dir") +(cd "$TEST_ROOT/links"; ln -s "itself" "itself") + + +# Awkward passwd testcases. +# sudo is needed for these because we technically aren’t a member +# of the groups (because they don’t exist), and chown and chgrp +# are smart enough to disallow it! +mkdir "$TEST_ROOT/passwd" +echo -e "\033[1m[ 6/13]\033[0m Creating user and group testcases" + +touch -t $FIXED_DATE "$TEST_ROOT/passwd/unknown-uid" +chmod 644 "$TEST_ROOT/passwd/unknown-uid" +sudo chown $FIXED_BAD_UID:$FIXED_USER "$TEST_ROOT/passwd/unknown-uid" + +touch -t $FIXED_DATE "$TEST_ROOT/passwd/unknown-gid" +chmod 644 "$TEST_ROOT/passwd/unknown-gid" +sudo chown $FIXED_USER:$FIXED_BAD_GID "$TEST_ROOT/passwd/unknown-gid" + + +# Awkward permission testcases. +# Differences in the way ‘chmod’ handles setting ‘setuid’ and ‘setgid’ +# when you don’t already own the file mean that we need to use ‘sudo’ +# to change permissions to those. +mkdir "$TEST_ROOT/permissions" +echo -e "\033[1m[ 7/13]\033[0m Creating file permission testcases" + +mkdir "$TEST_ROOT/permissions/forbidden-directory" +chmod 000 "$TEST_ROOT/permissions/forbidden-directory" +touch -t $FIXED_DATE "$TEST_ROOT/permissions/forbidden-directory" +sudo chown $FIXED_USER:$FIXED_USER "$TEST_ROOT/permissions/forbidden-directory" + +for perms in 000 001 002 004 010 020 040 100 200 400 644 755 777 1000 1001 2000 2010 4000 4100 7666 7777; do + touch "$TEST_ROOT/permissions/$perms" + sudo chown $FIXED_USER:$FIXED_USER "$TEST_ROOT/permissions/$perms" + sudo chmod $perms "$TEST_ROOT/permissions/$perms" + sudo touch -t $FIXED_DATE "$TEST_ROOT/permissions/$perms" +done + + +# Awkward date and time testcases. +mkdir "$TEST_ROOT/dates" +echo -e "\033[1m[ 8/13]\033[0m Creating date and time testcases" + +# created dates +# there’s no way to touch the created date of a file... +# so we have to do this the old-fashioned way! +# (and make sure these don't actually get listed) +touch -t $FIXED_OLD_DATE "$TEST_ROOT/dates/peach"; sleep 1 +touch -t $FIXED_MED_DATE "$TEST_ROOT/dates/plum"; sleep 1 +touch -t $FIXED_NEW_DATE "$TEST_ROOT/dates/pear" + +# modified dates +touch -t $FIXED_OLD_DATE -m "$TEST_ROOT/dates/pear" +touch -t $FIXED_MED_DATE -m "$TEST_ROOT/dates/peach" +touch -t $FIXED_NEW_DATE -m "$TEST_ROOT/dates/plum" + +# accessed dates +touch -t $FIXED_OLD_DATE -a "$TEST_ROOT/dates/plum" +touch -t $FIXED_MED_DATE -a "$TEST_ROOT/dates/pear" +touch -t $FIXED_NEW_DATE -a "$TEST_ROOT/dates/peach" + +sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/dates" + +mkdir "$TEST_ROOT/far-dates" +touch -t $FIXED_PAST_DATE "$TEST_ROOT/far-dates/the-distant-past" +touch -t $FIXED_FUTURE_DATE "$TEST_ROOT/far-dates/beyond-the-future" + + +# Awkward extended attribute testcases. +# We need to test combinations of various numbers of files *and* +# extended attributes in directories. Turns out, the easiest way to +# do this is to generate all combinations of files with “one-xattr” +# or “two-xattrs” in their name and directories with “empty” or +# “one-file” in their name, then just give the right number of +# xattrs and children to those. +mkdir "$TEST_ROOT/attributes" +echo -e "\033[1m[ 9/13]\033[0m Creating extended attribute testcases" + +mkdir "$TEST_ROOT/attributes/files" +touch "$TEST_ROOT/attributes/files/"{no-xattrs,one-xattr,two-xattrs}{,_forbidden} + +mkdir "$TEST_ROOT/attributes/dirs" +mkdir "$TEST_ROOT/attributes/dirs/"{no-xattrs,one-xattr,two-xattrs}_{empty,one-file,two-files}{,_forbidden} + +setfattr -n user.greeting -v hello "$TEST_ROOT/attributes"/**/*{one-xattr,two-xattrs}* +setfattr -n user.another_greeting -v hi "$TEST_ROOT/attributes"/**/*two-xattrs* + +for dir in "$TEST_ROOT/attributes/dirs/"*one-file*; do + touch $dir/file-in-question +done + +for dir in "$TEST_ROOT/attributes/dirs/"*two-files*; do + touch $dir/this-file + touch $dir/that-file +done + +find "$TEST_ROOT/attributes" -exec touch {} -t $FIXED_DATE \; + +# I want to use the following to test, +# but it only works on macos: +#chmod +a "$FIXED_USER deny readextattr" "$TEST_ROOT/attributes"/**/*_forbidden + +sudo chmod 000 "$TEST_ROOT/attributes"/**/*_forbidden +sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/attributes" + + +# A sample Git repository +# This uses cd because it's easier than telling Git where to go each time +echo -e "\033[1m[10/13]\033[0m Creating Git testcases (1/4)" +mkdir "$TEST_ROOT/git" +cd "$TEST_ROOT/git" +git init >/dev/null + +mkdir edits additions moves + +echo "original content" | tee edits/{staged,unstaged,both} >/dev/null +echo "this file gets moved" > moves/hither + +git add edits moves +git config --global user.email "exa@exa.exa" +git config --global user.name "Exa Exa" +git commit -m "Automated test commit" >/dev/null + +echo "modifications!" | tee edits/{staged,both} >/dev/null +touch additions/{staged,edited} +mv moves/{hither,thither} + +git add edits moves additions +echo "more modifications!" | tee edits/unstaged edits/both additions/edited >/dev/null +touch additions/unstaged + +find "$TEST_ROOT/git" -exec touch {} -t $FIXED_DATE \; +sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/git" + + +# A second Git repository +# for testing two at once +echo -e "\033[1m[11/13]\033[0m Creating Git testcases (2/4)" +mkdir -p "$TEST_ROOT/git2/deeply/nested/directory" +cd "$TEST_ROOT/git2" +git init >/dev/null + +touch "deeply/nested/directory/upd8d" +git add "deeply/nested/directory/upd8d" +git commit -m "Automated test commit" >/dev/null + +echo "Now with contents" > "deeply/nested/directory/upd8d" +touch "deeply/nested/directory/l8st" + +echo -e "target\n*.mp3" > ".gitignore" +mkdir "ignoreds" +touch "ignoreds/music.mp3" +touch "ignoreds/music.m4a" +mkdir "ignoreds/nested" +touch "ignoreds/nested/70s grove.mp3" +touch "ignoreds/nested/funky chicken.m4a" +mkdir "ignoreds/nested2" +touch "ignoreds/nested2/ievan polkka.mp3" + +mkdir "target" +touch "target/another ignored file" + +mkdir "deeply/nested/repository" +cd "deeply/nested/repository" +git init >/dev/null +touch subfile +# This file, ‘subfile’, should _not_ be marked as a new file by exa, because +# it’s in the sub-repository but hasn’t been added to it. Were the sub-repo not +# present, it would be marked as a new file, as the top-level repo knows about +# the ‘deeply’ directory. + +find "$TEST_ROOT/git2" -exec touch {} -t $FIXED_DATE \; +sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/git2" + + +# A third Git repository +# Regression test for https://github.com/ogham/exa/issues/526 +echo -e "\033[1m[12/13]\033[0m Creating Git testcases (3/4)" +mkdir -p "$TEST_ROOT/git3" +cd "$TEST_ROOT/git3" +git init >/dev/null + +# Create a symbolic link pointing to a non-existing file +ln -s aaa/aaa/a b + +# This normally fails with: +find "$TEST_ROOT/git3" -exec touch {} -h -t $FIXED_DATE \; +sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/git3" + + +# A fourth Git repository +# Regression test for https://github.com/ogham/exa/issues/698 +echo -e "\033[1m[12/13]\033[0m Creating Git testcases (4/4)" +mkdir -p "$TEST_ROOT/git4" +cd "$TEST_ROOT/git4" +git init >/dev/null + +# Create a non UTF-8 file +touch 'P'$'\b\211''UUU' + +find "$TEST_ROOT/git4" -exec touch {} -h -t $FIXED_DATE \; +sudo chown $FIXED_USER:$FIXED_USER -R "$TEST_ROOT/git4" + + +# Hidden and dot file testcases. +# We need to set the permissions of `.` and `..` because they actually +# get displayed in the output here, so this has to come last. +echo -e "\033[1m[13/13]\033[0m Creating hidden and dot file testcases" +shopt -u dotglob +GLOBIGNORE=".:.." + +mkdir "$TEST_ROOT/hiddens" +cd "$TEST_ROOT/hiddens" +touch "$TEST_ROOT/hiddens/visible" +touch "$TEST_ROOT/hiddens/.hidden" +touch "$TEST_ROOT/hiddens/..extra-hidden" + +# ./hiddens/ +touch -t $FIXED_DATE "$TEST_ROOT/hiddens/"* +chmod 644 "$TEST_ROOT/hiddens/"* +sudo chown $FIXED_USER:$FIXED_USER "$TEST_ROOT/hiddens/"* + +# . +touch -t $FIXED_DATE "$TEST_ROOT/hiddens" +chmod 755 "$TEST_ROOT/hiddens" +sudo chown $FIXED_USER:$FIXED_USER "$TEST_ROOT/hiddens" + +# .. +sudo touch -t $FIXED_DATE "$TEST_ROOT" +sudo chmod 755 "$TEST_ROOT" +sudo chown $FIXED_USER:$FIXED_USER "$TEST_ROOT" diff --git a/devtools/dev-download-and-check-release.sh b/devtools/dev-download-and-check-release.sh deleted file mode 100644 index d2b89a95..00000000 --- a/devtools/dev-download-and-check-release.sh +++ /dev/null @@ -1,51 +0,0 @@ -# This script downloads the published versions of exa from GitHub and my site, -# checks that the checksums match, and makes sure the files at least unzip and -# execute okay. -# -# The argument should be of the form “0.8.0”, no ‘v’. That version was the -# first one to offer checksums, so it’s the minimum version that can be tested. - -set +x -trap 'exit' ERR - -exa_version=$1 -if [[ -z "$exa_version" ]]; then - echo "Please specify a version, such as '$0 0.8.0'" - exit 1 -fi - - -# Delete anything that already exists -rm -rfv "/tmp/${exa_version}-downloads" - - -# Create a temporary directory and download exa into it -mkdir "/tmp/${exa_version}-downloads" -cd "/tmp/${exa_version}-downloads" - -echo -e "\n\033[4mDownloading stuff...\033[0m" -wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/exa-macos-x86_64-${exa_version}.zip" -wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/exa-linux-x86_64-${exa_version}.zip" - -wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/MD5SUMS" -wget --quiet --show-progress "https://github.com/ogham/exa/releases/download/v${exa_version}/SHA1SUMS" - - -# Unzip the zips and check the sums -echo -e "\n\033[4mExtracting that stuff...\033[0m" -unzip "exa-macos-x86_64-${exa_version}.zip" -unzip "exa-linux-x86_64-${exa_version}.zip" - -echo -e "\n\033[4mValidating MD5 checksums...\033[0m" -md5sum -c MD5SUMS - -echo -e "\n\033[4mValidating SHA1 checksums...\033[0m" -sha1sum -c SHA1SUMS - - -# Finally, give the Linux version a go -echo -e "\n\033[4mChecking it actually runs...\033[0m" -./"exa-linux-x86_64" --version -./"exa-linux-x86_64" --long - -echo -e "\n\033[1;32mAll's lookin' good!\033[0m" diff --git a/devtools/dev-fixtures.sh b/devtools/dev-fixtures.sh new file mode 100644 index 00000000..34256b67 --- /dev/null +++ b/devtools/dev-fixtures.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# This file contains the text fixtures — the known, constant data — that are +# used when setting up the environment that exa’s tests get run in. + + +# The directory that all the test files are created under. +export TEST_ROOT=/testcases + + +# Because the timestamps are formatted differently depending on whether +# they’re in the current year or not (see `details.rs`), we have to make +# sure that the files are created in the current year, so they get shown +# in the format we expect. +export CURRENT_YEAR=$(date "+%Y") +export FIXED_DATE="${CURRENT_YEAR}01011234.56" # 1st January, 12:34:56 + + +# We also need an UID and a GID that are guaranteed to not exist, to +# test what happen when they don’t. +export FIXED_BAD_UID=666 +export FIXED_BAD_GID=616 + + +# We create two users that own the test files. +# +# The first one just owns the ordinary ones, because we don’t want the +# test outputs to depend on “vagrant” or “ubuntu” existing. +# +# The second one has a long name, to test that the file owner column +# widens correctly. The benefit of Vagrant is that we don’t need to +# set this up on the *actual* system! +export FIXED_USER="cassowary" +export FIXED_LONG_USER="antidisestablishmentarienism" + + +# A couple of dates, for date-time testing. +export FIXED_OLD_DATE='200303030000.00' +export FIXED_MED_DATE='200606152314.29' # the june gets used for fr_FR locale tests +export FIXED_NEW_DATE='200912221038.53' # and the december for ja_JP local tests + +# Dates that extend beyond 32-bit timespace. +export FIXED_PAST_DATE='170001010000.00' +export FIXED_FUTURE_DATE='230001010000.00' diff --git a/devtools/dev-generate-checksums.sh b/devtools/dev-generate-checksums.sh deleted file mode 100644 index 0075c3fa..00000000 --- a/devtools/dev-generate-checksums.sh +++ /dev/null @@ -1,15 +0,0 @@ -# This script generates the MD5SUMS and SHA1SUMS files. -# You’ll need to have run ‘dev-download-and-check-release.sh’ and -# ‘local-package-for-macos.sh’ scripts to generate the binaries first. - -set +x -trap 'exit' ERR - -cd /vagrant -rm -f MD5SUMS SHA1SUMS - -echo -e "\n\033[4mValidating MD5 checksums...\033[0m" -md5sum exa-linux-x86_64 exa-macos-x86_64 | tee MD5SUMS - -echo -e "\n\033[4mValidating SHA1 checksums...\033[0m" -sha1sum exa-linux-x86_64 exa-macos-x86_64 | tee SHA1SUMS diff --git a/devtools/dev-help-testvm.sh b/devtools/dev-help-testvm.sh deleted file mode 100644 index 0961792e..00000000 --- a/devtools/dev-help-testvm.sh +++ /dev/null @@ -1,12 +0,0 @@ -# This file is like the other one, except for the testing VM. -# It also gets dumped into /etc/motd. - - -echo -e " -\033[1;33mThe exa testing environment!\033[0m -This machine is dependency-free, and can be used to test that -released versions of exa still work on vanilla Linux installs. - -\033[4mCommands\033[0m -\033[32;1mcheck-release\033[0m to download and verify released binaries -" diff --git a/devtools/dev-package-for-linux.sh b/devtools/dev-package-for-linux.sh index 146b3250..4497df0e 100644 --- a/devtools/dev-package-for-linux.sh +++ b/devtools/dev-package-for-linux.sh @@ -9,7 +9,7 @@ set -e # Linux check! -uname=`uname -s` +uname=$(uname -s) if [[ "$uname" != "Linux" ]]; then echo "Gotta be on Linux to run this (detected '$uname')!" exit 1 @@ -29,8 +29,8 @@ fi # Weekly builds have a bit more information in their version number (see build.rs). if [[ "$1" == "--weekly" ]]; then - git_hash=`GIT_DIR=/vagrant/.git git rev-parse --short --verify HEAD` - date=`date +"%Y-%m-%d"` + git_hash=$(GIT_DIR=/vagrant/.git git rev-parse --short --verify HEAD) + date=$(date +"%Y-%m-%d") echo "Building exa weekly v$exa_version, date $date, Git hash $git_hash" else echo "Building exa v$exa_version" @@ -57,9 +57,10 @@ strip -v "$exa_linux_binary" # the binaries can have consistent names, and it’s still possible to tell # different *downloads* apart. echo -e "\n\033[4mZipping binary...\033[0m" -if [[ "$1" == "--weekly" ]] - then exa_linux_zip="/vagrant/exa-linux-x86_64-${exa_version}-${date}-${git_hash}.zip" - else exa_linux_zip="/vagrant/exa-linux-x86_64-${exa_version}.zip" +if [[ "$1" == "--weekly" ]]; then + exa_linux_zip="/vagrant/exa-linux-x86_64-${exa_version}-${date}-${git_hash}.zip" +else + exa_linux_zip="/vagrant/exa-linux-x86_64.zip" fi rm -vf "$exa_linux_zip" zip -j "$exa_linux_zip" "$exa_linux_binary" diff --git a/devtools/dev-set-up-environment.sh b/devtools/dev-set-up-environment.sh new file mode 100755 index 00000000..518e6e40 --- /dev/null +++ b/devtools/dev-set-up-environment.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +if [[ ! -d "/vagrant" ]]; then + echo "This script should be run in the Vagrant environment" + exit 1 +fi + +if [[ $EUID -ne 0 ]]; then + echo "This script should be run as root" + exit 1 +fi + +source "/vagrant/devtools/dev-fixtures.sh" + + +# create our test users + +if id -u $FIXED_USER &>/dev/null; then + echo "Normal user already exists" +else + echo "Creating normal user" + useradd $FIXED_USER +fi + +if id -u $FIXED_LONG_USER &>/dev/null; then + echo "Long user already exists" +else + echo "Creating long user" + useradd $FIXED_LONG_USER +fi + + +# locale generation + +# remove most of this file, it slows down locale-gen +if grep -F -q "en_GB.UTF-8 UTF-8" /var/lib/locales/supported.d/en; then + echo "Removing existing locales" + echo "en_US.UTF-8 UTF-8" > /var/lib/locales/supported.d/en +fi + +# uncomment these from the config file +if grep -F -q "# fr_FR.UTF-8" /etc/locale.gen; then + sed -i '/fr_FR.UTF-8/s/^# //g' /etc/locale.gen +fi +if grep -F -q "# ja_JP.UTF-8" /etc/locale.gen; then + sed -i '/ja_JP.UTF-8/s/^# //g' /etc/locale.gen +fi + +# only regenerate locales if the config files are newer than the locale archive +if [[ ( /var/lib/locales/supported.d/en -nt /usr/lib/locale/locale-archive ) || \ + ( /etc/locale_gen -nt /usr/lib/locale/locale-archive ) ]]; then + locale-gen +else + echo "Locales already generated" +fi diff --git a/devtools/local-package-for-macos.sh b/devtools/local-package-for-macos.sh index 57dfbe6f..f82841c3 100644 --- a/devtools/local-package-for-macos.sh +++ b/devtools/local-package-for-macos.sh @@ -11,7 +11,7 @@ set -e # Virtualising macOS is a legal minefield, so this script is ‘local’ instead # of ‘dev’: I run it from my actual machine, rather than from a VM. -uname=`uname -s` +uname=$(uname -s) if [[ "$uname" != "Darwin" ]]; then echo "Gotta be on Darwin to run this (detected '$uname')!" exit 1 @@ -36,8 +36,8 @@ fi # Weekly builds have a bit more information in their version number (see build.rs). if [[ "$1" == "--weekly" ]]; then - git_hash=`GIT_DIR=$exa_root/.git git rev-parse --short --verify HEAD` - date=`date +"%Y-%m-%d"` + git_hash=$(GIT_DIR=$exa_root/.git git rev-parse --short --verify HEAD) + date=$(date +"%Y-%m-%d") echo "Building exa weekly v$exa_version, date $date, Git hash $git_hash" else echo "Building exa v$exa_version" @@ -65,9 +65,10 @@ echo "strip $exa_macos_binary" # the binaries can have consistent names, and it’s still possible to tell # different *downloads* apart. echo -e "\n\033[4mZipping binary...\033[0m" -if [[ "$1" == "--weekly" ]] - then exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}-${date}-${git_hash}.zip" - else exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}.zip" +if [[ "$1" == "--weekly" ]]; then + exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}-${date}-${git_hash}.zip" +else + exa_macos_zip="$exa_root/exa-macos-x86_64-${exa_version}.zip" fi rm -vf "$exa_macos_zip" | sed 's/^/removing /' zip -j "$exa_macos_zip" "$exa_macos_binary" diff --git a/man/exa.1.md b/man/exa.1.md new file mode 100644 index 00000000..7bafd3f7 --- /dev/null +++ b/man/exa.1.md @@ -0,0 +1,266 @@ +% exa(1) v0.9.0 + + + + + + +NAME +==== + +exa — a modern replacement for ls + + +SYNOPSIS +======== + +`exa [options] [files...]` + +**exa** is a modern replacement for `ls`. +It uses colours for information by default, helping you distinguish between many types of files, such as whether you are the owner, or in the owning group. + +It also has extra features not present in the original `ls`, such as viewing the Git status for a directory, or recursing into directories with a tree view. + + +EXAMPLES +======== + +`exa` +: Lists the contents of the current directory in a grid. + +`exa --oneline --reverse --sort=size` +: Displays a list of files with the largest at the top. + +`exa --long --header --inode --git` +: Displays a table of files with a header, showing each file’s metadata, inode, and Git status. + +`exa --long --tree --level=3` +: Displays a tree of files, three levels deep, as well as each file’s metadata. + + +DISPLAY OPTIONS +=============== + +`-1`, `--oneline` +: Display one entry per line. + +`-F`, `--classify` +: Display file kind indicators next to file names. + +`-G`, `--grid` +: Display entries as a grid (default). + +`-l`, `--long` +: Display extended file metadata as a table. + +`-R`, `--recurse` +: Recurse into directories. + +`-T`, `--tree` +: Recurse into directories as a tree. + +`-x`, `--across` +: Sort the grid across, rather than downwards. + +`--color`, `--colour=WHEN` +: When to use terminal colours. +Valid settings are ‘`always`’, ‘`automatic`’, and ‘`never`’. + +`--color-scale`, `--colour-scale` +: Colour file sizes on a scale. + +`--icons` +: Display icons next to file names. + +`--no-icons` +: Don't display icons. (Always overrides --icons) + + +FILTERING AND SORTING OPTIONS +============================= + +`-a`, `--all` +: Show hidden and “dot” files. +Use this twice to also show the ‘`.`’ and ‘`..`’ directories. + +`-d`, `--list-dirs` +: List directories as regular files, rather than recursing and listing their contents. + +`-L`, `--level=DEPTH` +: Limit the depth of recursion. + +`-r`, `--reverse` +: Reverse the sort order. + +`-s`, `--sort=SORT_FIELD` +: Which field to sort by. + +Valid sort fields are ‘`name`’, ‘`Name`’, ‘`extension`’, ‘`Extension`’, ‘`size`’, ‘`modified`’, ‘`changed`’, ‘`accessed`’, ‘`created`’, ‘`inode`’, ‘`type`’, and ‘`none`’. + +The `modified` sort field has the aliases ‘`date`’, ‘`time`’, and ‘`newest`’, and its reverse order has the aliases ‘`age`’ and ‘`oldest`’. + +Sort fields starting with a capital letter will sort uppercase before lowercase: ‘A’ then ‘B’ then ‘a’ then ‘b’. Fields starting with a lowercase letter will mix them: ‘A’ then ‘a’ then ‘B’ then ‘b’. + +`-I`, `--ignore-glob=GLOBS` +: Glob patterns, pipe-separated, of files to ignore. + +`--git-ignore` [if exa was built with git support] +: Do not list files that are ignored by Git. + +`--group-directories-first` +: List directories before other files. + +`-D`, `--only-dirs` +: List only directories, not files. + + +LONG VIEW OPTIONS +================= + +These options are available when running with `--long` (`-l`): + +`-b`, `--binary` +: List file sizes with binary prefixes. + +`-B`, `--bytes` +: List file sizes in bytes, without any prefixes. + +`--changed` +: Use the changed timestamp field. + +`-g`, `--group` +: List each file’s group. + +`-h`, `--header` +: Add a header row to each column. + +`-H`, `--links` +: List each file’s number of hard links. + +`-i`, `--inode` +: List each file’s inode number. + +`-m`, `--modified` +: Use the modified timestamp field. + +`-n`, `--numeric` +: List numeric user and group IDs. + +`-S`, `--blocks` +: List each file’s number of file system blocks. + +`-t`, `--time=WORD` +: Which timestamp field to list. + +: Valid timestamp fields are ‘`modified`’, ‘`changed`’, ‘`accessed`’, and ‘`created`’. + +`--time-style=STYLE` +: How to format timestamps. + +: Valid timestamp styles are ‘`default`’, ‘`iso`’, ‘`long-iso`’, and ‘`full-iso`’. + +`-u`, `--accessed` +: Use the accessed timestamp field. + +`-U`, `--created` +: Use the created timestamp field. + +`--no-permissions` +: Suppress the permissions field. + +`--no-filesize` +: Suppress the file size field. + +`--no-user` +: Suppress the user field. + +`--no-time` +: Suppress the time field. + +`-@`, `--extended` +: List each file’s extended attributes and sizes. + +`--git` [if exa was built with git support] +: List each file’s Git status, if tracked. + +This adds a two-character column indicating the staged and unstaged statuses respectively. The status character can be ‘`-`’ for not modified, ‘`M`’ for a modified file, ‘`N`’ for a new file, ‘`D`’ for deleted, ‘`R`’ for renamed, ‘`T`’ for type-change, ‘`I`’ for ignored, and ‘`U`’ for conflicted. + +Directories will be shown to have the status of their contents, which is how ‘deleted’ is possible: if a directory contains a file that has a certain status, it will be shown to have that status. + + +ENVIRONMENT VARIABLES +===================== + +exa responds to the following environment variables: + +## `COLUMNS` + +Overrides the width of the terminal, in characters. + +For example, ‘`COLUMNS=80 exa`’ will show a grid view with a maximum width of 80 characters. + +This option won’t do anything when exa’s output doesn’t wrap, such as when using the `--long` view. + +## `EXA_STRICT` + +Enables _strict mode_, which will make exa error when two command-line options are incompatible. + +Usually, options can override each other going right-to-left on the command line, so that exa can be given aliases: creating an alias ‘`exa=exa --sort=ext`’ then running ‘`exa --sort=size`’ with that alias will run ‘`exa --sort=ext --sort=size`’, and the sorting specified by the user will override the sorting specified by the alias. + +In strict mode, the two options will not co-operate, and exa will error. + +This option is intended for use with automated scripts and other situations where you want to be certain you’re typing in the right command. + +## `EXA_GRID_ROWS` + +Limits the grid-details view (‘`exa --grid --long`’) so it’s only activated when at least the given number of rows of output would be generated. + +With widescreen displays, it’s possible for the grid to look very wide and sparse, on just one or two lines with none of the columns lining up. +By specifying a minimum number of rows, you can only use the view if it’s going to be worth using. + +## `EXA_ICON_SPACING` + +Specifies the number of spaces to print between an icon (see the ‘`--icons`’ option) and its file name. + +Different terminals display icons differently, as they usually take up more than one character width on screen, so there’s no “standard” number of spaces that exa can use to separate an icon from text. One space may place the icon too close to the text, and two spaces may place it too far away. So the choice is left up to the user to configure depending on their terminal emulator. + +## `NO_COLOR` + +Disables colours in the output (regardless of its value). Can be overridden by `--color` option. + +See `https://no-color.org/` for details. + +## `LS_COLORS`, `EXA_COLORS` + +Specifies the colour scheme used to highlight files based on their name and kind, as well as highlighting metadata and parts of the UI. + +For more information on the format of these environment variables, see the `exa_colors(5)` manual page. + + +EXIT STATUSES +============= + +0 +: If everything goes OK. + +1 +: If there was an I/O error during operation. + +3 +: If there was a problem with the command-line arguments. + + +AUTHOR +====== + +exa is maintained by Benjamin ‘ogham’ Sago and many other contributors. + +**Website:** `https://the.exa.website/` \ +**Source code:** `https://github.com/ogham/exa` \ +**Contributors:** `https://github.com/ogham/exa/graphs/contributors` + + +SEE ALSO +======== + +- `exa_colors(5)` diff --git a/man/exa_colors.5.md b/man/exa_colors.5.md new file mode 100644 index 00000000..c775f0a1 --- /dev/null +++ b/man/exa_colors.5.md @@ -0,0 +1,282 @@ +% exa_colors(5) v0.9.0 + + + + + + +NAME +==== + +exa_colors — customising the file and UI colours of exa + + +SYNOPSIS +======== + +The `EXA_COLORS` environment variable can be used to customise the colours that `exa` uses to highlight file names, file metadata, and parts of the UI. + +You can use the `dircolors` program to generate a script that sets the variable from an input file, or if you don’t mind editing long strings of text, you can just type it out directly. These variables have the following structure: + +- A list of key-value pairs separated by ‘`=`’, such as ‘`*.txt=32`’. +- Multiple ANSI formatting codes are separated by ‘`;`’, such as ‘`*.txt=32;1;4`’. +- Finally, multiple pairs are separated by ‘`:`’, such as ‘`*.txt=32:*.mp3=1;35`’. + +The key half of the pair can either be a two-letter code or a file glob, and anything that’s not a valid code will be treated as a glob, including keys that happen to be two letters long. + + +EXAMPLES +======== + +`EXA_COLORS="uu=0:gu=0"` +: Disable the “current user” highlighting + +`EXA_COLORS="da=32"` +: Turn the date column green + +`EXA_COLORS="Vagrantfile=1;4;33"` +: Highlight Vagrantfiles + +`EXA_COLORS="*.zip=38;5;125"` +: Override the existing zip colour + +`EXA_COLORS="*.md=38;5;121:*.log=38;5;248"` +: Markdown files a shade of green, log files a shade of grey + + +LIST OF CODES +============= + +`LS_COLORS` can use these ten codes: + +`di` +: directories + +`ex` +: executable files + +`fi` +: regular files + +`pi` +: named pipes + +`so` +: sockets + +`bd` +: block devices + +`cd` +: character devices + +`ln` +: symlinks + +`or` +: symlinks with no target + + +`EXA_COLORS` can use many more: + +`ur` +: the user-read permission bit + +`uw` +: the user-write permission bit + +`ux` +: the user-execute permission bit for regular files + +`ue` +: the user-execute for other file kinds + +`gr` +: the group-read permission bit + +`gw` +: the group-write permission bit + +`gx` +: the group-execute permission bit + +`tr` +: the others-read permission bit + +`tw` +: the others-write permission bit + +`tx` +: the others-execute permission bit + +`su` +: setuid, setgid, and sticky permission bits for files + +`sf` +: setuid, setgid, and sticky for other file kinds + +`xa` +: the extended attribute indicator + +`sn` +: the numbers of a file’s size (sets `nb`, `nk`, `nm`, `ng` and `nh`) + +`nb` +: the numbers of a file’s size if it is lower than 1 KB/Kib + +`nk` +: the numbers of a file’s size if it is between 1 KB/KiB and 1 MB/MiB + +`nm` +: the numbers of a file’s size if it is between 1 MB/MiB and 1 GB/GiB + +`ng` +: the numbers of a file’s size if it is between 1 GB/GiB and 1 TB/TiB + +`nt` +: the numbers of a file’s size if it is 1 TB/TiB or higher + +`sb` +: the units of a file’s size (sets `ub`, `uk`, `um`, `ug` and `uh`) + +`ub` +: the units of a file’s size if it is lower than 1 KB/Kib + +`uk` +: the units of a file’s size if it is between 1 KB/KiB and 1 MB/MiB + +`um` +: the units of a file’s size if it is between 1 MB/MiB and 1 GB/GiB + +`ug` +: the units of a file’s size if it is between 1 GB/GiB and 1 TB/TiB + +`ut` +: the units of a file’s size if it is 1 TB/TiB or higher + +`df` +: a device’s major ID + +`ds` +: a device’s minor ID + +`uu` +: a user that’s you + +`un` +: a user that’s someone else + +`gu` +: a group that you belong to + +`gn` +: a group you aren’t a member of + +`lc` +: a number of hard links + +`lm` +: a number of hard links for a regular file with at least two + +`ga` +: a new flag in Git + +`gm` +: a modified flag in Git + +`gd` +: a deleted flag in Git + +`gv` +: a renamed flag in Git + +`gt` +: a modified metadata flag in Git + +`xx` +: “punctuation”, including many background UI elements + +`da` +: a file’s date + +`in` +: a file’s inode number + +`bl` +: a file’s number of blocks + +`hd` +: the header row of a table + +`lp` +: the path of a symlink + +`cc` +: an escaped character in a filename + +`bO` +: the overlay style for broken symlink paths + +Values in `EXA_COLORS` override those given in `LS_COLORS`, so you don’t need to re-write an existing `LS_COLORS` variable with proprietary extensions. + + +LIST OF STYLES +============== + +Unlike some versions of `ls`, the given ANSI values must be valid colour codes: exa won’t just print out whichever characters are given. + +The codes accepted by exa are: + +`1` +: for bold + +`4` +: for underline + +`31` +: for red text + +`32` +: for green text + +`33` +: for yellow text + +`34` +: for blue text + +`35` +: for purple text + +`36` +: for cyan text + +`37` +: for white text + +`38;5;nnn` +: for a colour from 0 to 255 (replace the `nnn` part) + +Many terminals will treat bolded text as a different colour, or at least provide the option to. + +exa provides its own built-in set of file extension mappings that cover a large range of common file extensions, including documents, archives, media, and temporary files. +Any mappings in the environment variables will override this default set: running exa with `LS_COLORS="*.zip=32"` will turn zip files green but leave the colours of other compressed files alone. + +You can also disable this built-in set entirely by including a `reset` entry at the beginning of `EXA_COLORS`. +So setting `EXA_COLORS="reset:*.txt=31"` will highlight only text files; setting `EXA_COLORS="reset"` will highlight nothing. + + +AUTHOR +====== + +exa is maintained by Benjamin ‘ogham’ Sago and many other contributors. + +**Website:** `https://the.exa.website/` \ +**Source code:** `https://github.com/ogham/exa` \ +**Contributors:** `https://github.com/ogham/exa/graphs/contributors` + + +SEE ALSO +======== + +- `exa(1)` diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..f701aa53 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.66.1" diff --git a/src/bin/main.rs b/src/bin/main.rs deleted file mode 100644 index e070d46d..00000000 --- a/src/bin/main.rs +++ /dev/null @@ -1,85 +0,0 @@ -extern crate exa; -use exa::Exa; - -use std::ffi::OsString; -use std::env::{args_os, var_os}; -use std::io::{stdout, stderr, Write, ErrorKind}; -use std::process::exit; - - -fn main() { - configure_logger(); - - let args: Vec = args_os().skip(1).collect(); - match Exa::from_args(args.iter(), &mut stdout()) { - Ok(mut exa) => { - match exa.run() { - Ok(exit_status) => exit(exit_status), - Err(e) => { - match e.kind() { - ErrorKind::BrokenPipe => exit(exits::SUCCESS), - _ => { - eprintln!("{}", e); - exit(exits::RUNTIME_ERROR); - }, - }; - } - }; - }, - - Err(ref e) if e.is_error() => { - let mut stderr = stderr(); - writeln!(stderr, "{}", e).unwrap(); - - if let Some(s) = e.suggestion() { - let _ = writeln!(stderr, "{}", s); - } - - exit(exits::OPTIONS_ERROR); - }, - - Err(ref e) => { - println!("{}", e); - exit(exits::SUCCESS); - }, - }; -} - - -/// Sets up a global logger if one is asked for. -/// The ‘EXA_DEBUG’ environment variable controls whether log messages are -/// displayed or not. Currently there are just two settings (on and off). -/// -/// This can’t be done in exa’s own option parsing because that part of it -/// logs as well, so by the time execution gets there, the logger needs to -/// have already been set up. -pub fn configure_logger() { - extern crate env_logger; - extern crate log; - - let present = match var_os(exa::vars::EXA_DEBUG) { - Some(debug) => debug.len() > 0, - None => false, - }; - - let mut logs = env_logger::Builder::new(); - if present { - logs.filter(None, log::LevelFilter::Debug); - } - else { - logs.filter(None, log::LevelFilter::Off); - } - - logs.init() -} - - -extern crate libc; -#[allow(trivial_numeric_casts)] -mod exits { - use libc::{self, c_int}; - - pub const SUCCESS: c_int = libc::EXIT_SUCCESS; - pub const RUNTIME_ERROR: c_int = libc::EXIT_FAILURE; - pub const OPTIONS_ERROR: c_int = 3 as c_int; -} diff --git a/src/exa.rs b/src/exa.rs deleted file mode 100644 index 53e02a14..00000000 --- a/src/exa.rs +++ /dev/null @@ -1,238 +0,0 @@ -#![warn(trivial_casts, trivial_numeric_casts)] -#![warn(unused_results)] - -use std::env::var_os; -use std::ffi::{OsStr, OsString}; -use std::io::{stderr, Write, Result as IOResult}; -use std::path::{Component, PathBuf}; - -use ansi_term::{ANSIStrings, Style}; - -use log::debug; - -use crate::fs::{Dir, File}; -use crate::fs::feature::ignore::IgnoreCache; -use crate::fs::feature::git::GitCache; -use crate::options::{Options, Vars}; -pub use crate::options::vars; -pub use crate::options::Misfire; -use crate::output::{escape, lines, grid, grid_details, details, View, Mode}; - -mod fs; -mod info; -mod options; -mod output; -mod style; - - -/// The main program wrapper. -pub struct Exa<'args, 'w, W: Write + 'w> { - - /// List of command-line options, having been successfully parsed. - pub options: Options, - - /// The output handle that we write to. When running the program normally, - /// this will be `std::io::Stdout`, but it can accept any struct that’s - /// `Write` so we can write into, say, a vector for testing. - pub writer: &'w mut W, - - /// List of the free command-line arguments that should correspond to file - /// names (anything that isn’t an option). - pub args: Vec<&'args OsStr>, - - /// A global Git cache, if the option was passed in. - /// This has to last the lifetime of the program, because the user might - /// want to list several directories in the same repository. - pub git: Option, - - /// A cache of git-ignored files. - /// This lasts the lifetime of the program too, for the same reason. - pub ignore: Option, -} - -/// The “real” environment variables type. -/// Instead of just calling `var_os` from within the options module, -/// the method of looking up environment variables has to be passed in. -struct LiveVars; -impl Vars for LiveVars { - fn get(&self, name: &'static str) -> Option { - var_os(name) - } -} - -/// Create a Git cache populated with the arguments that are going to be -/// listed before they’re actually listed, if the options demand it. -fn git_options(options: &Options, args: &[&OsStr]) -> Option { - if options.should_scan_for_git() { - Some(args.iter().map(PathBuf::from).collect()) - } - else { - None - } -} - -fn ignore_cache(options: &Options) -> Option { - use crate::fs::filter::GitIgnore; - - match options.filter.git_ignore { - GitIgnore::CheckAndIgnore => Some(IgnoreCache::new()), - GitIgnore::Off => None, - } -} - -impl<'args, 'w, W: Write + 'w> Exa<'args, 'w, W> { - pub fn from_args(args: I, writer: &'w mut W) -> Result, Misfire> - where I: Iterator { - Options::parse(args, &LiveVars).map(move |(options, mut args)| { - debug!("Dir action from arguments: {:#?}", options.dir_action); - debug!("Filter from arguments: {:#?}", options.filter); - debug!("View from arguments: {:#?}", options.view.mode); - - // List the current directory by default, like ls. - // This has to be done here, otherwise git_options won’t see it. - if args.is_empty() { - args = vec![ OsStr::new(".") ]; - } - - let git = git_options(&options, &args); - let ignore = ignore_cache(&options); - Exa { options, writer, args, git, ignore } - }) - } - - pub fn run(&mut self) -> IOResult { - let mut files = Vec::new(); - let mut dirs = Vec::new(); - let mut exit_status = 0; - - for file_path in &self.args { - match File::from_args(PathBuf::from(file_path), None, None) { - Err(e) => { - exit_status = 2; - writeln!(stderr(), "{:?}: {}", file_path, e)?; - }, - Ok(f) => { - if f.points_to_directory() && !self.options.dir_action.treat_dirs_as_files() { - match f.to_dir() { - Ok(d) => dirs.push(d), - Err(e) => writeln!(stderr(), "{:?}: {}", file_path, e)?, - } - } - else { - files.push(f); - } - }, - } - } - - // We want to print a directory’s name before we list it, *except* in - // the case where it’s the only directory, *except* if there are any - // files to print as well. (It’s a double negative) - - let no_files = files.is_empty(); - let is_only_dir = dirs.len() == 1 && no_files; - - self.options.filter.filter_argument_files(&mut files); - self.print_files(None, files)?; - - self.print_dirs(dirs, no_files, is_only_dir, exit_status) - } - - fn print_dirs(&mut self, dir_files: Vec, mut first: bool, is_only_dir: bool, exit_status: i32) -> IOResult { - for dir in dir_files { - - // Put a gap between directories, or between the list of files and - // the first directory. - if first { - first = false; - } - else { - writeln!(self.writer)?; - } - - if !is_only_dir { - let mut bits = Vec::new(); - escape(dir.path.display().to_string(), &mut bits, Style::default(), Style::default()); - writeln!(self.writer, "{}:", ANSIStrings(&bits))?; - } - - let mut children = Vec::new(); - for file in dir.files(self.options.filter.dot_filter, self.ignore.as_ref()) { - match file { - Ok(file) => children.push(file), - Err((path, e)) => writeln!(stderr(), "[{}: {}]", path.display(), e)?, - } - }; - - self.options.filter.filter_child_files(&mut children); - self.options.filter.sort_files(&mut children); - - if let Some(recurse_opts) = self.options.dir_action.recurse_options() { - let depth = dir.path.components().filter(|&c| c != Component::CurDir).count() + 1; - if !recurse_opts.tree && !recurse_opts.is_too_deep(depth) { - - let mut child_dirs = Vec::new(); - for child_dir in children.iter().filter(|f| f.is_directory() && !f.is_all_all) { - match child_dir.to_dir() { - Ok(d) => child_dirs.push(d), - Err(e) => writeln!(stderr(), "{}: {}", child_dir.path.display(), e)?, - } - } - - self.print_files(Some(&dir), children)?; - match self.print_dirs(child_dirs, false, false, exit_status) { - Ok(_) => (), - Err(e) => return Err(e), - } - continue; - } - } - - self.print_files(Some(&dir), children)?; - } - - Ok(exit_status) - } - - /// Prints the list of files using whichever view is selected. - /// For various annoying logistical reasons, each one handles - /// printing differently... - fn print_files(&mut self, dir: Option<&Dir>, files: Vec) -> IOResult<()> { - if !files.is_empty() { - let View { ref mode, ref colours, ref style } = self.options.view; - - match *mode { - Mode::Lines(ref opts) => { - let r = lines::Render { files, colours, style, opts }; - r.render(self.writer) - } - - Mode::Grid(ref opts) => { - let r = grid::Render { files, colours, style, opts }; - r.render(self.writer) - } - - Mode::Details(ref opts) => { - let filter = &self.options.filter; - let recurse = self.options.dir_action.recurse_options(); - - let r = details::Render { dir, files, colours, style, opts, filter, recurse }; - r.render(self.git.as_ref(), self.ignore.as_ref(), self.writer) - } - - Mode::GridDetails(ref opts) => { - let grid = &opts.grid; - let filter = &self.options.filter; - let details = &opts.details; - let row_threshold = opts.row_threshold; - - let r = grid_details::Render { dir, files, colours, style, grid, details, filter, row_threshold }; - r.render(self.git.as_ref(), self.writer) - } - } - } - else { - Ok(()) - } - } -} diff --git a/src/fs/dir.rs b/src/fs/dir.rs index 4d5cfbba..9d4d4f2b 100644 --- a/src/fs/dir.rs +++ b/src/fs/dir.rs @@ -1,15 +1,16 @@ -use std::io::{self, Result as IOResult}; +use crate::fs::feature::git::GitCache; +use crate::fs::fields::GitStatus; +use std::io; use std::fs; use std::path::{Path, PathBuf}; use std::slice::Iter as SliceIter; -use log::info; +use log::*; use crate::fs::File; -use crate::fs::feature::ignore::IgnoreCache; -/// A **Dir** provides a cached list of the file paths in a directory that's +/// A **Dir** provides a cached list of the file paths in a directory that’s /// being listed. /// /// This object gets passed to the Files themselves, in order for them to @@ -34,27 +35,26 @@ impl Dir { /// The `read_dir` iterator doesn’t actually yield the `.` and `..` /// entries, so if the user wants to see them, we’ll have to add them /// ourselves after the files have been read. - pub fn read_dir(path: PathBuf) -> IOResult { + pub fn read_dir(path: PathBuf) -> io::Result { info!("Reading directory {:?}", &path); let contents = fs::read_dir(&path)? - .map(|result| result.map(|entry| entry.path())) - .collect::>()?; + .map(|result| result.map(|entry| entry.path())) + .collect::>()?; - Ok(Dir { contents, path }) + Ok(Self { contents, path }) } /// Produce an iterator of IO results of trying to read all the files in /// this directory. - pub fn files<'dir, 'ig>(&'dir self, dots: DotFilter, ignore: Option<&'ig IgnoreCache>) -> Files<'dir, 'ig> { - if let Some(i) = ignore { i.discover_underneath(&self.path); } - + pub fn files<'dir, 'ig>(&'dir self, dots: DotFilter, git: Option<&'ig GitCache>, git_ignoring: bool) -> Files<'dir, 'ig> { Files { inner: self.contents.iter(), dir: self, dotfiles: dots.shows_dotfiles(), dots: dots.dots(), - ignore, + git, + git_ignoring, } } @@ -86,7 +86,9 @@ pub struct Files<'dir, 'ig> { /// any files have been listed. dots: DotsNext, - ignore: Option<&'ig IgnoreCache>, + git: Option<&'ig GitCache>, + + git_ignoring: bool, } impl<'dir, 'ig> Files<'dir, 'ig> { @@ -105,18 +107,29 @@ impl<'dir, 'ig> Files<'dir, 'ig> { loop { if let Some(path) = self.inner.next() { let filename = File::filename(path); - if !self.dotfiles && filename.starts_with('.') { continue } + if ! self.dotfiles && filename.starts_with('.') { + continue; + } - if let Some(i) = self.ignore { - if i.is_ignored(path) { continue } + // Also hide _prefix files on Windows because it's used by old applications + // as an alternative to dot-prefix files. + #[cfg(windows)] + if ! self.dotfiles && filename.starts_with('_') { + continue; + } + + if self.git_ignoring { + let git_status = self.git.map(|g| g.get(path, false)).unwrap_or_default(); + if git_status.unstaged == GitStatus::Ignored { + continue; + } } return Some(File::from_args(path.clone(), self.dir, filename) .map_err(|e| (path.clone(), e))) } - else { - return None - } + + return None } } } @@ -135,7 +148,6 @@ enum DotsNext { Files, } - impl<'dir, 'ig> Iterator for Files<'dir, 'ig> { type Item = Result, (PathBuf, io::Error)>; @@ -145,24 +157,26 @@ impl<'dir, 'ig> Iterator for Files<'dir, 'ig> { self.dots = DotsNext::DotDot; Some(File::new_aa_current(self.dir) .map_err(|e| (Path::new(".").to_path_buf(), e))) - }, + } + DotsNext::DotDot => { self.dots = DotsNext::Files; Some(File::new_aa_parent(self.parent(), self.dir) .map_err(|e| (self.parent(), e))) - }, + } + DotsNext::Files => { self.next_visible_file() - }, + } } } } /// Usually files in Unix use a leading dot to be hidden or visible, but two -/// entries in particular are "extra-hidden": `.` and `..`, which only become +/// entries in particular are “extra-hidden”: `.` and `..`, which only become /// visible after an extra `-a` option. -#[derive(PartialEq, Debug, Copy, Clone)] +#[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum DotFilter { /// Shows files, dotfiles, and `.` and `..`. @@ -176,8 +190,8 @@ pub enum DotFilter { } impl Default for DotFilter { - fn default() -> DotFilter { - DotFilter::JustFiles + fn default() -> Self { + Self::JustFiles } } @@ -186,18 +200,18 @@ impl DotFilter { /// Whether this filter should show dotfiles in a listing. fn shows_dotfiles(self) -> bool { match self { - DotFilter::JustFiles => false, - DotFilter::Dotfiles => true, - DotFilter::DotfilesAndDots => true, + Self::JustFiles => false, + Self::Dotfiles => true, + Self::DotfilesAndDots => true, } } /// Whether this filter should add dot directories to a listing. fn dots(self) -> DotsNext { match self { - DotFilter::JustFiles => DotsNext::Files, - DotFilter::Dotfiles => DotsNext::Files, - DotFilter::DotfilesAndDots => DotsNext::Dot, + Self::JustFiles => DotsNext::Files, + Self::Dotfiles => DotsNext::Files, + Self::DotfilesAndDots => DotsNext::Dot, } } } diff --git a/src/fs/dir_action.rs b/src/fs/dir_action.rs index ce382acc..6e10403d 100644 --- a/src/fs/dir_action.rs +++ b/src/fs/dir_action.rs @@ -19,7 +19,7 @@ /// into them and print out their contents. The recurse mode does this by /// having extra output blocks at the end, while the tree mode will show /// directories inline, with their contents immediately underneath. -#[derive(PartialEq, Debug, Copy, Clone)] +#[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum DirAction { /// This directory should be listed along with the regular files, instead @@ -39,26 +39,26 @@ pub enum DirAction { impl DirAction { /// Gets the recurse options, if this dir action has any. - pub fn recurse_options(&self) -> Option { - match *self { - DirAction::Recurse(o) => Some(o), - _ => None, + pub fn recurse_options(self) -> Option { + match self { + Self::Recurse(o) => Some(o), + _ => None, } } /// Whether to treat directories as regular files or not. - pub fn treat_dirs_as_files(&self) -> bool { - match *self { - DirAction::AsFile => true, - DirAction::Recurse(o) => o.tree, - _ => false, + pub fn treat_dirs_as_files(self) -> bool { + match self { + Self::AsFile => true, + Self::Recurse(o) => o.tree, + Self::List => false, } } } /// The options that determine how to recurse into a directory. -#[derive(PartialEq, Debug, Copy, Clone)] +#[derive(PartialEq, Eq, Debug, Copy, Clone)] pub struct RecurseOptions { /// Whether recursion should be done as a tree or as multiple individual @@ -73,10 +73,10 @@ pub struct RecurseOptions { impl RecurseOptions { /// Returns whether a directory of the given depth would be too deep. - pub fn is_too_deep(&self, depth: usize) -> bool { + pub fn is_too_deep(self, depth: usize) -> bool { match self.max_depth { - None => false, - Some(d) => d <= depth + None => false, + Some(d) => d <= depth } } } diff --git a/src/fs/feature/git.rs b/src/fs/feature/git.rs index b0646d3b..c7007302 100644 --- a/src/fs/feature/git.rs +++ b/src/fs/feature/git.rs @@ -1,10 +1,12 @@ //! Getting the Git status of files and directories. +use std::ffi::OsStr; +#[cfg(target_family = "unix")] +use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::sync::Mutex; -use git2; -use log::{debug, error, info, warn}; +use log::*; use crate::fs::fields as f; @@ -37,9 +39,11 @@ impl GitCache { use std::iter::FromIterator; impl FromIterator for GitCache { - fn from_iter>(iter: I) -> Self { + fn from_iter(iter: I) -> Self + where I: IntoIterator + { let iter = iter.into_iter(); - let mut git = GitCache { + let mut git = Self { repos: Vec::with_capacity(iter.size_hint().0), misses: Vec::new(), }; @@ -62,8 +66,10 @@ impl FromIterator for GitCache { debug!("Discovered new Git repo"); git.repos.push(r); - }, - Err(miss) => git.misses.push(miss), + } + Err(miss) => { + git.misses.push(miss) + } } } } @@ -73,8 +79,6 @@ impl FromIterator for GitCache { } - - /// A **Git repository** is one we’ve discovered somewhere on the filesystem. pub struct GitRepo { @@ -100,7 +104,9 @@ pub struct GitRepo { enum GitContents { /// All the interesting Git stuff goes through this. - Before { repo: git2::Repository }, + Before { + repo: git2::Repository, + }, /// Temporary value used in `repo_to_statuses` so we can move the /// repository out of the `Before` variant. @@ -108,7 +114,9 @@ enum GitContents { /// The data we’ve extracted from the repository, but only after we’ve /// actually done so. - After { statuses: Git } + After { + statuses: Git, + }, } impl GitRepo { @@ -117,27 +125,26 @@ impl GitRepo { /// depending on the prefix-lookup flag) and returns its Git status. /// /// Actually querying the `git2` repository for the mapping of paths to - /// Git statuses is only done once, and gets cached so we don't need to + /// Git statuses is only done once, and gets cached so we don’t need to /// re-query the entire repository the times after that. /// /// The temporary `Processing` enum variant is used after the `git2` /// repository is moved out, but before the results have been moved in! - /// See https://stackoverflow.com/q/45985827/3484614 + /// See fn search(&self, index: &Path, prefix_lookup: bool) -> f::Git { - use self::GitContents::*; use std::mem::replace; let mut contents = self.contents.lock().unwrap(); - if let After { ref statuses } = *contents { + if let GitContents::After { ref statuses } = *contents { debug!("Git repo {:?} has been found in cache", &self.workdir); return statuses.status(index, prefix_lookup); } debug!("Querying Git repo {:?} for the first time", &self.workdir); - let repo = replace(&mut *contents, Processing).inner_repo(); + let repo = replace(&mut *contents, GitContents::Processing).inner_repo(); let statuses = repo_to_statuses(&repo, &self.workdir); let result = statuses.status(index, prefix_lookup); - let _processing = replace(&mut *contents, After { statuses }); + let _processing = replace(&mut *contents, GitContents::After { statuses }); result } @@ -153,7 +160,7 @@ impl GitRepo { /// Searches for a Git repository at any point above the given path. /// Returns the original buffer if none is found. - fn discover(path: PathBuf) -> Result { + fn discover(path: PathBuf) -> Result { info!("Searching for Git repository above {:?}", path); let repo = match git2::Repository::discover(&path) { Ok(r) => r, @@ -163,15 +170,14 @@ impl GitRepo { } }; - match repo.workdir().map(|wd| wd.to_path_buf()) { - Some(workdir) => { - let contents = Mutex::new(GitContents::Before { repo }); - Ok(GitRepo { contents, workdir, original_path: path, extra_paths: Vec::new() }) - }, - None => { - warn!("Repository has no workdir?"); - Err(path) - } + if let Some(workdir) = repo.workdir() { + let workdir = workdir.to_path_buf(); + let contents = Mutex::new(GitContents::Before { repo }); + Ok(Self { contents, workdir, original_path: path, extra_paths: Vec::new() }) + } + else { + warn!("Repository has no workdir?"); + Err(path) } } } @@ -182,7 +188,7 @@ impl GitContents { /// (consuming the value) if it has. This is needed because the entire /// enum variant gets replaced when a repo is queried (see above). fn inner_repo(self) -> git2::Repository { - if let GitContents::Before { repo } = self { + if let Self::Before { repo } = self { repo } else { @@ -202,12 +208,19 @@ fn repo_to_statuses(repo: &git2::Repository, workdir: &Path) -> Git { match repo.statuses(None) { Ok(es) => { for e in es.iter() { + #[cfg(target_family = "unix")] + let path = workdir.join(Path::new(OsStr::from_bytes(e.path_bytes()))); + // TODO: handle non Unix systems better: + // https://github.com/ogham/exa/issues/698 + #[cfg(not(target_family = "unix"))] let path = workdir.join(Path::new(e.path().unwrap())); let elem = (path, e.status()); statuses.push(elem); } - }, - Err(e) => error!("Error looking up Git statuses: {:?}", e), + } + Err(e) => { + error!("Error looking up Git statuses: {:?}", e); + } } Git { statuses } @@ -218,7 +231,7 @@ fn repo_to_statuses(repo: &git2::Repository, workdir: &Path) -> Git { // 20.311276 INFO:exa::fs::feature::git: Getting Git statuses for repo with workdir "/vagrant/" // 20.799610 DEBUG:exa::output::table: Getting Git status for file "./Cargo.toml" // -// Even inserting another logging line immediately afterwards doesn't make it +// Even inserting another logging line immediately afterwards doesn’t make it // look any faster. @@ -237,42 +250,73 @@ impl Git { else { self.file_status(index) } } - /// Get the status for the file at the given path. + /// Get the user-facing status of a file. + /// We check the statuses directly applying to a file, and for the ignored + /// status we check if any of its parents directories is ignored by git. fn file_status(&self, file: &Path) -> f::Git { let path = reorient(file); - self.statuses.iter() - .find(|p| p.0.as_path() == path) - .map(|&(_, s)| f::Git { staged: index_status(s), unstaged: working_tree_status(s) }) - .unwrap_or_default() + + let s = self.statuses.iter() + .filter(|p| if p.1 == git2::Status::IGNORED { + path.starts_with(&p.0) + } else { + p.0 == path + }) + .fold(git2::Status::empty(), |a, b| a | b.1); + + let staged = index_status(s); + let unstaged = working_tree_status(s); + f::Git { staged, unstaged } } - /// Get the combined status for all the files whose paths begin with the - /// path that gets passed in. This is used for getting the status of - /// directories, which don’t really have an ‘official’ status. + /// Get the combined, user-facing status of a directory. + /// Statuses are aggregating (for example, a directory is considered + /// modified if any file under it has the status modified), except for + /// ignored status which applies to files under (for example, a directory + /// is considered ignored if one of its parent directories is ignored). fn dir_status(&self, dir: &Path) -> f::Git { let path = reorient(dir); - let s = self.statuses.iter() - .filter(|p| p.0.starts_with(&path)) - .fold(git2::Status::empty(), |a, b| a | b.1); - f::Git { staged: index_status(s), unstaged: working_tree_status(s) } + let s = self.statuses.iter() + .filter(|p| if p.1 == git2::Status::IGNORED { + path.starts_with(&p.0) + } else { + p.0.starts_with(&path) + }) + .fold(git2::Status::empty(), |a, b| a | b.1); + + let staged = index_status(s); + let unstaged = working_tree_status(s); + f::Git { staged, unstaged } } } + /// Converts a path to an absolute path based on the current directory. /// Paths need to be absolute for them to be compared properly, otherwise /// you’d ask a repo about “./README.md” but it only knows about -/// “/vagrant/REAMDE.md”, prefixed by the workdir. +/// “/vagrant/README.md”, prefixed by the workdir. +#[cfg(unix)] fn reorient(path: &Path) -> PathBuf { use std::env::current_dir; - // I’m not 100% on this func tbh + + // TODO: I’m not 100% on this func tbh let path = match current_dir() { - Err(_) => Path::new(".").join(&path), - Ok(dir) => dir.join(&path), + Err(_) => Path::new(".").join(&path), + Ok(dir) => dir.join(&path), }; + path.canonicalize().unwrap_or(path) } +#[cfg(windows)] +fn reorient(path: &Path) -> PathBuf { + let unc_path = path.canonicalize().unwrap(); + // On Windows UNC path is returned. We need to strip the prefix for it to work. + let normal_path = unc_path.as_os_str().to_str().unwrap().trim_left_matches("\\\\?\\"); + return PathBuf::from(normal_path); +} + /// The character to display if the file has been modified, but not staged. fn working_tree_status(status: git2::Status) -> f::GitStatus { match status { @@ -282,6 +326,7 @@ fn working_tree_status(status: git2::Status) -> f::GitStatus { s if s.contains(git2::Status::WT_RENAMED) => f::GitStatus::Renamed, s if s.contains(git2::Status::WT_TYPECHANGE) => f::GitStatus::TypeChange, s if s.contains(git2::Status::IGNORED) => f::GitStatus::Ignored, + s if s.contains(git2::Status::CONFLICTED) => f::GitStatus::Conflicted, _ => f::GitStatus::NotModified, } } diff --git a/src/fs/feature/ignore.rs b/src/fs/feature/ignore.rs deleted file mode 100644 index 160940ae..00000000 --- a/src/fs/feature/ignore.rs +++ /dev/null @@ -1,198 +0,0 @@ -//! Ignoring globs in `.gitignore` files. -//! -//! This uses a cache because the file with the globs in might not be the same -//! directory that we’re listing! - -use std::fs::File; -use std::io::Read; -use std::path::{Path, PathBuf}; -use std::sync::RwLock; - -use log::debug; - -use crate::fs::filter::IgnorePatterns; - - -/// An **ignore cache** holds sets of glob patterns paired with the -/// directories that they should be ignored underneath. Believe it or not, -/// that’s a valid English sentence. -#[derive(Default, Debug)] -pub struct IgnoreCache { - entries: RwLock> -} - -impl IgnoreCache { - pub fn new() -> IgnoreCache { - IgnoreCache::default() - } - - pub fn discover_underneath(&self, path: &Path) { - let mut path = Some(path); - let mut entries = self.entries.write().unwrap(); - - while let Some(p) = path { - if p.components().next().is_none() { break } - - let ignore_file = p.join(".gitignore"); - if ignore_file.is_file() { - debug!("Found a .gitignore file: {:?}", ignore_file); - if let Ok(mut file) = File::open(ignore_file) { - let mut contents = String::new(); - - match file.read_to_string(&mut contents) { - Ok(_) => { - let patterns = file_lines_to_patterns(contents.lines()); - entries.push((p.into(), patterns)); - } - Err(e) => debug!("Failed to read a .gitignore: {:?}", e) - } - } - } - else { - debug!("Found no .gitignore file at {:?}", ignore_file); - } - - path = p.parent(); - } - } - - pub fn is_ignored(&self, suspect: &Path) -> bool { - let entries = self.entries.read().unwrap(); - entries.iter().any(|&(ref base_path, ref patterns)| { - if let Ok(suffix) = suspect.strip_prefix(&base_path) { - patterns.is_ignored_path(suffix) - } - else { - false - } - }) - } -} - - -fn file_lines_to_patterns<'a, I>(iter: I) -> IgnorePatterns -where I: Iterator -{ - let iter = iter.filter(|el| !el.is_empty()); - let iter = iter.filter(|el| !el.starts_with('#')); - - // TODO: Figure out if this should trim whitespace or not - - // Errors are currently being ignored... not a good look - IgnorePatterns::parse_from_iter(iter).0 -} - - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn parse_nothing() { - use std::iter::empty; - let (patterns, _) = IgnorePatterns::parse_from_iter(empty()); - assert_eq!(patterns, file_lines_to_patterns(empty())); - } - - #[test] - fn parse_some_globs() { - let stuff = vec![ "*.mp3", "README.md" ]; - let reals = vec![ "*.mp3", "README.md" ]; - let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter()); - assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter())); - } - - #[test] - fn parse_some_comments() { - let stuff = vec![ "*.mp3", "# I am a comment!", "#", "README.md" ]; - let reals = vec![ "*.mp3", "README.md" ]; - let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter()); - assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter())); - } - - #[test] - fn parse_some_blank_lines() { - let stuff = vec![ "*.mp3", "", "", "README.md" ]; - let reals = vec![ "*.mp3", "README.md" ]; - let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter()); - assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter())); - } - - #[test] - fn parse_some_whitespacey_lines() { - let stuff = vec![ " *.mp3", " ", " a ", "README.md " ]; - let reals = vec![ " *.mp3", " ", " a ", "README.md " ]; - let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter()); - assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter())); - } - - - fn test_cache(dir: &'static str, pats: Vec<&str>) -> IgnoreCache { - IgnoreCache { entries: RwLock::new(vec![ (dir.into(), IgnorePatterns::parse_from_iter(pats.into_iter()).0) ]) } - } - - #[test] - fn an_empty_cache_ignores_nothing() { - let ignores = IgnoreCache::default(); - assert_eq!(false, ignores.is_ignored(Path::new("/usr/bin/drinking"))); - assert_eq!(false, ignores.is_ignored(Path::new("target/debug/exa"))); - } - - #[test] - fn a_nonempty_cache_ignores_some_things() { - let ignores = test_cache("/vagrant", vec![ "target" ]); - assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/src"))); - assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/target"))); - } - - #[test] - fn ignore_some_globs() { - let ignores = test_cache("/vagrant", vec![ "*.ipr", "*.iws", ".docker" ]); - assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/exa.ipr"))); - assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/exa.iws"))); - assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/exa.iwiwal"))); - assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/.docker"))); - assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/exa.docker"))); - - assert_eq!(false, ignores.is_ignored(Path::new("/srcode/exa.ipr"))); - assert_eq!(false, ignores.is_ignored(Path::new("/srcode/exa.iws"))); - } - - #[test] #[ignore] - fn ignore_relatively() { - let ignores = test_cache(".", vec![ "target" ]); - assert_eq!(true, ignores.is_ignored(Path::new("./target"))); - assert_eq!(true, ignores.is_ignored(Path::new("./project/target"))); - assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target"))); - assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target"))); - - assert_eq!(false, ignores.is_ignored(Path::new("./.target"))); - } - - #[test] #[ignore] - fn ignore_relatively_sometimes() { - let ignores = test_cache(".", vec![ "project/target" ]); - assert_eq!(false, ignores.is_ignored(Path::new("./target"))); - assert_eq!(true, ignores.is_ignored(Path::new("./project/target"))); - assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target"))); - assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target"))); - } - - #[test] #[ignore] - fn ignore_relatively_absolutely() { - let ignores = test_cache(".", vec![ "/project/target" ]); - assert_eq!(false, ignores.is_ignored(Path::new("./target"))); - assert_eq!(true, ignores.is_ignored(Path::new("./project/target"))); - assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target"))); - assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target"))); - } - - #[test] #[ignore] // not 100% sure if dot works this way... - fn ignore_relatively_absolutely_dot() { - let ignores = test_cache(".", vec![ "./project/target" ]); - assert_eq!(false, ignores.is_ignored(Path::new("./target"))); - assert_eq!(true, ignores.is_ignored(Path::new("./project/target"))); - assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target"))); - assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target"))); - } -} diff --git a/src/fs/feature/mod.rs b/src/fs/feature/mod.rs index 7c230d18..10f49159 100644 --- a/src/fs/feature/mod.rs +++ b/src/fs/feature/mod.rs @@ -1,9 +1,9 @@ pub mod xattr; -pub mod ignore; -#[cfg(feature="git")] pub mod git; +#[cfg(feature = "git")] +pub mod git; -#[cfg(not(feature="git"))] +#[cfg(not(feature = "git"))] pub mod git { use std::iter::FromIterator; use std::path::{Path, PathBuf}; @@ -14,8 +14,10 @@ pub mod git { pub struct GitCache; impl FromIterator for GitCache { - fn from_iter>(_iter: I) -> Self { - GitCache + fn from_iter(_iter: I) -> Self + where I: IntoIterator + { + Self } } @@ -25,7 +27,7 @@ pub mod git { } pub fn get(&self, _index: &Path, _prefix_lookup: bool) -> f::Git { - panic!("Tried to query a Git cache, but Git support is disabled") + unreachable!(); } } } diff --git a/src/fs/feature/xattr.rs b/src/fs/feature/xattr.rs index b42b05e3..192371cf 100644 --- a/src/fs/feature/xattr.rs +++ b/src/fs/feature/xattr.rs @@ -1,11 +1,14 @@ //! Extended attribute support for Darwin and Linux systems. + #![allow(trivial_casts)] // for ARM -extern crate libc; +use std::cmp::Ordering; use std::io; use std::path::Path; -pub const ENABLED: bool = cfg!(feature="git") && cfg!(any(target_os="macos", target_os="linux")); + +pub const ENABLED: bool = cfg!(any(target_os = "macos", target_os = "linux")); + pub trait FileAttributes { fn attributes(&self) -> io::Result>; @@ -26,20 +29,21 @@ impl FileAttributes for Path { #[cfg(not(any(target_os = "macos", target_os = "linux")))] impl FileAttributes for Path { fn attributes(&self) -> io::Result> { - Ok(vec![]) + Ok(Vec::new()) } fn symlink_attributes(&self) -> io::Result> { - Ok(vec![]) + Ok(Vec::new()) } } + /// Attributes which can be passed to `Attribute::list_with_flags` #[cfg(any(target_os = "macos", target_os = "linux"))] #[derive(Copy, Clone)] pub enum FollowSymlinks { Yes, - No + No, } /// Extended attribute @@ -49,74 +53,84 @@ pub struct Attribute { pub size: usize, } + #[cfg(any(target_os = "macos", target_os = "linux"))] pub fn list_attrs(lister: &lister::Lister, path: &Path) -> io::Result> { use std::ffi::CString; - let c_path = match path.to_str().and_then(|s| { CString::new(s).ok() }) { + let c_path = match path.to_str().and_then(|s| CString::new(s).ok()) { Some(cstring) => cstring, - None => return Err(io::Error::new(io::ErrorKind::Other, "Error: path somehow contained a NUL?")), + None => { + return Err(io::Error::new(io::ErrorKind::Other, "Error: path somehow contained a NUL?")); + } }; - let mut names = Vec::new(); let bufsize = lister.listxattr_first(&c_path); - - if bufsize < 0 { - return Err(io::Error::last_os_error()); + match bufsize.cmp(&0) { + Ordering::Less => return Err(io::Error::last_os_error()), + Ordering::Equal => return Ok(Vec::new()), + Ordering::Greater => {}, } - else if bufsize > 0 { - let mut buf = vec![0u8; bufsize as usize]; - let err = lister.listxattr_second(&c_path, &mut buf, bufsize); - if err < 0 { - return Err(io::Error::last_os_error()); - } + let mut buf = vec![0_u8; bufsize as usize]; + let err = lister.listxattr_second(&c_path, &mut buf, bufsize); + + match err.cmp(&0) { + Ordering::Less => return Err(io::Error::last_os_error()), + Ordering::Equal => return Ok(Vec::new()), + Ordering::Greater => {}, + } - if err > 0 { - // End indicies of the attribute names - // the buffer contains 0-terminates c-strings - let idx = buf.iter().enumerate().filter_map(|(i, v)| - if *v == 0 { Some(i) } else { None } - ); - let mut start = 0; - - for end in idx { - let c_end = end + 1; // end of the c-string (including 0) - let size = lister.getxattr(&c_path, &buf[start..c_end]); - - if size > 0 { - names.push(Attribute { - name: lister.translate_attribute_name(&buf[start..end]), - size: size as usize - }); - } - - start = c_end; + let mut names = Vec::new(); + if err > 0 { + // End indices of the attribute names + // the buffer contains 0-terminated c-strings + let idx = buf.iter().enumerate().filter_map(|(i, v)| + if *v == 0 { Some(i) } else { None } + ); + let mut start = 0; + + for end in idx { + let c_end = end + 1; // end of the c-string (including 0) + let size = lister.getxattr(&c_path, &buf[start..c_end]); + + if size > 0 { + names.push(Attribute { + name: lister.translate_attribute_name(&buf[start..end]), + size: size as usize, + }); } + start = c_end; } - } + Ok(names) } + #[cfg(target_os = "macos")] mod lister { - use std::ffi::CString; - use libc::{c_int, size_t, ssize_t, c_char, c_void, uint32_t}; use super::FollowSymlinks; + use libc::{c_int, size_t, ssize_t, c_char, c_void}; + use std::ffi::CString; use std::ptr; extern "C" { fn listxattr( - path: *const c_char, namebuf: *mut c_char, - size: size_t, options: c_int + path: *const c_char, + namebuf: *mut c_char, + size: size_t, + options: c_int, ) -> ssize_t; fn getxattr( - path: *const c_char, name: *const c_char, - value: *mut c_void, size: size_t, position: uint32_t, - options: c_int + path: *const c_char, + name: *const c_char, + value: *mut c_void, + size: size_t, + position: u32, + options: c_int, ) -> ssize_t; } @@ -125,26 +139,27 @@ mod lister { } impl Lister { - pub fn new(do_follow: FollowSymlinks) -> Lister { + pub fn new(do_follow: FollowSymlinks) -> Self { let c_flags: c_int = match do_follow { - FollowSymlinks::Yes => 0x0001, - FollowSymlinks::No => 0x0000, + FollowSymlinks::Yes => 0x0001, + FollowSymlinks::No => 0x0000, }; - Lister { c_flags } + Self { c_flags } } pub fn translate_attribute_name(&self, input: &[u8]) -> String { - use std::str::from_utf8_unchecked; - - unsafe { - from_utf8_unchecked(input).into() - } + unsafe { std::str::from_utf8_unchecked(input).into() } } pub fn listxattr_first(&self, c_path: &CString) -> ssize_t { unsafe { - listxattr(c_path.as_ptr(), ptr::null_mut(), 0, self.c_flags) + listxattr( + c_path.as_ptr(), + ptr::null_mut(), + 0, + self.c_flags, + ) } } @@ -152,8 +167,9 @@ mod lister { unsafe { listxattr( c_path.as_ptr(), - buf.as_mut_ptr() as *mut c_char, - bufsize as size_t, self.c_flags + buf.as_mut_ptr().cast::(), + bufsize as size_t, + self.c_flags, ) } } @@ -162,14 +178,18 @@ mod lister { unsafe { getxattr( c_path.as_ptr(), - buf.as_ptr() as *const c_char, - ptr::null_mut(), 0, 0, self.c_flags + buf.as_ptr().cast::(), + ptr::null_mut(), + 0, + 0, + self.c_flags, ) } } } } + #[cfg(target_os = "linux")] mod lister { use std::ffi::CString; @@ -179,21 +199,29 @@ mod lister { extern "C" { fn listxattr( - path: *const c_char, list: *mut c_char, size: size_t + path: *const c_char, + list: *mut c_char, + size: size_t, ) -> ssize_t; fn llistxattr( - path: *const c_char, list: *mut c_char, size: size_t + path: *const c_char, + list: *mut c_char, + size: size_t, ) -> ssize_t; fn getxattr( - path: *const c_char, name: *const c_char, - value: *mut c_void, size: size_t + path: *const c_char, + name: *const c_char, + value: *mut c_void, + size: size_t, ) -> ssize_t; fn lgetxattr( - path: *const c_char, name: *const c_char, - value: *mut c_void, size: size_t + path: *const c_char, + name: *const c_char, + value: *mut c_void, + size: size_t, ) -> ssize_t; } @@ -212,41 +240,46 @@ mod lister { pub fn listxattr_first(&self, c_path: &CString) -> ssize_t { let listxattr = match self.follow_symlinks { - FollowSymlinks::Yes => listxattr, - FollowSymlinks::No => llistxattr, + FollowSymlinks::Yes => listxattr, + FollowSymlinks::No => llistxattr, }; unsafe { - listxattr(c_path.as_ptr() as *const _, ptr::null_mut(), 0) + listxattr( + c_path.as_ptr().cast(), + ptr::null_mut(), + 0, + ) } } pub fn listxattr_second(&self, c_path: &CString, buf: &mut Vec, bufsize: ssize_t) -> ssize_t { let listxattr = match self.follow_symlinks { - FollowSymlinks::Yes => listxattr, - FollowSymlinks::No => llistxattr, + FollowSymlinks::Yes => listxattr, + FollowSymlinks::No => llistxattr, }; unsafe { listxattr( - c_path.as_ptr() as *const _, - buf.as_mut_ptr() as *mut c_char, - bufsize as size_t + c_path.as_ptr().cast(), + buf.as_mut_ptr().cast(), + bufsize as size_t, ) } } pub fn getxattr(&self, c_path: &CString, buf: &[u8]) -> ssize_t { let getxattr = match self.follow_symlinks { - FollowSymlinks::Yes => getxattr, - FollowSymlinks::No => lgetxattr, + FollowSymlinks::Yes => getxattr, + FollowSymlinks::No => lgetxattr, }; unsafe { getxattr( - c_path.as_ptr() as *const _, - buf.as_ptr() as *const c_char, - ptr::null_mut(), 0 + c_path.as_ptr().cast(), + buf.as_ptr().cast(), + ptr::null_mut(), + 0, ) } } diff --git a/src/fs/fields.rs b/src/fs/fields.rs index 87cebfc9..9369ecb4 100644 --- a/src/fs/fields.rs +++ b/src/fs/fields.rs @@ -1,4 +1,3 @@ - //! Wrapper types for the values returned from `File`s. //! //! The methods of `File` that return information about the entry on the @@ -14,6 +13,7 @@ // C-style `blkcnt_t` types don’t follow Rust’s rules! #![allow(non_camel_case_types)] +#![allow(clippy::struct_excessive_bools)] /// The type of a file’s block count. @@ -43,22 +43,27 @@ pub type uid_t = u32; /// regular file. (See the `filetype` module for those checks.) /// /// Its ordering is used when sorting by type. -#[derive(PartialEq, Eq, PartialOrd, Ord)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] pub enum Type { - Directory, File, Link, Pipe, Socket, CharDevice, BlockDevice, Special, + Directory, + File, + Link, + Pipe, + Socket, + CharDevice, + BlockDevice, + Special, } impl Type { - pub fn is_regular_file(&self) -> bool { - match *self { - Type::File => true, - _ => false, - } + pub fn is_regular_file(self) -> bool { + matches!(self, Self::File) } } /// The file’s Unix permission bitfield, with one entry per bit. +#[derive(Copy, Clone)] pub struct Permissions { pub user_read: bool, pub user_write: bool, @@ -77,22 +82,44 @@ pub struct Permissions { pub setuid: bool, } +/// The file's FileAttributes field, available only on Windows. +#[derive(Copy, Clone)] +pub struct Attributes { + pub archive: bool, + pub directory: bool, + pub readonly: bool, + pub hidden: bool, + pub system: bool, + pub reparse_point: bool, +} + /// The three pieces of information that are displayed as a single column in /// the details view. These values are fused together to make the output a /// little more compressed. +#[derive(Copy, Clone)] pub struct PermissionsPlus { pub file_type: Type, + #[cfg(unix)] pub permissions: Permissions, + #[cfg(windows)] + pub attributes: Attributes, pub xattrs: bool, } +/// The permissions encoded as octal values +#[derive(Copy, Clone)] +pub struct OctalPermissions { + pub permissions: Permissions, +} + /// A file’s number of hard links on the filesystem. /// /// Under Unix, a file can exist on the filesystem only once but appear in /// multiple directories. However, it’s rare (but occasionally useful!) for a /// regular file to have a link count greater than 1, so we highlight the /// block count specifically for this case. +#[derive(Copy, Clone)] pub struct Links { /// The actual link count. @@ -106,10 +133,12 @@ pub struct Links { /// A file’s inode. Every directory entry on a Unix filesystem has an inode, /// including directories and links, so this is applicable to everything exa /// can deal with. +#[derive(Copy, Clone)] pub struct Inode(pub ino_t); /// The number of blocks that a file takes up on the filesystem, if any. +#[derive(Copy, Clone)] pub enum Blocks { /// This file has the given number of blocks. @@ -122,14 +151,17 @@ pub enum Blocks { /// The ID of the user that owns a file. This will only ever be a number; /// looking up the username is done in the `display` module. +#[derive(Copy, Clone)] pub struct User(pub uid_t); /// The ID of the group that a file belongs to. +#[derive(Copy, Clone)] pub struct Group(pub gid_t); /// A file’s size, in bytes. This is usually formatted by the `number_prefix` /// crate into something human-readable. +#[derive(Copy, Clone)] pub enum Size { /// This file has a defined size. @@ -141,10 +173,10 @@ pub enum Size { /// have a file size. For example, a directory will just contain a list of /// its files as its “contents” and will be specially flagged as being a /// directory, rather than a file. However, seeing the “file size” of this - /// data is rarely useful -- I can’t think of a time when I’ve seen it and + /// data is rarely useful — I can’t think of a time when I’ve seen it and /// learnt something. So we discard it and just output “-” instead. /// - /// See this answer for more: http://unix.stackexchange.com/a/68266 + /// See this answer for more: None, /// This file is a block or character device, so instead of a size, print @@ -158,8 +190,9 @@ pub enum Size { /// The major and minor device IDs that gets displayed for device files. /// /// You can see what these device numbers mean: -/// - http://www.lanana.org/docs/device-list/ -/// - http://www.lanana.org/docs/device-list/devices-2.6+.txt +/// - +/// - +#[derive(Copy, Clone)] pub struct DeviceIDs { pub major: u8, pub minor: u8, @@ -177,6 +210,7 @@ pub struct Time { /// A file’s status in a Git repository. Whether a file is in a repository or /// not is handled by the Git module, rather than having a “null” variant in /// this enum. +#[derive(PartialEq, Eq, Copy, Clone)] pub enum GitStatus { /// This file hasn’t changed since the last commit. @@ -200,21 +234,28 @@ pub enum GitStatus { /// A file that’s ignored (that matches a line in .gitignore) Ignored, + + /// A file that’s updated but unmerged. + Conflicted, } + /// A file’s complete Git status. It’s possible to make changes to a file, add /// it to the staging area, then make *more* changes, so we need to list each /// file’s status for both of these. +#[derive(Copy, Clone)] pub struct Git { pub staged: GitStatus, pub unstaged: GitStatus, } -use std::default::Default; impl Default for Git { /// Create a Git status for a file with nothing done to it. - fn default() -> Git { - Git { staged: GitStatus::NotModified, unstaged: GitStatus::NotModified } + fn default() -> Self { + Self { + staged: GitStatus::NotModified, + unstaged: GitStatus::NotModified, + } } } diff --git a/src/fs/file.rs b/src/fs/file.rs index 0d42d548..bccc6550 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -1,17 +1,20 @@ //! Files, and methods and fields to access their metadata. -use std::io::Error as IOError; -use std::io::Result as IOResult; -use std::os::unix::fs::{MetadataExt, PermissionsExt, FileTypeExt}; +use std::io; +#[cfg(unix)] +use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; +#[cfg(windows)] +use std::os::windows::fs::MetadataExt; use std::path::{Path, PathBuf}; -use std::time::{UNIX_EPOCH, Duration}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use log::{debug, error}; +use log::*; use crate::fs::dir::Dir; use crate::fs::fields as f; -/// A **File** is a wrapper around one of Rust's Path objects, along with + +/// A **File** is a wrapper around one of Rust’s `PathBuf` values, along with /// associated data about the file. /// /// Each file is definitely going to have its filename displayed at least @@ -44,7 +47,7 @@ pub struct File<'dir> { /// /// This too is queried multiple times, and is *not* cached by the OS, as /// it could easily change between invocations — but exa is so short-lived - /// it's better to just cache it. + /// it’s better to just cache it. pub metadata: std::fs::Metadata, /// A reference to the directory that contains this file, if any. @@ -60,13 +63,13 @@ pub struct File<'dir> { /// Whether this is one of the two `--all all` directories, `.` and `..`. /// /// Unlike all other entries, these are not returned as part of the - /// directory's children, and are in fact added specifically by exa; this + /// directory’s children, and are in fact added specifically by exa; this /// means that they should be skipped when recursing. pub is_all_all: bool, } impl<'dir> File<'dir> { - pub fn from_args(path: PathBuf, parent_dir: PD, filename: FN) -> IOResult> + pub fn from_args(path: PathBuf, parent_dir: PD, filename: FN) -> io::Result> where PD: Into>, FN: Into> { @@ -78,28 +81,30 @@ impl<'dir> File<'dir> { let metadata = std::fs::symlink_metadata(&path)?; let is_all_all = false; - Ok(File { path, parent_dir, metadata, ext, name, is_all_all }) + Ok(File { name, ext, path, metadata, parent_dir, is_all_all }) } - pub fn new_aa_current(parent_dir: &'dir Dir) -> IOResult> { - let path = parent_dir.path.to_path_buf(); + pub fn new_aa_current(parent_dir: &'dir Dir) -> io::Result> { + let path = parent_dir.path.clone(); let ext = File::ext(&path); debug!("Statting file {:?}", &path); let metadata = std::fs::symlink_metadata(&path)?; let is_all_all = true; + let parent_dir = Some(parent_dir); - Ok(File { path, parent_dir: Some(parent_dir), metadata, ext, name: ".".to_string(), is_all_all }) + Ok(File { path, parent_dir, metadata, ext, name: ".".into(), is_all_all }) } - pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> IOResult> { + pub fn new_aa_parent(path: PathBuf, parent_dir: &'dir Dir) -> io::Result> { let ext = File::ext(&path); debug!("Statting file {:?}", &path); let metadata = std::fs::symlink_metadata(&path)?; let is_all_all = true; + let parent_dir = Some(parent_dir); - Ok(File { path, parent_dir: Some(parent_dir), metadata, ext, name: "..".to_string(), is_all_all }) + Ok(File { path, parent_dir, metadata, ext, name: "..".into(), is_all_all }) } /// A file’s name is derived from its string. This needs to handle directories @@ -127,7 +132,9 @@ impl<'dir> File<'dir> { fn ext(path: &Path) -> Option { let name = path.file_name().map(|f| f.to_string_lossy().to_string())?; - name.rfind('.').map(|p| name[p+1..].to_ascii_lowercase()) + name.rfind('.') + .map(|p| name[p + 1 ..] + .to_ascii_lowercase()) } /// Whether this file is a directory on the filesystem. @@ -157,7 +164,7 @@ impl<'dir> File<'dir> { /// /// Returns an IO error upon failure, but this shouldn’t be used to check /// if a `File` is a directory or not! For that, just use `is_directory()`. - pub fn to_dir(&self) -> IOResult { + pub fn to_dir(&self) -> io::Result { Dir::read_dir(self.path.clone()) } @@ -170,6 +177,7 @@ impl<'dir> File<'dir> { /// Whether this file is both a regular file *and* executable for the /// current user. An executable file has a different purpose from an /// executable directory, so they should be highlighted differently. + #[cfg(unix)] pub fn is_executable_file(&self) -> bool { let bit = modes::USER_EXECUTE; self.is_file() && (self.metadata.permissions().mode() & bit) == bit @@ -181,21 +189,25 @@ impl<'dir> File<'dir> { } /// Whether this file is a named pipe on the filesystem. + #[cfg(unix)] pub fn is_pipe(&self) -> bool { self.metadata.file_type().is_fifo() } /// Whether this file is a char device on the filesystem. + #[cfg(unix)] pub fn is_char_device(&self) -> bool { self.metadata.file_type().is_char_device() } /// Whether this file is a block device on the filesystem. + #[cfg(unix)] pub fn is_block_device(&self) -> bool { self.metadata.file_type().is_block_device() } /// Whether this file is a socket on the filesystem. + #[cfg(unix)] pub fn is_socket(&self) -> bool { self.metadata.file_type().is_socket() } @@ -209,13 +221,13 @@ impl<'dir> File<'dir> { path.to_path_buf() } else if let Some(dir) = self.parent_dir { - dir.join(&*path) + dir.join(path) } else if let Some(parent) = self.path.parent() { - parent.join(&*path) + parent.join(path) } else { - self.path.join(&*path) + self.path.join(path) } } @@ -249,7 +261,8 @@ impl<'dir> File<'dir> { Ok(metadata) => { let ext = File::ext(&path); let name = File::filename(&path); - FileTarget::Ok(Box::new(File { parent_dir: None, path, ext, metadata, name, is_all_all: false })) + let file = File { parent_dir: None, path, ext, metadata, name, is_all_all: false }; + FileTarget::Ok(Box::new(file)) } Err(e) => { error!("Error following link {:?}: {:#?}", &path, e); @@ -265,6 +278,7 @@ impl<'dir> File<'dir> { /// is uncommon, while you come across directories and other types /// with multiple links much more often. Thus, it should get highlighted /// more attentively. + #[cfg(unix)] pub fn links(&self) -> f::Links { let count = self.metadata.nlink(); @@ -274,14 +288,16 @@ impl<'dir> File<'dir> { } } - /// This file's inode. + /// This file’s inode. + #[cfg(unix)] pub fn inode(&self) -> f::Inode { f::Inode(self.metadata.ino()) } - /// This file's number of filesystem blocks. + /// This file’s number of filesystem blocks. /// - /// (Not the size of each block, which we don't actually report on) + /// (Not the size of each block, which we don’t actually report on) + #[cfg(unix)] pub fn blocks(&self) -> f::Blocks { if self.is_file() || self.is_link() { f::Blocks::Some(self.metadata.blocks()) @@ -292,11 +308,13 @@ impl<'dir> File<'dir> { } /// The ID of the user that own this file. + #[cfg(unix)] pub fn user(&self) -> f::User { f::User(self.metadata.uid()) } /// The ID of the group that owns this file. + #[cfg(unix)] pub fn group(&self) -> f::Group { f::Group(self.metadata.gid()) } @@ -309,15 +327,21 @@ impl<'dir> File<'dir> { /// /// Block and character devices return their device IDs, because they /// usually just have a file size of zero. + #[cfg(unix)] pub fn size(&self) -> f::Size { if self.is_directory() { f::Size::None } else if self.is_char_device() || self.is_block_device() { - let dev = self.metadata.rdev(); + let device_ids = self.metadata.rdev().to_be_bytes(); + + // In C-land, getting the major and minor device IDs is done with + // preprocessor macros called `major` and `minor` that depend on + // the size of `dev_t`, but we just take the second-to-last and + // last bytes. f::Size::DeviceIDs(f::DeviceIDs { - major: (dev / 256) as u8, - minor: (dev % 256) as u8, + major: device_ids[6], + minor: device_ids[7], }) } else { @@ -325,36 +349,54 @@ impl<'dir> File<'dir> { } } - /// This file’s last modified timestamp. - /// If the file's time is invalid, assume it was modified today - pub fn modified_time(&self) -> Duration { - match self.metadata.modified() { - Ok(system_time) => system_time.duration_since(UNIX_EPOCH).unwrap(), - Err(_) => Duration::new(0, 0), + #[cfg(windows)] + pub fn size(&self) -> f::Size { + if self.is_directory() { + f::Size::None + } + else { + f::Size::Some(self.metadata.len()) } } - /// This file’s last changed timestamp. - pub fn changed_time(&self) -> Duration { - Duration::new(self.metadata.ctime() as u64, self.metadata.ctime_nsec() as u32) + /// This file’s last modified timestamp, if available on this platform. + pub fn modified_time(&self) -> Option { + self.metadata.modified().ok() } - /// This file’s last accessed timestamp. - /// If the file's time is invalid, assume it was accessed today - pub fn accessed_time(&self) -> Duration { - match self.metadata.accessed() { - Ok(system_time) => system_time.duration_since(UNIX_EPOCH).unwrap(), - Err(_) => Duration::new(0, 0), + /// This file’s last changed timestamp, if available on this platform. + #[cfg(unix)] + pub fn changed_time(&self) -> Option { + let (mut sec, mut nanosec) = (self.metadata.ctime(), self.metadata.ctime_nsec()); + + if sec < 0 { + if nanosec > 0 { + sec += 1; + nanosec -= 1_000_000_000; + } + + let duration = Duration::new(sec.unsigned_abs(), nanosec.unsigned_abs() as u32); + Some(UNIX_EPOCH - duration) + } + else { + let duration = Duration::new(sec as u64, nanosec as u32); + Some(UNIX_EPOCH + duration) } } - /// This file’s created timestamp. - /// If the file's time is invalid, assume it was created today - pub fn created_time(&self) -> Duration { - match self.metadata.created() { - Ok(system_time) => system_time.duration_since(UNIX_EPOCH).unwrap(), - Err(_) => Duration::new(0, 0), - } + #[cfg(windows)] + pub fn changed_time(&self) -> Option { + return self.modified_time() + } + + /// This file’s last accessed timestamp, if available on this platform. + pub fn accessed_time(&self) -> Option { + self.metadata.accessed().ok() + } + + /// This file’s created timestamp, if available on this platform. + pub fn created_time(&self) -> Option { + self.metadata.created().ok() } /// This file’s ‘type’. @@ -362,6 +404,7 @@ impl<'dir> File<'dir> { /// This is used a the leftmost character of the permissions column. /// The file type can usually be guessed from the colour of the file, but /// ls puts this character there. + #[cfg(unix)] pub fn type_char(&self) -> f::Type { if self.is_file() { f::Type::File @@ -389,10 +432,24 @@ impl<'dir> File<'dir> { } } + #[cfg(windows)] + pub fn type_char(&self) -> f::Type { + if self.is_file() { + f::Type::File + } + else if self.is_directory() { + f::Type::Directory + } + else { + f::Type::Special + } + } + /// This file’s permissions, with flags for each bit. + #[cfg(unix)] pub fn permissions(&self) -> f::Permissions { let bits = self.metadata.mode(); - let has_bit = |bit| { bits & bit == bit }; + let has_bit = |bit| bits & bit == bit; f::Permissions { user_read: has_bit(modes::USER_READ), @@ -413,17 +470,33 @@ impl<'dir> File<'dir> { } } + #[cfg(windows)] + pub fn attributes(&self) -> f::Attributes { + let bits = self.metadata.file_attributes(); + let has_bit = |bit| bits & bit == bit; + + // https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants + f::Attributes { + directory: has_bit(0x10), + archive: has_bit(0x20), + readonly: has_bit(0x1), + hidden: has_bit(0x2), + system: has_bit(0x4), + reparse_point: has_bit(0x400), + } + } + /// Whether this file’s extension is any of the strings that get passed in. /// /// This will always return `false` if the file has no extension. pub fn extension_is_one_of(&self, choices: &[&str]) -> bool { - match self.ext { - Some(ref ext) => choices.contains(&&ext[..]), - None => false, + match &self.ext { + Some(ext) => choices.contains(&&ext[..]), + None => false, } } - /// Whether this file's name, including extension, is any of the strings + /// Whether this file’s name, including extension, is any of the strings /// that get passed in. pub fn name_is_one_of(&self, choices: &[&str]) -> bool { choices.contains(&&self.name[..]) @@ -451,11 +524,11 @@ pub enum FileTarget<'dir> { /// There was an IO error when following the link. This can happen if the /// file isn’t a link to begin with, but also if, say, we don’t have /// permission to follow it. - Err(IOError), + Err(io::Error), // Err is its own variant, instead of having the whole thing be inside an - // `IOResult`, because being unable to follow a symlink is not a serious - // error -- we just display the error message and move on. + // `io::Result`, because being unable to follow a symlink is not a serious + // error — we just display the error message and move on. } impl<'dir> FileTarget<'dir> { @@ -463,22 +536,19 @@ impl<'dir> FileTarget<'dir> { /// Whether this link doesn’t lead to a file, for whatever reason. This /// gets used to determine how to highlight the link in grid views. pub fn is_broken(&self) -> bool { - match *self { - FileTarget::Ok(_) => false, - FileTarget::Broken(_) | FileTarget::Err(_) => true, - } + matches!(self, Self::Broken(_) | Self::Err(_)) } } /// More readable aliases for the permission bits exposed by libc. #[allow(trivial_numeric_casts)] +#[cfg(unix)] mod modes { - use libc; - pub type Mode = u32; // The `libc::mode_t` type’s actual type varies, but the value returned // from `metadata.permissions().mode()` is always `u32`. + pub type Mode = u32; pub const USER_READ: Mode = libc::S_IRUSR as Mode; pub const USER_WRITE: Mode = libc::S_IWUSR as Mode; @@ -551,6 +621,7 @@ mod filename_test { } #[test] + #[cfg(unix)] fn topmost() { assert_eq!("/", File::filename(Path::new("/"))) } diff --git a/src/fs/filter.rs b/src/fs/filter.rs index 44944c33..4cd08b02 100644 --- a/src/fs/filter.rs +++ b/src/fs/filter.rs @@ -2,14 +2,11 @@ use std::cmp::Ordering; use std::iter::FromIterator; +#[cfg(unix)] use std::os::unix::fs::MetadataExt; -use std::path::Path; -use glob; -use natord; - -use crate::fs::File; use crate::fs::DotFilter; +use crate::fs::File; /// The **file filter** processes a list of files before displaying them to @@ -26,7 +23,7 @@ use crate::fs::DotFilter; /// The filter also governs sorting the list. After being filtered, pairs of /// files are compared and sorted based on the result, with the sort field /// performing the comparison. -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone)] pub struct FileFilter { /// Whether directories should be listed first, and other types of file @@ -53,31 +50,8 @@ pub struct FileFilter { /// /// This came about more or less by a complete historical accident, /// when the original `ls` tried to hide `.` and `..`: - /// https://plus.google.com/+RobPikeTheHuman/posts/R58WgWwN9jp - /// - /// When one typed ls, however, these files appeared, so either Ken or - /// Dennis added a simple test to the program. It was in assembler then, - /// but the code in question was equivalent to something like this: - /// if (name[0] == '.') continue; - /// This statement was a little shorter than what it should have been, - /// which is: - /// if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue; - /// but hey, it was easy. /// - /// Two things resulted. - /// - /// First, a bad precedent was set. A lot of other lazy programmers - /// introduced bugs by making the same simplification. Actual files - /// beginning with periods are often skipped when they should be counted. - /// - /// Second, and much worse, the idea of a "hidden" or "dot" file was - /// created. As a consequence, more lazy programmers started dropping - /// files into everyone's home directory. I don't have all that much - /// stuff installed on the machine I'm using to type this, but my home - /// directory has about a hundred dot files and I don't even know what - /// most of them are or whether they're still needed. Every file name - /// evaluation that goes through my home directory is slowed down by - /// this accumulated sludge. + /// [Linux History: How Dot Files Became Hidden Files](https://linux-audit.com/linux-history-how-dot-files-became-hidden-files/) pub dot_filter: DotFilter, /// Glob patterns to ignore. Any file name that matches *any* of these @@ -85,21 +59,17 @@ pub struct FileFilter { pub ignore_patterns: IgnorePatterns, /// Whether to ignore Git-ignored patterns. - /// This is implemented completely separately from the actual Git - /// repository scanning — a `.gitignore` file will still be scanned even - /// if there’s no `.git` folder present. pub git_ignore: GitIgnore, } - impl FileFilter { /// Remove every file in the given vector that does *not* pass the /// filter predicate for files found inside a directory. - pub fn filter_child_files(&self, files: &mut Vec) { - files.retain(|f| !self.ignore_patterns.is_ignored(&f.name)); + pub fn filter_child_files(&self, files: &mut Vec>) { + files.retain(|f| ! self.ignore_patterns.is_ignored(&f.name)); if self.only_dirs { - files.retain(|f| f.is_directory()); + files.retain(File::is_directory); } } @@ -112,15 +82,19 @@ impl FileFilter { /// dotfile, because it’s been directly specified. But running /// `exa -I='*.ogg' music/*` should filter out the ogg files obtained /// from the glob, even though the globbing is done by the shell! - pub fn filter_argument_files(&self, files: &mut Vec) { - files.retain(|f| !self.ignore_patterns.is_ignored(&f.name)); + pub fn filter_argument_files(&self, files: &mut Vec>) { + files.retain(|f| { + ! self.ignore_patterns.is_ignored(&f.name) + }); } /// Sort the files in the given vector based on the sort field option. - pub fn sort_files<'a, F>(&self, files: &mut Vec) - where F: AsRef> { - - files.sort_by(|a, b| self.sort_field.compare_files(a.as_ref(), b.as_ref())); + pub fn sort_files<'a, F>(&self, files: &mut [F]) + where F: AsRef> + { + files.sort_by(|a, b| { + self.sort_field.compare_files(a.as_ref(), b.as_ref()) + }); if self.reverse { files.reverse(); @@ -129,8 +103,9 @@ impl FileFilter { if self.list_dirs_first { // This relies on the fact that `sort_by` is *stable*: it will keep // adjacent elements next to each other. - files.sort_by(|a, b| {b.as_ref().points_to_directory() - .cmp(&a.as_ref().points_to_directory()) + files.sort_by(|a, b| { + b.as_ref().points_to_directory() + .cmp(&a.as_ref().points_to_directory()) }); } } @@ -138,7 +113,7 @@ impl FileFilter { /// User-supplied field to sort by. -#[derive(PartialEq, Debug, Copy, Clone)] +#[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum SortField { /// Don’t apply any sorting. This is usually used as an optimisation in @@ -156,6 +131,7 @@ pub enum SortField { /// The file’s inode, which usually corresponds to the order in which /// files were created on the filesystem, more or less. + #[cfg(unix)] FileInode, /// The time the file was modified (the “mtime”). @@ -172,19 +148,19 @@ pub enum SortField { /// slows the whole operation down, so many systems will only update the /// timestamp in certain circumstances. This has become common enough that /// it’s now expected behaviour! - /// http://unix.stackexchange.com/a/8842 + /// AccessedDate, /// The time the file was changed (the “ctime”). /// /// This field is used to mark the time when a file’s metadata - /// changed -- its permissions, owners, or link count. + /// changed — its permissions, owners, or link count. /// /// In original Unix, this was, however, meant as creation time. - /// https://www.bell-labs.com/usr/dmr/www/cacm.html + /// ChangedDate, - /// The time the file was created (the "btime" or "birthtime"). + /// The time the file was created (the “btime” or “birthtime”). CreatedDate, /// The type of the file: directories, links, pipes, regular, files, etc. @@ -218,7 +194,7 @@ pub enum SortField { /// lowercase letters because it takes the difference between the two cases /// into account? I gave up and just named these two variants after the /// effects they have. -#[derive(PartialEq, Debug, Copy, Clone)] +#[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum SortCase { /// Sort files case-sensitively with uppercase first, with ‘A’ coming @@ -239,54 +215,54 @@ impl SortField { /// into groups between letters and numbers, and then sorts those blocks /// together, so `file10` will sort after `file9`, instead of before it /// because of the `1`. - pub fn compare_files(self, a: &File, b: &File) -> Ordering { + pub fn compare_files(self, a: &File<'_>, b: &File<'_>) -> Ordering { use self::SortCase::{ABCabc, AaBbCc}; match self { - SortField::Unsorted => Ordering::Equal, + Self::Unsorted => Ordering::Equal, - SortField::Name(ABCabc) => natord::compare(&a.name, &b.name), - SortField::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name), + Self::Name(ABCabc) => natord::compare(&a.name, &b.name), + Self::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name), - SortField::Size => a.metadata.len().cmp(&b.metadata.len()), - SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()), - SortField::ModifiedDate => a.modified_time().cmp(&b.modified_time()), - SortField::AccessedDate => a.accessed_time().cmp(&b.accessed_time()), - SortField::ChangedDate => a.changed_time().cmp(&b.changed_time()), - SortField::CreatedDate => a.created_time().cmp(&b.created_time()), - SortField::ModifiedAge => b.modified_time().cmp(&a.modified_time()), // flip b and a + Self::Size => a.metadata.len().cmp(&b.metadata.len()), + #[cfg(unix)] + Self::FileInode => a.metadata.ino().cmp(&b.metadata.ino()), + Self::ModifiedDate => a.modified_time().cmp(&b.modified_time()), + Self::AccessedDate => a.accessed_time().cmp(&b.accessed_time()), + Self::ChangedDate => a.changed_time().cmp(&b.changed_time()), + Self::CreatedDate => a.created_time().cmp(&b.created_time()), + Self::ModifiedAge => b.modified_time().cmp(&a.modified_time()), // flip b and a - SortField::FileType => match a.type_char().cmp(&b.type_char()) { // todo: this recomputes + Self::FileType => match a.type_char().cmp(&b.type_char()) { // todo: this recomputes Ordering::Equal => natord::compare(&*a.name, &*b.name), order => order, }, - SortField::Extension(ABCabc) => match a.ext.cmp(&b.ext) { + Self::Extension(ABCabc) => match a.ext.cmp(&b.ext) { Ordering::Equal => natord::compare(&*a.name, &*b.name), order => order, }, - SortField::Extension(AaBbCc) => match a.ext.cmp(&b.ext) { + Self::Extension(AaBbCc) => match a.ext.cmp(&b.ext) { Ordering::Equal => natord::compare_ignore_case(&*a.name, &*b.name), order => order, }, - SortField::NameMixHidden(ABCabc) => natord::compare( - SortField::strip_dot(&a.name), - SortField::strip_dot(&b.name) + Self::NameMixHidden(ABCabc) => natord::compare( + Self::strip_dot(&a.name), + Self::strip_dot(&b.name) ), - SortField::NameMixHidden(AaBbCc) => natord::compare_ignore_case( - SortField::strip_dot(&a.name), - SortField::strip_dot(&b.name) + Self::NameMixHidden(AaBbCc) => natord::compare_ignore_case( + Self::strip_dot(&a.name), + Self::strip_dot(&b.name) ) } } fn strip_dot(n: &str) -> &str { - if n.starts_with('.') { - &n[1..] - } else { - n + match n.strip_prefix('.') { + Some(s) => s, + None => n, } } } @@ -295,22 +271,26 @@ impl SortField { /// The **ignore patterns** are a list of globs that are tested against /// each filename, and if any of them match, that file isn’t displayed. /// This lets a user hide, say, text files by ignoring `*.txt`. -#[derive(PartialEq, Default, Debug, Clone)] +#[derive(PartialEq, Eq, Default, Debug, Clone)] pub struct IgnorePatterns { patterns: Vec, } impl FromIterator for IgnorePatterns { - fn from_iter>(iter: I) -> Self { - IgnorePatterns { patterns: iter.into_iter().collect() } + + fn from_iter(iter: I) -> Self + where I: IntoIterator + { + let patterns = iter.into_iter().collect(); + Self { patterns } } } impl IgnorePatterns { /// Create a new list from the input glob strings, turning the inputs that - /// are valid glob patterns into an IgnorePatterns. The inputs that don’t - /// parse correctly are returned separately. + /// are valid glob patterns into an `IgnorePatterns`. The inputs that + /// don’t parse correctly are returned separately. pub fn parse_from_iter<'a, I: IntoIterator>(iter: I) -> (Self, Vec) { let iter = iter.into_iter(); @@ -331,48 +311,32 @@ impl IgnorePatterns { } } - (IgnorePatterns { patterns }, errors) + (Self { patterns }, errors) } /// Create a new empty set of patterns that matches nothing. - pub fn empty() -> IgnorePatterns { - IgnorePatterns { patterns: Vec::new() } + pub fn empty() -> Self { + Self { patterns: Vec::new() } } /// Test whether the given file should be hidden from the results. fn is_ignored(&self, file: &str) -> bool { self.patterns.iter().any(|p| p.matches(file)) } - - /// Test whether the given file should be hidden from the results. - pub fn is_ignored_path(&self, file: &Path) -> bool { - self.patterns.iter().any(|p| p.matches_path(file)) - } - - // TODO(ogham): The fact that `is_ignored_path` is pub while `is_ignored` - // isn’t probably means it’s in the wrong place } -/// Whether to ignore or display files that are mentioned in `.gitignore` files. -#[derive(PartialEq, Debug, Copy, Clone)] +/// Whether to ignore or display files that Git would ignore. +#[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum GitIgnore { - /// Ignore files that Git would ignore. This means doing a check for a - /// `.gitignore` file, possibly recursively up the filesystem tree. + /// Ignore files that Git would ignore. CheckAndIgnore, /// Display files, even if Git would ignore them. Off, } -// This is not fully baked yet. The `ignore` crate lists a lot more files that -// we aren’t checking: -// -// > By default, all ignore files found are respected. This includes .ignore, -// > .gitignore, .git/info/exclude and even your global gitignore globs, -// > usually found in $XDG_CONFIG_HOME/git/ignore. - #[cfg(test)] @@ -382,31 +346,31 @@ mod test_ignores { #[test] fn empty_matches_nothing() { let pats = IgnorePatterns::empty(); - assert_eq!(false, pats.is_ignored("nothing")); - assert_eq!(false, pats.is_ignored("test.mp3")); + assert!(!pats.is_ignored("nothing")); + assert!(!pats.is_ignored("test.mp3")); } #[test] fn ignores_a_glob() { let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "*.mp3" ]); assert!(fails.is_empty()); - assert_eq!(false, pats.is_ignored("nothing")); - assert_eq!(true, pats.is_ignored("test.mp3")); + assert!(!pats.is_ignored("nothing")); + assert!(pats.is_ignored("test.mp3")); } #[test] fn ignores_an_exact_filename() { let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing" ]); assert!(fails.is_empty()); - assert_eq!(true, pats.is_ignored("nothing")); - assert_eq!(false, pats.is_ignored("test.mp3")); + assert!(pats.is_ignored("nothing")); + assert!(!pats.is_ignored("test.mp3")); } #[test] fn ignores_both() { let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing", "*.mp3" ]); assert!(fails.is_empty()); - assert_eq!(true, pats.is_ignored("nothing")); - assert_eq!(true, pats.is_ignored("test.mp3")); + assert!(pats.is_ignored("nothing")); + assert!(pats.is_ignored("test.mp3")); } } diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 3275ccf3..1188f615 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -4,7 +4,7 @@ pub use self::dir::{Dir, DotFilter}; mod file; pub use self::file::{File, FileTarget}; +pub mod dir_action; pub mod feature; pub mod fields; pub mod filter; -pub mod dir_action; diff --git a/src/info/filetype.rs b/src/info/filetype.rs index 0dc758ff..9223fdc2 100644 --- a/src/info/filetype.rs +++ b/src/info/filetype.rs @@ -7,11 +7,11 @@ use ansi_term::Style; use crate::fs::File; -use crate::output::file_name::FileColours; use crate::output::icons::FileIcon; +use crate::theme::FileColours; -#[derive(Debug, Default, PartialEq)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct FileExtensions; impl FileExtensions { @@ -19,76 +19,80 @@ impl FileExtensions { /// An “immediate” file is something that can be run or activated somehow /// in order to kick off the build of a project. It’s usually only present /// in directories full of source code. - fn is_immediate(&self, file: &File) -> bool { + #[allow(clippy::case_sensitive_file_extension_comparisons)] + fn is_immediate(&self, file: &File<'_>) -> bool { file.name.to_lowercase().starts_with("readme") || file.name.ends_with(".ninja") || file.name_is_one_of( &[ "Makefile", "Cargo.toml", "SConstruct", "CMakeLists.txt", "build.gradle", "pom.xml", "Rakefile", "package.json", "Gruntfile.js", - "Gruntfile.coffee", "BUILD", "BUILD.bazel", "WORKSPACE", "build.xml", - "webpack.config.js", "meson.build", + "Gruntfile.coffee", "BUILD", "BUILD.bazel", "WORKSPACE", "build.xml", "Podfile", + "webpack.config.js", "meson.build", "composer.json", "RoboFile.php", "PKGBUILD", + "Justfile", "Procfile", "Dockerfile", "Containerfile", "Vagrantfile", "Brewfile", + "Gemfile", "Pipfile", "build.sbt", "mix.exs", "bsconfig.json", "tsconfig.json", ]) } - fn is_image(&self, file: &File) -> bool { + fn is_image(&self, file: &File<'_>) -> bool { file.extension_is_one_of( &[ - "png", "jpeg", "jpg", "gif", "bmp", "tiff", "tif", - "ppm", "pgm", "pbm", "pnm", "webp", "raw", "arw", - "svg", "stl", "eps", "dvi", "ps", "cbr", "jpf", - "cbz", "xpm", "ico", "cr2", "orf", "nef", + "png", "jfi", "jfif", "jif", "jpe", "jpeg", "jpg", "gif", "bmp", + "tiff", "tif", "ppm", "pgm", "pbm", "pnm", "webp", "raw", "arw", + "svg", "stl", "eps", "dvi", "ps", "cbr", "jpf", "cbz", "xpm", + "ico", "cr2", "orf", "nef", "heif", "avif", "jxl", "j2k", "jp2", + "j2c", "jpx", ]) } - fn is_video(&self, file: &File) -> bool { + fn is_video(&self, file: &File<'_>) -> bool { file.extension_is_one_of( &[ "avi", "flv", "m2v", "m4v", "mkv", "mov", "mp4", "mpeg", - "mpg", "ogm", "ogv", "vob", "wmv", "webm", "m2ts", + "mpg", "ogm", "ogv", "vob", "wmv", "webm", "m2ts", "heic", ]) } - fn is_music(&self, file: &File) -> bool { + fn is_music(&self, file: &File<'_>) -> bool { file.extension_is_one_of( &[ "aac", "m4a", "mp3", "ogg", "wma", "mka", "opus", ]) } // Lossless music, rather than any other kind of data... - fn is_lossless(&self, file: &File) -> bool { + fn is_lossless(&self, file: &File<'_>) -> bool { file.extension_is_one_of( &[ "alac", "ape", "flac", "wav", ]) } - fn is_crypto(&self, file: &File) -> bool { + fn is_crypto(&self, file: &File<'_>) -> bool { file.extension_is_one_of( &[ "asc", "enc", "gpg", "pgp", "sig", "signature", "pfx", "p12", ]) } - fn is_document(&self, file: &File) -> bool { + fn is_document(&self, file: &File<'_>) -> bool { file.extension_is_one_of( &[ - "djvu", "doc", "docx", "dvi", "eml", "eps", "fotd", - "odp", "odt", "pdf", "ppt", "pptx", "rtf", - "xls", "xlsx", + "djvu", "doc", "docx", "dvi", "eml", "eps", "fotd", "key", + "keynote", "numbers", "odp", "odt", "pages", "pdf", "ppt", + "pptx", "rtf", "xls", "xlsx", ]) } - fn is_compressed(&self, file: &File) -> bool { + fn is_compressed(&self, file: &File<'_>) -> bool { file.extension_is_one_of( &[ "zip", "tar", "Z", "z", "gz", "bz2", "a", "ar", "7z", "iso", "dmg", "tc", "rar", "par", "tgz", "xz", "txz", - "lz", "tlz", "lzma", "deb", "rpm", "zst", + "lz", "tlz", "lzma", "deb", "rpm", "zst", "lz4", "cpio", ]) } - fn is_temp(&self, file: &File) -> bool { + fn is_temp(&self, file: &File<'_>) -> bool { file.name.ends_with('~') || (file.name.starts_with('#') && file.name.ends_with('#')) - || file.extension_is_one_of( &[ "tmp", "swp", "swo", "swn", "bak", "bk" ]) + || file.extension_is_one_of( &[ "tmp", "swp", "swo", "swn", "bak", "bkp", "bk" ]) } - fn is_compiled(&self, file: &File) -> bool { - if file.extension_is_one_of( &[ "class", "elc", "hi", "o", "pyc", "zwc" ]) { + fn is_compiled(&self, file: &File<'_>) -> bool { + if file.extension_is_one_of( &[ "class", "elc", "hi", "o", "pyc", "zwc", "ko" ]) { true } else if let Some(dir) = file.parent_dir { @@ -101,7 +105,7 @@ impl FileExtensions { } impl FileColours for FileExtensions { - fn colour_file(&self, file: &File) -> Option