Compare commits

...

87 Commits

Author SHA1 Message Date
kdurant 93fe658574 add install.sh 2026-02-21 15:29:15 +00:00
andy.boot 709b87e137 docs: Update README.md 2026-01-08 23:30:04 +00:00
andy.boot 3a16a6a234 version: increment version 2026-01-08 23:16:16 +00:00
andy.boot 2066b7fa86 Revert "build: Attempt to add deb musl build."
This reverts commit 37dd671e5e.
2026-01-08 23:03:10 +00:00
andy.boot 07be4261b9 build: Attempt to add deb musl build.
I have no idea if this will work
2026-01-08 20:28:52 +00:00
andy.boot d26971f869 Fix: windows build missed in nu_ansi_term upgrade 2026-01-08 20:14:56 +00:00
andy.boot 28018bc8c2 deps: update dependencies 2026-01-08 20:14:56 +00:00
andy.boot 37dd671e5e build: Attempt to add deb musl build.
I have no idea if this will work
2026-01-08 20:03:42 +00:00
Jeremy Bícha d39224d670 deps: switch from ansi_term to nu_ansi_term 2026-01-08 19:32:58 +00:00
andy.boot 6c20fae3e8 docs: Update README.md
typos
2026-01-07 20:05:43 +00:00
andy.boot 3ab81fcd2f docs: Update README.md 2026-01-07 20:03:28 +00:00
andy.boot 0703784cab docs: Update README.md 2026-01-07 20:02:58 +00:00
andy.boot f51bd0d6a3 docs: update image
better image
2026-01-07 20:00:55 +00:00
andy.boot 514bdcdc74 docs: Update README.md
using a new image
2026-01-07 19:59:26 +00:00
andy.boot 4b64585290 docs: Update README.md 2026-01-07 19:54:43 +00:00
andy.boot 10eba445c9 docs: update description of 'n'
Change sample image file
2026-01-07 19:51:47 +00:00
andy.boot a0c0779849 tests: Update assert_cmd
Use new cargo_bin_cmd macro. Old way of building Commands is deprecated
2026-01-07 19:13:33 +00:00
andy.boot 62bf1e14de deps: update libraries 2025-11-24 21:15:44 +00:00
andy.boot de2d748a88 chore: Retire directories library
Only used in one place (less dependencies = good).
Also directories has been retired as a github project. Easier to replace
with std::env::home() which has all the functionality we need
2025-11-20 23:46:34 +00:00
Benjamin A. Beasley 509d51e872 Update directories from 4 to 6 2025-11-20 22:46:40 +00:00
AminurAlam f98b841d23 fix: dont format filecount when output is set to json 2025-10-13 19:49:24 +01:00
andy.boot 67d23e80ff style: fix clippy 2025-10-06 20:42:19 +01:00
andy.boot 968377eebd deps: cargo update 2025-10-06 20:42:19 +01:00
Vaso Putica 96e04fe168 fix: number-of-lines option in config file is ignored (#528)
Co-authored-by: andy.boot <bootandy@gmail.com>
2025-10-06 20:22:35 +01:00
Junghyeon Park 7974e2eaf0 feat: support --files-from/--files0-from
Read NUL- or newline-terminated paths from FILE or stdin.

Wire options through `config` and `main`. Add tests and update the
man page, README, and completion scripts.
2025-10-06 20:11:01 +01:00
dustin fletcher 14efddfd05 Update README.md
wrap one-liner in Alternatives section with backticks (`) for code formatting
2025-10-06 20:01:04 +01:00
andy.boot 4e83421da6 version: increment version 2025-07-31 22:33:44 +01:00
andy.boot 901bc3895a feat: Return 1 if no dirs are found
A better error code
2025-07-31 22:33:44 +01:00
andy.boot 3cce61f854 tests: add test for d0
Test to stop bug in previous commit occuring
Also cargo clippy
2025-07-28 22:35:40 +01:00
Yip Coekjan 222cd83ff3 Fix multi-paths output when depth is 0 2025-07-28 22:15:34 +01:00
andy.boot 76b9f32859 release: increment version 2025-07-06 09:46:33 +01:00
andy.boot 17662e8ff1 refactor: ThreadPool use build not build_global (#512)
Dangerous: Change how threadpool initialized
2025-07-06 09:42:02 +01:00
andy.boot 9cc557cada docs: update readme
Improve description of -f,  by default this counts by inode,
Use -f -s to count files and include duplicate inodes.
2025-07-05 15:34:08 +01:00
andy.boot 81722b695d fix: dir_walker interrupted error (#507)
* fix: dir_walker interrupted error

Ignore and continue if we get many interrupted errors instead of
panicing on 3.

This is from user feedback who reported occasional panics from too many
interrupted errors

https://github.com/bootandy/dust/issues/495#issuecomment-3026673312

* Revert "fix: dir_walker interrupted error"

This reverts commit 84fa0ea9a4.

* fix: interrupted error, set limit to 999 instead.
2025-07-05 10:01:10 +01:00
andy.boot 51dc167345 deps: cargo update (#510)
* hack

* Revert "hack"

This reverts commit d51c5b890439ec5ea46a10454801f9ca3593afca.

* deps: upgrade
2025-07-05 09:42:26 +01:00
andy.boot 9b5f6d6c5a style: fix clippy 2025-07-02 18:53:24 +01:00
andy.boot 74ffd78901 refactor: pull out more methods, remove unused Option 2025-06-06 20:44:00 +01:00
andy.boot 9b2dc4655d refactor: extract function 2025-06-04 20:57:57 +01:00
andy.boot 29441eda19 version: increment version 2025-06-04 20:07:23 +01:00
andy.boot e6f90362a7 fix: bug: Remove bad error handling
This may be causing dust to lock up as we were accidentally creating a
second editable_error causing threads to lock.

https://github.com/bootandy/dust/issues/495
2025-06-04 19:55:50 +01:00
Shun Sakai 702f0f0fe9 chore(cli): Migrate to Derive API
Change the definition of CLI from the Builder API to the Derive API.
2025-05-16 22:09:27 +01:00
andy.boot 6a14d7e8b3 style: Fix clippy
New clippy has new standards.

Clean up get_metadata by removing unused return
2025-04-20 19:55:36 +01:00
Teemu Pätsi 4e2d93f362 refactor: Fix clippy warnings (#488)
* refactor: Use `repeat_n` instead of `repeat` and `take`

Fixes `clippy::manual_repeat_n`

* refactor: Remove unnecessary let binding
2025-04-20 18:48:11 +01:00
Teemu Pätsi b616378ba0 Fix: miscalculation of NTFS mount file sizes inside WSL (#487)
* fix: Limit file size based on the file system I/O block size

* fix: Take possible file pre-allocation into account

* refactor: Reduce indenting with early return

* refactor: Fix clippy::manual_div_ceil

* fix: Use target_size instead of max_size

* fix: Take possible pre-allocation for a file into account

https://github.com/bootandy/dust/pull/487#issuecomment-2816885444
2025-04-20 08:50:09 +01:00
andy.boot 646cdd976d release: increment version (#485) 2025-04-01 23:19:59 +01:00
andy.boot 9a49221ac1 fix: status mesages go to stderr not stdout (#483)
Progress spinner and status line are written to stderr
instead of stdout.

No longer any need to detect if stdout is being redirected.
2025-04-01 23:19:36 +01:00
andy.boot 1b4116e39d fix: all arguments now use '-' instead of '_' (#484)
* cli: unify long arguments (dashes instead of underscores)

* completions: autoregen completions

---------

Co-authored-by: Pavel Kulyov <kulyov.pavel@gmail.com>
2025-04-01 23:18:49 +01:00
Aidan Beggs 733abb2a3f feat: Abort immedietly when ^C is received. (#478)
Previously, we attempted to perform a clean shutdown, which could take a
significant period of time on slow filesystems. This commit changes the
shutdown logic to abort immedietly when ^C is received by the program.
2025-03-30 17:35:28 +01:00
andy.boot dd799706fb deps: cargo update (#474)
* deps: cargo update

* deps: Update edition 2024

and run cargo update
2025-03-11 00:38:38 +00:00
Teemu Pätsi b219981c52 rewrite: Reduce indentation with guard clause 2025-03-10 21:25:13 +00:00
Teemu Pätsi c31468b199 perf: Canonicalize ignored absolute path only once 2025-03-10 21:25:13 +00:00
Teemu Pätsi 28d409ea27 refactor: Extract is_ignored_path function 2025-03-10 21:25:13 +00:00
Teemu Pätsi aa319e3599 perf: Do not canonicalize non-absolute ignored path 2025-03-10 21:25:13 +00:00
Teemu Pätsi c2a4c4573a fix: Ignoring absolute path with -X option 2025-03-10 21:25:13 +00:00
andy.boot d876cc28a7 release: Increment version 2025-02-27 22:08:38 +00:00
andy.boot 137e366eca feat: Handle duplicate dir names better
If we run `dust /usr/*/Trash`
We see several 'Trash' directories in the output but do not know which
user they belong to.

This fix means if we see duplicate names in a directory we will display
the parent directory name as well
2025-02-20 22:08:06 +00:00
andy.boot a962b80eec deps: cargo update 2025-02-06 00:30:39 +00:00
andy.boot 01c0aaeade feat: New --collapse flag
--collapse will keep that directory collapsed and will not expand it.
2025-01-27 22:00:08 +00:00
andy.boot 6cbd736e11 fix: Bars in --skip-total flag
Before we calculated the % by taking the longest bar. If you use
--skip-total the longest bar is not the total. We need to sum up all the
children of root to work out what the largest size is.
2025-01-26 11:22:37 +00:00
andy.boot 8e087e09da fix: Handle Interrupted Error
Rust may throw Interrupted errors while scanning filesystem. These may
be retried:
https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.Interrupted
2025-01-26 11:22:04 +00:00
andy.boot 9ba0b6d1d0 feat: Support -o flag for JSON output
requested in: https://github.com/bootandy/dust/issues/450
2025-01-17 20:48:56 +00:00
andy.boot 775d841840 style: clippy 2025-01-15 21:37:29 +00:00
n4n5 609fc1e760 clippy 2025-01-15 19:14:00 +00:00
n4n5 eeb686562d push config option 2025-01-15 19:14:00 +00:00
n4n5 e0eaeccc0b add wget install 2025-01-15 19:08:18 +00:00
n4n5 2e56a261e0 clippy 2025-01-15 19:08:18 +00:00
n4n5 bfe7323b20 fix typo Operator 2025-01-15 19:08:18 +00:00
janbridley 1372815007 Format src/display.rs 2024-11-08 22:50:03 +00:00
janbridley 7c9e2f1833 Enable pretty format for petabyte data 2024-11-08 22:50:03 +00:00
Camille Louédoc-Eyriès 1d40ca0870 docs(readme): warn about snap-dust limitations 2024-10-17 23:03:35 +01:00
Yukai Chou 86b2bd944c refactor: simplify get_height_of_terminal() and get_width... 2024-09-16 22:01:10 +01:00
andy.boot b63608604a docs: Update README.md 2024-09-03 21:57:36 +01:00
andy.boot b24fab720d deps: cargo update 2024-09-03 21:54:20 +01:00
wugeer d81b9065a1 fix: man-page and completions missing in debian package 2024-09-03 21:49:23 +01:00
andy.boot 38c4d23732 docs: Update README.md
include snap
2024-08-21 18:53:55 +01:00
wugeer 99bf0fc041 docs: update sample config.toml 2024-08-21 18:47:15 +01:00
wugeer 75d0566949 feat: use pre-commit hooks to standardize commit messages 2024-08-21 18:46:20 +01:00
NoisyCoil 489d9ada44 fix: 64-bit atomics for platforms with no 64-bit atomics
Closes: #423
2024-08-09 19:28:20 +01:00
wugeer f48fcc790a feat: support Dust tree by age 2024-08-07 19:31:22 +01:00
wugeer 733117d0f6 fix: retrieve metadata for symbolic links without following them
Previously, the function get_metadata in platform.rs used `fs::metadata` which follows symbolic links
and returns metadata for the target file. This caused issues #421: du / dust disagreement
when trying to determine properties of the symbolic link itself
2024-07-31 00:16:59 +01:00
andy.boot dbd18f90e7 docs: update release procedure
Previously I accidentally tagged a release before building it leading
to an out of date Cargo.lock file.
2024-07-17 19:35:44 +01:00
andy.boot dad88ad660 version: increment version 2024-07-17 19:35:44 +01:00
andy.boot 00a7c410a0 ci: fix warning in windows build 2024-07-17 19:35:44 +01:00
andy.boot 1ab0b2f531 refactor: rename variable 2024-07-17 19:35:44 +01:00
andy.boot c09073151d fix: perf issues with v1.1.0
Bring performance back
2024-07-17 19:16:46 +01:00
andy.boot b4a517a096 version: update version
Also update docs, so I don't partially update a version number again
2024-07-16 22:48:27 +01:00
andy.boot e654d30f9d version: increment version 2024-07-16 22:05:17 +01:00
37 changed files with 2473 additions and 1308 deletions
+5
View File
@@ -45,6 +45,11 @@ jobs:
override: true override: true
profile: minimal # minimal component installation (ie, no documentation) profile: minimal # minimal component installation (ie, no documentation)
components: rustfmt, clippy components: rustfmt, clippy
- name: Install wget for Windows
if: matrix.job.os == 'windows-latest'
run: choco install wget --no-progress
- name: typos-action
uses: crate-ci/typos@v1.28.4
- name: "`fmt` testing" - name: "`fmt` testing"
if: steps.vars.outputs.JOB_DO_FORMAT_TESTING if: steps.vars.outputs.JOB_DO_FORMAT_TESTING
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
+11
View File
@@ -0,0 +1,11 @@
repos:
- repo: https://github.com/doublify/pre-commit-rust
rev: v1.0
hooks:
- id: cargo-check
stages: [commit]
- id: fmt
stages: [commit]
- id: clippy
args: [--all-targets, --all-features]
stages: [commit]
Generated
+489 -384
View File
File diff suppressed because it is too large Load Diff
+23 -11
View File
@@ -1,9 +1,9 @@
[package] [package]
name = "du-dust" name = "du-dust"
description = "A more intuitive version of du" description = "A more intuitive version of du"
version = "1.0.0" version = "1.2.4"
authors = ["bootandy <bootandy@gmail.com>", "nebkor <code@ardent.nebcorp.com>"] authors = ["bootandy <bootandy@gmail.com>", "nebkor <code@ardent.nebcorp.com>"]
edition = "2021" edition = "2024"
readme = "README.md" readme = "README.md"
documentation = "https://github.com/bootandy/dust" documentation = "https://github.com/bootandy/dust"
@@ -27,11 +27,11 @@ lto = true
strip = true strip = true
[dependencies] [dependencies]
ansi_term = "0.12" clap = { version = "4", features = ["derive"] }
clap = "4.4" lscolors = "0.21"
lscolors = "0.13" nu-ansi-term = "0.50"
terminal_size = "0.2" terminal_size = "0.4"
unicode-width = "0.1" unicode-width = "0.2"
rayon = "1" rayon = "1"
thousands = "0.2" thousands = "0.2"
stfu8 = "0.2" stfu8 = "0.2"
@@ -39,11 +39,13 @@ regex = "1"
config-file = "0.2" config-file = "0.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
directories = "4" sysinfo = "0.37"
sysinfo = "0.27" ctrlc = "3"
ctrlc = "3.4"
chrono = "0.4" chrono = "0.4"
[target.'cfg(not(target_has_atomic = "64"))'.dependencies]
portable-atomic = "1.4"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winapi-util = "0.1" winapi-util = "0.1"
filesize = "0.2.0" filesize = "0.2.0"
@@ -53,7 +55,7 @@ assert_cmd = "2"
tempfile = "=3" tempfile = "=3"
[build-dependencies] [build-dependencies]
clap = "4.4" clap = { version = "4.4", features = ["derive"] }
clap_complete = "4.4" clap_complete = "4.4"
clap_mangen = "0.2" clap_mangen = "0.2"
@@ -83,6 +85,16 @@ assets = [
"usr/share/doc/du-dust/README", "usr/share/doc/du-dust/README",
"644", "644",
], ],
[
"man-page/dust.1",
"usr/share/man/man1/dust.1",
"644",
],
[
"completions/dust.bash",
"usr/share/bash-completion/completions/dust",
"644",
],
] ]
extended-description = """\ extended-description = """\
Dust is meant to give you an instant overview of which directories are using Dust is meant to give you an instant overview of which directories are using
+48 -6
View File
@@ -13,9 +13,27 @@ Because I want an easy way to see where my disk is being used.
![Example](media/snap.png) ![Example](media/snap.png)
Study the above picture.
* We see `target` has 1.8G
* `target/debug` is the same size as `target` - so we know nearly all the disk usage of the 1.8G is in this folder
* `target/debug/deps` this is 1.2G - Note the bar jumps down to 70% to indicate that most disk usage is here but not all.
* `target/debug/deps/dust-e78c9f87a17f24f3` - This is the largest file in this folder, but it is only 46M - Note the bar jumps down to 3% to indicate the file is small.
From here we can conclude:
* `target/debug/deps` takes the majority of the space in `target` and that `target/debug/deps` has a large number of relatively small files.
## Install ## Install
#### Cargo <a href="https://repology.org/project/du-dust/versions"><img src="https://repology.org/badge/vertical-allrepos/du-dust.svg" alt="Packaging status" align="right"></a> ### Quick Install (Linux, macOS, Windows)
```bash
curl -sSfL https://raw.githubusercontent.com/bootandy/dust/refs/heads/master/install.sh | sh
```
### Cargo <a href="https://repology.org/project/du-dust/versions"><img src="https://repology.org/badge/vertical-allrepos/du-dust.svg" alt="Packaging status" align="right"></a>
#### Cargo
- `cargo install du-dust` - `cargo install du-dust`
@@ -27,11 +45,17 @@ Because I want an easy way to see where my disk is being used.
- `brew install dust` - `brew install dust`
#### [Snap](https://ubuntu.com/core/services/guide/snaps-intro) Ubuntu and [supported systems](https://snapcraft.io/docs/installing-snapd)
- `snap install dust`
Note: `dust` installed through `snap` can only access files stored in the `/home` directory. See daniejstriata/dust-snap#2 for more information.
#### [Pacstall](https://github.com/pacstall/pacstall) (Debian/Ubuntu) #### [Pacstall](https://github.com/pacstall/pacstall) (Debian/Ubuntu)
- `pacstall -I dust-bin` - `pacstall -I dust-bin`
### Anaconda (conda-forge) #### Anaconda (conda-forge)
- `conda install -c conda-forge dust` - `conda install -c conda-forge dust`
@@ -39,6 +63,10 @@ Because I want an easy way to see where my disk is being used.
- `deb-get install du-dust` - `deb-get install du-dust`
#### [x-cmd](https://www.x-cmd.com/pkg/#VPContent)
- `x env use dust`
#### Windows: #### Windows:
- `scoop install dust` - `scoop install dust`
@@ -59,6 +87,8 @@ Dust will list a slightly-less-than-the-terminal-height number of the biggest su
The different colors on the bars: These represent the combined tree hierarchy & disk usage. The shades of grey are used to indicate which parent folder a subfolder belongs to. For instance, look at the above screenshot. `.steam` is a folder taking 44% of the space. From the `.steam` bar is a light grey line that goes up. All these folders are inside `.steam` so if you delete `.steam` all that stuff will be gone too. The different colors on the bars: These represent the combined tree hierarchy & disk usage. The shades of grey are used to indicate which parent folder a subfolder belongs to. For instance, look at the above screenshot. `.steam` is a folder taking 44% of the space. From the `.steam` bar is a light grey line that goes up. All these folders are inside `.steam` so if you delete `.steam` all that stuff will be gone too.
If you are new to the tool I recommend to try tweaking the `-n` parameter. `dust -n 10`, `dust -n 50`.
## Usage ## Usage
``` ```
@@ -80,7 +110,7 @@ Usage: dust -B (--bars-on-right - Percent bars moved to right side of screen)
Usage: dust -i (Do not show hidden files) Usage: dust -i (Do not show hidden files)
Usage: dust -c (No colors [monochrome]) Usage: dust -c (No colors [monochrome])
Usage: dust -C (Force colors) Usage: dust -C (Force colors)
Usage: dust -f (Count files instead of diskspace) Usage: dust -f (Count files instead of diskspace [Counts by inode, to include duplicate inodes use dust -f -s])
Usage: dust -t (Group by filetype) Usage: dust -t (Group by filetype)
Usage: dust -z 10M (min-size, Only include files larger than 10M) Usage: dust -z 10M (min-size, Only include files larger than 10M)
Usage: dust -e regex (Only include files matching this regex (eg dust -e "\.png$" would match png files)) Usage: dust -e regex (Only include files matching this regex (eg dust -e "\.png$" would match png files))
@@ -92,7 +122,9 @@ Usage: dust -S (Custom Stack size - Use if you see: 'fatal runtime error: stack
Usage: dust --skip-total (No total row will be displayed) Usage: dust --skip-total (No total row will be displayed)
Usage: dust -z 40000/30MB/20kib (Exclude output files/directories below size 40000 bytes / 30MB / 20KiB) Usage: dust -z 40000/30MB/20kib (Exclude output files/directories below size 40000 bytes / 30MB / 20KiB)
Usage: dust -j (Prints JSON representation of directories, try: dust -j | jq) Usage: dust -j (Prints JSON representation of directories, try: dust -j | jq)
Usage: dust --files0-from=FILE (Reads null-terminated file paths from FILE); If FILE is - then read from stdin Usage: dust --files0-from=FILE (Read NUL-terminated file paths from FILE; if FILE is '-', read from stdin)
Usage: dust --files-from=FILE (Read newline-terminated file paths from FILE; if FILE is '-', read from stdin)
Usage: dust --collapse=node-modules will keep the node-modules folder collapsed in display instead of recursively opening it
``` ```
## Config file ## Config file
@@ -111,6 +143,16 @@ reverse=true
- [dua](https://github.com/Byron/dua-cli/) - [dua](https://github.com/Byron/dua-cli/)
- [pdu](https://github.com/KSXGitHub/parallel-disk-usage) - [pdu](https://github.com/KSXGitHub/parallel-disk-usage)
- [dirstat-rs](https://github.com/scullionw/dirstat-rs) - [dirstat-rs](https://github.com/scullionw/dirstat-rs)
- du -d 1 -h | sort -h - `du -d 1 -h | sort -h`
## Why to use Dust over the Alternatives
Dust simply Does The Right Thing when handling lots of small files & directories. Dust keeps the output simple by only showing large entries.
Tools like ncdu & baobab, give you a view of directory sizes but you have no idea where the largest files are. For example directory A could have a size larger than directory B, but in fact the largest file is in B and not A. Finding this out via these other tools is not trivial whereas Dust will show the large file clearly in the tree hierarchy
Dust will not count hard links multiple times (unless you want to `-s`).
Typing `dust -n 90` will show you your 90 largest entries. `-n` is not quite like `head -n` or `tail -n`, dust is intelligent and chooses the largest entries
Note: Apparent-size is calculated slightly differently in dust to gdu. In dust each hard link is counted as using file_length space. In gdu only the first entry is counted.
+2 -1
View File
@@ -1,3 +1,4 @@
use clap::CommandFactory;
use clap_complete::{generate_to, shells::*}; use clap_complete::{generate_to, shells::*};
use clap_mangen::Man; use clap_mangen::Man;
use std::fs::File; use std::fs::File;
@@ -9,7 +10,7 @@ include!("src/cli.rs");
fn main() -> Result<(), Error> { fn main() -> Result<(), Error> {
let outdir = "completions"; let outdir = "completions";
let app_name = "dust"; let app_name = "dust";
let mut cmd = build_cli(); let mut cmd = Cli::command();
generate_to(Bash, &mut cmd, app_name, outdir)?; generate_to(Bash, &mut cmd, app_name, outdir)?;
generate_to(Zsh, &mut cmd, app_name, outdir)?; generate_to(Zsh, &mut cmd, app_name, outdir)?;
+13 -6
View File
@@ -1,14 +1,21 @@
# ----------- To do a release --------- # ----------- To do a release ---------
# Compare times of runs to check no drastic slow down:
# time target/release/dust ~/dev
# time dust ~dev
# edit version in cargo.toml # ----------- Pre release ---------
# Compare times of runs to check no drastic slow down:
# hyperfine 'target/release/dust /home/andy'
# hyperfine 'dust /home/andy'
# ----------- Release ---------
# inc version in cargo.toml
# cargo build --release
# commit changed files
# merge to master in github
# tag a commit and push (increment version in Cargo.toml first): # tag a commit and push (increment version in Cargo.toml first):
# git tag v0.4.5 # git tag v0.4.5
# git push origin v0.4.5 # git push origin v0.4.5
# cargo publish to put it in crates.io # cargo publish to put it in crates.io
# To install locally [Do before pushing it] # Optional: To install locally
#cargo install --path . #cargo install --path .
+65 -38
View File
@@ -14,36 +14,63 @@ _dust() {
fi fi
local context curcontext="$curcontext" state line local context curcontext="$curcontext" state line
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" : \
'-d+[Depth to show]:DEPTH: ' \ '-d+[Depth to show]:DEPTH:_default' \
'--depth=[Depth to show]:DEPTH: ' \ '--depth=[Depth to show]:DEPTH:_default' \
'-T+[Number of threads to use]: : ' \ '-T+[Number of threads to use]:THREADS:_default' \
'--threads=[Number of threads to use]: : ' \ '--threads=[Number of threads to use]:THREADS:_default' \
'-n+[Number of lines of output to show. (Default is terminal_height - 10)]:NUMBER: ' \ '--config=[Specify a config file to use]:FILE:_files' \
'--number-of-lines=[Number of lines of output to show. (Default is terminal_height - 10)]:NUMBER: ' \ '-n+[Display the '\''n'\'' largest entries. (Default is terminal_height)]:NUMBER:_default' \
'*-X+[Exclude any file or directory with this name]:PATH:_files' \ '--number-of-lines=[Display the '\''n'\'' largest entries. (Default is terminal_height)]:NUMBER:_default' \
'*--ignore-directory=[Exclude any file or directory with this name]:PATH:_files' \ '*-X+[Exclude any file or directory with this path]:PATH:_files' \
'*--ignore-directory=[Exclude any file or directory with this path]:PATH:_files' \
'-I+[Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter]:FILE:_files' \ '-I+[Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter]:FILE:_files' \
'--ignore-all-in-file=[Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter]:FILE:_files' \ '--ignore-all-in-file=[Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter]:FILE:_files' \
'-z+[Minimum size file to include in output]:MIN_SIZE: ' \ '-z+[Minimum size file to include in output]:MIN_SIZE:_default' \
'--min-size=[Minimum size file to include in output]:MIN_SIZE: ' \ '--min-size=[Minimum size file to include in output]:MIN_SIZE:_default' \
'(-e --filter -t --file_types)*-v+[Exclude filepaths matching this regex. To ignore png files type\: -v "\\.png\$" ]:REGEX: ' \ '(-e --filter -t --file-types)*-v+[Exclude filepaths matching this regex. To ignore png files type\: -v "\\.png\$"]:REGEX:_default' \
'(-e --filter -t --file_types)*--invert-filter=[Exclude filepaths matching this regex. To ignore png files type\: -v "\\.png\$" ]:REGEX: ' \ '(-e --filter -t --file-types)*--invert-filter=[Exclude filepaths matching this regex. To ignore png files type\: -v "\\.png\$"]:REGEX:_default' \
'(-t --file_types)*-e+[Only include filepaths matching this regex. For png files type\: -e "\\.png\$" ]:REGEX: ' \ '(-t --file-types)*-e+[Only include filepaths matching this regex. For png files type\: -e "\\.png\$"]:REGEX:_default' \
'(-t --file_types)*--filter=[Only include filepaths matching this regex. For png files type\: -e "\\.png\$" ]:REGEX: ' \ '(-t --file-types)*--filter=[Only include filepaths matching this regex. For png files type\: -e "\\.png\$"]:REGEX:_default' \
'-w+[Specify width of output overriding the auto detection of terminal width]:WIDTH: ' \ '-w+[Specify width of output overriding the auto detection of terminal width]:WIDTH:_default' \
'--terminal_width=[Specify width of output overriding the auto detection of terminal width]:WIDTH: ' \ '--terminal-width=[Specify width of output overriding the auto detection of terminal width]:WIDTH:_default' \
'-o+[Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size.]:FORMAT:(si b k m g t kb mb gb tb)' \ '-o+[Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size]:FORMAT:((si\:"SI prefix (powers of 1000)"
'--output-format=[Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size.]:FORMAT:(si b k m g t kb mb gb tb)' \ b\:"byte (B)"
'-S+[Specify memory to use as stack size - use if you see\: '\''fatal runtime error\: stack overflow'\'' (default low memory=1048576, high memory=1073741824)]:STACK_SIZE: ' \ k\:"kibibyte (KiB)"
'--stack-size=[Specify memory to use as stack size - use if you see\: '\''fatal runtime error\: stack overflow'\'' (default low memory=1048576, high memory=1073741824)]:STACK_SIZE: ' \ m\:"mebibyte (MiB)"
'-M+[+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => \[curr(n+1), currn), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)]: : ' \ g\:"gibibyte (GiB)"
'--mtime=[+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => \[curr(n+1), currn), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)]: : ' \ t\:"tebibyte (TiB)"
'-A+[just like -mtime, but based on file access time]: : ' \ kb\:"kilobyte (kB)"
'--atime=[just like -mtime, but based on file access time]: : ' \ mb\:"megabyte (MB)"
'-y+[just like -mtime, but based on file change time]: : ' \ gb\:"gigabyte (GB)"
'--ctime=[just like -mtime, but based on file change time]: : ' \ tb\:"terabyte (TB)"))' \
'--files0-from=[run dust on NUL-terminated file names specified in file; if argument is -, then read names from standard input]: :_files' \ '--output-format=[Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size]:FORMAT:((si\:"SI prefix (powers of 1000)"
b\:"byte (B)"
k\:"kibibyte (KiB)"
m\:"mebibyte (MiB)"
g\:"gibibyte (GiB)"
t\:"tebibyte (TiB)"
kb\:"kilobyte (kB)"
mb\:"megabyte (MB)"
gb\:"gigabyte (GB)"
tb\:"terabyte (TB)"))' \
'-S+[Specify memory to use as stack size - use if you see\: '\''fatal runtime error\: stack overflow'\'' (default low memory=1048576, high memory=1073741824)]:STACK_SIZE:_default' \
'--stack-size=[Specify memory to use as stack size - use if you see\: '\''fatal runtime error\: stack overflow'\'' (default low memory=1048576, high memory=1073741824)]:STACK_SIZE:_default' \
'-M+[+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => \[curr(n+1), currn), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)]:MTIME:_default' \
'--mtime=[+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => \[curr(n+1), currn), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)]:MTIME:_default' \
'-A+[just like -mtime, but based on file access time]:ATIME:_default' \
'--atime=[just like -mtime, but based on file access time]:ATIME:_default' \
'-y+[just like -mtime, but based on file change time]:CTIME:_default' \
'--ctime=[just like -mtime, but based on file change time]:CTIME:_default' \
'(--files-from)--files0-from=[Read NUL-terminated paths from FILE (use \`-\` for stdin)]:FILES0_FROM:_files' \
'(--files0-from)--files-from=[Read newline-terminated paths from FILE (use \`-\` for stdin)]:FILES_FROM:_files' \
'*--collapse=[Keep these directories collapsed]:COLLAPSE:_files' \
'-m+[Directory '\''size'\'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time]:FILETIME:((a\:"last accessed time"
c\:"last changed time"
m\:"last modified time"))' \
'--filetime=[Directory '\''size'\'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time]:FILETIME:((a\:"last accessed time"
c\:"last changed time"
m\:"last modified time"))' \
'-p[Subdirectories will not have their path shortened]' \ '-p[Subdirectories will not have their path shortened]' \
'--full-paths[Subdirectories will not have their path shortened]' \ '--full-paths[Subdirectories will not have their path shortened]' \
'-L[dereference sym links - Treat sym links as directories and go into them]' \ '-L[dereference sym links - Treat sym links as directories and go into them]' \
@@ -68,23 +95,23 @@ _dust() {
'-f[Directory '\''size'\'' is number of child files instead of disk size]' \ '-f[Directory '\''size'\'' is number of child files instead of disk size]' \
'--filecount[Directory '\''size'\'' is number of child files instead of disk size]' \ '--filecount[Directory '\''size'\'' is number of child files instead of disk size]' \
'-i[Do not display hidden files]' \ '-i[Do not display hidden files]' \
'--ignore_hidden[Do not display hidden files]' \ '--ignore-hidden[Do not display hidden files]' \
'(-d --depth -D --only-dir)-t[show only these file types]' \ '(-d --depth -D --only-dir)-t[show only these file types]' \
'(-d --depth -D --only-dir)--file_types[show only these file types]' \ '(-d --depth -D --only-dir)--file-types[show only these file types]' \
'-P[Disable the progress indication.]' \ '-P[Disable the progress indication]' \
'--no-progress[Disable the progress indication.]' \ '--no-progress[Disable the progress indication]' \
'--print-errors[Print path with errors.]' \ '--print-errors[Print path with errors]' \
'(-F --only-file -t --file_types)-D[Only directories will be displayed.]' \ '(-F --only-file -t --file-types)-D[Only directories will be displayed]' \
'(-F --only-file -t --file_types)--only-dir[Only directories will be displayed.]' \ '(-F --only-file -t --file-types)--only-dir[Only directories will be displayed]' \
'(-D --only-dir)-F[Only files will be displayed. (Finds your largest files)]' \ '(-D --only-dir)-F[Only files will be displayed. (Finds your largest files)]' \
'(-D --only-dir)--only-file[Only files will be displayed. (Finds your largest files)]' \ '(-D --only-dir)--only-file[Only files will be displayed. (Finds your largest files)]' \
'-j[Output the directory tree as json to the current directory]' \ '-j[Output the directory tree as json to the current directory]' \
'--output-json[Output the directory tree as json to the current directory]' \ '--output-json[Output the directory tree as json to the current directory]' \
'-h[Print help]' \ '-h[Print help (see more with '\''--help'\'')]' \
'--help[Print help]' \ '--help[Print help (see more with '\''--help'\'')]' \
'-V[Print version]' \ '-V[Print version]' \
'--version[Print version]' \ '--version[Print version]' \
'*::params:_files' \ '*::params -- Input files or directories:_files' \
&& ret=0 && ret=0
} }
+74 -69
View File
@@ -21,75 +21,80 @@ Register-ArgumentCompleter -Native -CommandName 'dust' -ScriptBlock {
$completions = @(switch ($command) { $completions = @(switch ($command) {
'dust' { 'dust' {
[CompletionResult]::new('-d', 'd', [CompletionResultType]::ParameterName, 'Depth to show') [CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'Depth to show')
[CompletionResult]::new('--depth', 'depth', [CompletionResultType]::ParameterName, 'Depth to show') [CompletionResult]::new('--depth', '--depth', [CompletionResultType]::ParameterName, 'Depth to show')
[CompletionResult]::new('-T', 'T ', [CompletionResultType]::ParameterName, 'Number of threads to use') [CompletionResult]::new('-T', '-T ', [CompletionResultType]::ParameterName, 'Number of threads to use')
[CompletionResult]::new('--threads', 'threads', [CompletionResultType]::ParameterName, 'Number of threads to use') [CompletionResult]::new('--threads', '--threads', [CompletionResultType]::ParameterName, 'Number of threads to use')
[CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Number of lines of output to show. (Default is terminal_height - 10)') [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Specify a config file to use')
[CompletionResult]::new('--number-of-lines', 'number-of-lines', [CompletionResultType]::ParameterName, 'Number of lines of output to show. (Default is terminal_height - 10)') [CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'Display the ''n'' largest entries. (Default is terminal_height)')
[CompletionResult]::new('-X', 'X ', [CompletionResultType]::ParameterName, 'Exclude any file or directory with this name') [CompletionResult]::new('--number-of-lines', '--number-of-lines', [CompletionResultType]::ParameterName, 'Display the ''n'' largest entries. (Default is terminal_height)')
[CompletionResult]::new('--ignore-directory', 'ignore-directory', [CompletionResultType]::ParameterName, 'Exclude any file or directory with this name') [CompletionResult]::new('-X', '-X ', [CompletionResultType]::ParameterName, 'Exclude any file or directory with this path')
[CompletionResult]::new('-I', 'I ', [CompletionResultType]::ParameterName, 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter') [CompletionResult]::new('--ignore-directory', '--ignore-directory', [CompletionResultType]::ParameterName, 'Exclude any file or directory with this path')
[CompletionResult]::new('--ignore-all-in-file', 'ignore-all-in-file', [CompletionResultType]::ParameterName, 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter') [CompletionResult]::new('-I', '-I ', [CompletionResultType]::ParameterName, 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter')
[CompletionResult]::new('-z', 'z', [CompletionResultType]::ParameterName, 'Minimum size file to include in output') [CompletionResult]::new('--ignore-all-in-file', '--ignore-all-in-file', [CompletionResultType]::ParameterName, 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter')
[CompletionResult]::new('--min-size', 'min-size', [CompletionResultType]::ParameterName, 'Minimum size file to include in output') [CompletionResult]::new('-z', '-z', [CompletionResultType]::ParameterName, 'Minimum size file to include in output')
[CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$" ') [CompletionResult]::new('--min-size', '--min-size', [CompletionResultType]::ParameterName, 'Minimum size file to include in output')
[CompletionResult]::new('--invert-filter', 'invert-filter', [CompletionResultType]::ParameterName, 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$" ') [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$"')
[CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Only include filepaths matching this regex. For png files type: -e "\.png$" ') [CompletionResult]::new('--invert-filter', '--invert-filter', [CompletionResultType]::ParameterName, 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$"')
[CompletionResult]::new('--filter', 'filter', [CompletionResultType]::ParameterName, 'Only include filepaths matching this regex. For png files type: -e "\.png$" ') [CompletionResult]::new('-e', '-e', [CompletionResultType]::ParameterName, 'Only include filepaths matching this regex. For png files type: -e "\.png$"')
[CompletionResult]::new('-w', 'w', [CompletionResultType]::ParameterName, 'Specify width of output overriding the auto detection of terminal width') [CompletionResult]::new('--filter', '--filter', [CompletionResultType]::ParameterName, 'Only include filepaths matching this regex. For png files type: -e "\.png$"')
[CompletionResult]::new('--terminal_width', 'terminal_width', [CompletionResultType]::ParameterName, 'Specify width of output overriding the auto detection of terminal width') [CompletionResult]::new('-w', '-w', [CompletionResultType]::ParameterName, 'Specify width of output overriding the auto detection of terminal width')
[CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size.') [CompletionResult]::new('--terminal-width', '--terminal-width', [CompletionResultType]::ParameterName, 'Specify width of output overriding the auto detection of terminal width')
[CompletionResult]::new('--output-format', 'output-format', [CompletionResultType]::ParameterName, 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size.') [CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size')
[CompletionResult]::new('-S', 'S ', [CompletionResultType]::ParameterName, 'Specify memory to use as stack size - use if you see: ''fatal runtime error: stack overflow'' (default low memory=1048576, high memory=1073741824)') [CompletionResult]::new('--output-format', '--output-format', [CompletionResultType]::ParameterName, 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size')
[CompletionResult]::new('--stack-size', 'stack-size', [CompletionResultType]::ParameterName, 'Specify memory to use as stack size - use if you see: ''fatal runtime error: stack overflow'' (default low memory=1048576, high memory=1073741824)') [CompletionResult]::new('-S', '-S ', [CompletionResultType]::ParameterName, 'Specify memory to use as stack size - use if you see: ''fatal runtime error: stack overflow'' (default low memory=1048576, high memory=1073741824)')
[CompletionResult]::new('-M', 'M ', [CompletionResultType]::ParameterName, '+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => [curr(n+1), currn), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)') [CompletionResult]::new('--stack-size', '--stack-size', [CompletionResultType]::ParameterName, 'Specify memory to use as stack size - use if you see: ''fatal runtime error: stack overflow'' (default low memory=1048576, high memory=1073741824)')
[CompletionResult]::new('--mtime', 'mtime', [CompletionResultType]::ParameterName, '+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => [curr(n+1), currn), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)') [CompletionResult]::new('-M', '-M ', [CompletionResultType]::ParameterName, '+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => [curr(n+1), currn), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)')
[CompletionResult]::new('-A', 'A ', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file access time') [CompletionResult]::new('--mtime', '--mtime', [CompletionResultType]::ParameterName, '+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => [curr(n+1), currn), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)')
[CompletionResult]::new('--atime', 'atime', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file access time') [CompletionResult]::new('-A', '-A ', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file access time')
[CompletionResult]::new('-y', 'y', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file change time') [CompletionResult]::new('--atime', '--atime', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file access time')
[CompletionResult]::new('--ctime', 'ctime', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file change time') [CompletionResult]::new('-y', '-y', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file change time')
[CompletionResult]::new('--files0-from', 'files0-from', [CompletionResultType]::ParameterName, 'run dust on NUL-terminated file names specified in file; if argument is -, then read names from standard input') [CompletionResult]::new('--ctime', '--ctime', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file change time')
[CompletionResult]::new('-p', 'p', [CompletionResultType]::ParameterName, 'Subdirectories will not have their path shortened') [CompletionResult]::new('--files0-from', '--files0-from', [CompletionResultType]::ParameterName, 'Read NUL-terminated paths from FILE (use `-` for stdin)')
[CompletionResult]::new('--full-paths', 'full-paths', [CompletionResultType]::ParameterName, 'Subdirectories will not have their path shortened') [CompletionResult]::new('--files-from', '--files-from', [CompletionResultType]::ParameterName, 'Read newline-terminated paths from FILE (use `-` for stdin)')
[CompletionResult]::new('-L', 'L ', [CompletionResultType]::ParameterName, 'dereference sym links - Treat sym links as directories and go into them') [CompletionResult]::new('--collapse', '--collapse', [CompletionResultType]::ParameterName, 'Keep these directories collapsed')
[CompletionResult]::new('--dereference-links', 'dereference-links', [CompletionResultType]::ParameterName, 'dereference sym links - Treat sym links as directories and go into them') [CompletionResult]::new('-m', '-m', [CompletionResultType]::ParameterName, 'Directory ''size'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time')
[CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'Only count the files and directories on the same filesystem as the supplied directory') [CompletionResult]::new('--filetime', '--filetime', [CompletionResultType]::ParameterName, 'Directory ''size'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time')
[CompletionResult]::new('--limit-filesystem', 'limit-filesystem', [CompletionResultType]::ParameterName, 'Only count the files and directories on the same filesystem as the supplied directory') [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Subdirectories will not have their path shortened')
[CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Use file length instead of blocks') [CompletionResult]::new('--full-paths', '--full-paths', [CompletionResultType]::ParameterName, 'Subdirectories will not have their path shortened')
[CompletionResult]::new('--apparent-size', 'apparent-size', [CompletionResultType]::ParameterName, 'Use file length instead of blocks') [CompletionResult]::new('-L', '-L ', [CompletionResultType]::ParameterName, 'dereference sym links - Treat sym links as directories and go into them')
[CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'Print tree upside down (biggest highest)') [CompletionResult]::new('--dereference-links', '--dereference-links', [CompletionResultType]::ParameterName, 'dereference sym links - Treat sym links as directories and go into them')
[CompletionResult]::new('--reverse', 'reverse', [CompletionResultType]::ParameterName, 'Print tree upside down (biggest highest)') [CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'Only count the files and directories on the same filesystem as the supplied directory')
[CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'No colors will be printed (Useful for commands like: watch)') [CompletionResult]::new('--limit-filesystem', '--limit-filesystem', [CompletionResultType]::ParameterName, 'Only count the files and directories on the same filesystem as the supplied directory')
[CompletionResult]::new('--no-colors', 'no-colors', [CompletionResultType]::ParameterName, 'No colors will be printed (Useful for commands like: watch)') [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Use file length instead of blocks')
[CompletionResult]::new('-C', 'C ', [CompletionResultType]::ParameterName, 'Force colors print') [CompletionResult]::new('--apparent-size', '--apparent-size', [CompletionResultType]::ParameterName, 'Use file length instead of blocks')
[CompletionResult]::new('--force-colors', 'force-colors', [CompletionResultType]::ParameterName, 'Force colors print') [CompletionResult]::new('-r', '-r', [CompletionResultType]::ParameterName, 'Print tree upside down (biggest highest)')
[CompletionResult]::new('-b', 'b', [CompletionResultType]::ParameterName, 'No percent bars or percentages will be displayed') [CompletionResult]::new('--reverse', '--reverse', [CompletionResultType]::ParameterName, 'Print tree upside down (biggest highest)')
[CompletionResult]::new('--no-percent-bars', 'no-percent-bars', [CompletionResultType]::ParameterName, 'No percent bars or percentages will be displayed') [CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'No colors will be printed (Useful for commands like: watch)')
[CompletionResult]::new('-B', 'B ', [CompletionResultType]::ParameterName, 'percent bars moved to right side of screen') [CompletionResult]::new('--no-colors', '--no-colors', [CompletionResultType]::ParameterName, 'No colors will be printed (Useful for commands like: watch)')
[CompletionResult]::new('--bars-on-right', 'bars-on-right', [CompletionResultType]::ParameterName, 'percent bars moved to right side of screen') [CompletionResult]::new('-C', '-C ', [CompletionResultType]::ParameterName, 'Force colors print')
[CompletionResult]::new('-R', 'R ', [CompletionResultType]::ParameterName, 'For screen readers. Removes bars. Adds new column: depth level (May want to use -p too for full path)') [CompletionResult]::new('--force-colors', '--force-colors', [CompletionResultType]::ParameterName, 'Force colors print')
[CompletionResult]::new('--screen-reader', 'screen-reader', [CompletionResultType]::ParameterName, 'For screen readers. Removes bars. Adds new column: depth level (May want to use -p too for full path)') [CompletionResult]::new('-b', '-b', [CompletionResultType]::ParameterName, 'No percent bars or percentages will be displayed')
[CompletionResult]::new('--skip-total', 'skip-total', [CompletionResultType]::ParameterName, 'No total row will be displayed') [CompletionResult]::new('--no-percent-bars', '--no-percent-bars', [CompletionResultType]::ParameterName, 'No percent bars or percentages will be displayed')
[CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Directory ''size'' is number of child files instead of disk size') [CompletionResult]::new('-B', '-B ', [CompletionResultType]::ParameterName, 'percent bars moved to right side of screen')
[CompletionResult]::new('--filecount', 'filecount', [CompletionResultType]::ParameterName, 'Directory ''size'' is number of child files instead of disk size') [CompletionResult]::new('--bars-on-right', '--bars-on-right', [CompletionResultType]::ParameterName, 'percent bars moved to right side of screen')
[CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'Do not display hidden files') [CompletionResult]::new('-R', '-R ', [CompletionResultType]::ParameterName, 'For screen readers. Removes bars. Adds new column: depth level (May want to use -p too for full path)')
[CompletionResult]::new('--ignore_hidden', 'ignore_hidden', [CompletionResultType]::ParameterName, 'Do not display hidden files') [CompletionResult]::new('--screen-reader', '--screen-reader', [CompletionResultType]::ParameterName, 'For screen readers. Removes bars. Adds new column: depth level (May want to use -p too for full path)')
[CompletionResult]::new('-t', 't', [CompletionResultType]::ParameterName, 'show only these file types') [CompletionResult]::new('--skip-total', '--skip-total', [CompletionResultType]::ParameterName, 'No total row will be displayed')
[CompletionResult]::new('--file_types', 'file_types', [CompletionResultType]::ParameterName, 'show only these file types') [CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Directory ''size'' is number of child files instead of disk size')
[CompletionResult]::new('-P', 'P ', [CompletionResultType]::ParameterName, 'Disable the progress indication.') [CompletionResult]::new('--filecount', '--filecount', [CompletionResultType]::ParameterName, 'Directory ''size'' is number of child files instead of disk size')
[CompletionResult]::new('--no-progress', 'no-progress', [CompletionResultType]::ParameterName, 'Disable the progress indication.') [CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'Do not display hidden files')
[CompletionResult]::new('--print-errors', 'print-errors', [CompletionResultType]::ParameterName, 'Print path with errors.') [CompletionResult]::new('--ignore-hidden', '--ignore-hidden', [CompletionResultType]::ParameterName, 'Do not display hidden files')
[CompletionResult]::new('-D', 'D ', [CompletionResultType]::ParameterName, 'Only directories will be displayed.') [CompletionResult]::new('-t', '-t', [CompletionResultType]::ParameterName, 'show only these file types')
[CompletionResult]::new('--only-dir', 'only-dir', [CompletionResultType]::ParameterName, 'Only directories will be displayed.') [CompletionResult]::new('--file-types', '--file-types', [CompletionResultType]::ParameterName, 'show only these file types')
[CompletionResult]::new('-F', 'F ', [CompletionResultType]::ParameterName, 'Only files will be displayed. (Finds your largest files)') [CompletionResult]::new('-P', '-P ', [CompletionResultType]::ParameterName, 'Disable the progress indication')
[CompletionResult]::new('--only-file', 'only-file', [CompletionResultType]::ParameterName, 'Only files will be displayed. (Finds your largest files)') [CompletionResult]::new('--no-progress', '--no-progress', [CompletionResultType]::ParameterName, 'Disable the progress indication')
[CompletionResult]::new('-j', 'j', [CompletionResultType]::ParameterName, 'Output the directory tree as json to the current directory') [CompletionResult]::new('--print-errors', '--print-errors', [CompletionResultType]::ParameterName, 'Print path with errors')
[CompletionResult]::new('--output-json', 'output-json', [CompletionResultType]::ParameterName, 'Output the directory tree as json to the current directory') [CompletionResult]::new('-D', '-D ', [CompletionResultType]::ParameterName, 'Only directories will be displayed')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('--only-dir', '--only-dir', [CompletionResultType]::ParameterName, 'Only directories will be displayed')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('-F', '-F ', [CompletionResultType]::ParameterName, 'Only files will be displayed. (Finds your largest files)')
[CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('--only-file', '--only-file', [CompletionResultType]::ParameterName, 'Only files will be displayed. (Finds your largest files)')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, 'Output the directory tree as json to the current directory')
[CompletionResult]::new('--output-json', '--output-json', [CompletionResultType]::ParameterName, 'Output the directory tree as json to the current directory')
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version')
break break
} }
}) })
+44 -9
View File
@@ -1,12 +1,16 @@
_dust() { _dust() {
local i cur prev opts cmd local i cur prev opts cmd
COMPREPLY=() COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}" if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
prev="${COMP_WORDS[COMP_CWORD-1]}" cur="$2"
else
cur="${COMP_WORDS[COMP_CWORD]}"
fi
prev="$3"
cmd="" cmd=""
opts="" opts=""
for i in ${COMP_WORDS[@]} for i in "${COMP_WORDS[@]:0:COMP_CWORD}"
do do
case "${cmd},${i}" in case "${cmd},${i}" in
",$1") ",$1")
@@ -19,7 +23,7 @@ _dust() {
case "${cmd}" in case "${cmd}" in
dust) dust)
opts="-d -T -n -p -X -I -L -x -s -r -c -C -b -B -z -R -f -i -v -e -t -w -P -D -F -o -S -j -M -A -y -h -V --depth --threads --number-of-lines --full-paths --ignore-directory --ignore-all-in-file --dereference-links --limit-filesystem --apparent-size --reverse --no-colors --force-colors --no-percent-bars --bars-on-right --min-size --screen-reader --skip-total --filecount --ignore_hidden --invert-filter --filter --file_types --terminal_width --no-progress --print-errors --only-dir --only-file --output-format --stack-size --output-json --mtime --atime --ctime --files0-from --help --version [PATH]..." opts="-d -T -n -p -X -I -L -x -s -r -c -C -b -B -z -R -f -i -v -e -t -w -P -D -F -o -S -j -M -A -y -m -h -V --depth --threads --config --number-of-lines --full-paths --ignore-directory --ignore-all-in-file --dereference-links --limit-filesystem --apparent-size --reverse --no-colors --force-colors --no-percent-bars --bars-on-right --min-size --screen-reader --skip-total --filecount --ignore-hidden --invert-filter --filter --file-types --terminal-width --no-progress --print-errors --only-dir --only-file --output-format --stack-size --output-json --mtime --atime --ctime --files0-from --files-from --collapse --filetime --help --version [PATH]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@@ -41,6 +45,21 @@ _dust() {
COMPREPLY=($(compgen -f "${cur}")) COMPREPLY=($(compgen -f "${cur}"))
return 0 return 0
;; ;;
--config)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
--number-of-lines) --number-of-lines)
COMPREPLY=($(compgen -f "${cur}")) COMPREPLY=($(compgen -f "${cur}"))
return 0 return 0
@@ -59,12 +78,12 @@ _dust() {
;; ;;
--ignore-all-in-file) --ignore-all-in-file)
local oldifs local oldifs
if [[ -v IFS ]]; then if [ -n "${IFS+x}" ]; then
oldifs="$IFS" oldifs="$IFS"
fi fi
IFS=$'\n' IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}")) COMPREPLY=($(compgen -f "${cur}"))
if [[ -v oldifs ]]; then if [ -n "${oldifs+x}" ]; then
IFS="$oldifs" IFS="$oldifs"
fi fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
@@ -74,12 +93,12 @@ _dust() {
;; ;;
-I) -I)
local oldifs local oldifs
if [[ -v IFS ]]; then if [ -n "${IFS+x}" ]; then
oldifs="$IFS" oldifs="$IFS"
fi fi
IFS=$'\n' IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}")) COMPREPLY=($(compgen -f "${cur}"))
if [[ -v oldifs ]]; then if [ -n "${oldifs+x}" ]; then
IFS="$oldifs" IFS="$oldifs"
fi fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
@@ -111,7 +130,7 @@ _dust() {
COMPREPLY=($(compgen -f "${cur}")) COMPREPLY=($(compgen -f "${cur}"))
return 0 return 0
;; ;;
--terminal_width) --terminal-width)
COMPREPLY=($(compgen -f "${cur}")) COMPREPLY=($(compgen -f "${cur}"))
return 0 return 0
;; ;;
@@ -163,6 +182,22 @@ _dust() {
COMPREPLY=($(compgen -f "${cur}")) COMPREPLY=($(compgen -f "${cur}"))
return 0 return 0
;; ;;
--files-from)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--collapse)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--filetime)
COMPREPLY=($(compgen -W "a c m" -- "${cur}"))
return 0
;;
-m)
COMPREPLY=($(compgen -W "a c m" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
+26 -21
View File
@@ -22,22 +22,23 @@ set edit:completion:arg-completer[dust] = {|@words|
cand --depth 'Depth to show' cand --depth 'Depth to show'
cand -T 'Number of threads to use' cand -T 'Number of threads to use'
cand --threads 'Number of threads to use' cand --threads 'Number of threads to use'
cand -n 'Number of lines of output to show. (Default is terminal_height - 10)' cand --config 'Specify a config file to use'
cand --number-of-lines 'Number of lines of output to show. (Default is terminal_height - 10)' cand -n 'Display the ''n'' largest entries. (Default is terminal_height)'
cand -X 'Exclude any file or directory with this name' cand --number-of-lines 'Display the ''n'' largest entries. (Default is terminal_height)'
cand --ignore-directory 'Exclude any file or directory with this name' cand -X 'Exclude any file or directory with this path'
cand --ignore-directory 'Exclude any file or directory with this path'
cand -I 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter' cand -I 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter'
cand --ignore-all-in-file 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter' cand --ignore-all-in-file 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter'
cand -z 'Minimum size file to include in output' cand -z 'Minimum size file to include in output'
cand --min-size 'Minimum size file to include in output' cand --min-size 'Minimum size file to include in output'
cand -v 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$" ' cand -v 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$"'
cand --invert-filter 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$" ' cand --invert-filter 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$"'
cand -e 'Only include filepaths matching this regex. For png files type: -e "\.png$" ' cand -e 'Only include filepaths matching this regex. For png files type: -e "\.png$"'
cand --filter 'Only include filepaths matching this regex. For png files type: -e "\.png$" ' cand --filter 'Only include filepaths matching this regex. For png files type: -e "\.png$"'
cand -w 'Specify width of output overriding the auto detection of terminal width' cand -w 'Specify width of output overriding the auto detection of terminal width'
cand --terminal_width 'Specify width of output overriding the auto detection of terminal width' cand --terminal-width 'Specify width of output overriding the auto detection of terminal width'
cand -o 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size.' cand -o 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size'
cand --output-format 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size.' cand --output-format 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size'
cand -S 'Specify memory to use as stack size - use if you see: ''fatal runtime error: stack overflow'' (default low memory=1048576, high memory=1073741824)' cand -S 'Specify memory to use as stack size - use if you see: ''fatal runtime error: stack overflow'' (default low memory=1048576, high memory=1073741824)'
cand --stack-size 'Specify memory to use as stack size - use if you see: ''fatal runtime error: stack overflow'' (default low memory=1048576, high memory=1073741824)' cand --stack-size 'Specify memory to use as stack size - use if you see: ''fatal runtime error: stack overflow'' (default low memory=1048576, high memory=1073741824)'
cand -M '+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => [curr(n+1), currn), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)' cand -M '+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => [curr(n+1), currn), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)'
@@ -46,7 +47,11 @@ set edit:completion:arg-completer[dust] = {|@words|
cand --atime 'just like -mtime, but based on file access time' cand --atime 'just like -mtime, but based on file access time'
cand -y 'just like -mtime, but based on file change time' cand -y 'just like -mtime, but based on file change time'
cand --ctime 'just like -mtime, but based on file change time' cand --ctime 'just like -mtime, but based on file change time'
cand --files0-from 'run dust on NUL-terminated file names specified in file; if argument is -, then read names from standard input' cand --files0-from 'Read NUL-terminated paths from FILE (use `-` for stdin)'
cand --files-from 'Read newline-terminated paths from FILE (use `-` for stdin)'
cand --collapse 'Keep these directories collapsed'
cand -m 'Directory ''size'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time'
cand --filetime 'Directory ''size'' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time'
cand -p 'Subdirectories will not have their path shortened' cand -p 'Subdirectories will not have their path shortened'
cand --full-paths 'Subdirectories will not have their path shortened' cand --full-paths 'Subdirectories will not have their path shortened'
cand -L 'dereference sym links - Treat sym links as directories and go into them' cand -L 'dereference sym links - Treat sym links as directories and go into them'
@@ -71,20 +76,20 @@ set edit:completion:arg-completer[dust] = {|@words|
cand -f 'Directory ''size'' is number of child files instead of disk size' cand -f 'Directory ''size'' is number of child files instead of disk size'
cand --filecount 'Directory ''size'' is number of child files instead of disk size' cand --filecount 'Directory ''size'' is number of child files instead of disk size'
cand -i 'Do not display hidden files' cand -i 'Do not display hidden files'
cand --ignore_hidden 'Do not display hidden files' cand --ignore-hidden 'Do not display hidden files'
cand -t 'show only these file types' cand -t 'show only these file types'
cand --file_types 'show only these file types' cand --file-types 'show only these file types'
cand -P 'Disable the progress indication.' cand -P 'Disable the progress indication'
cand --no-progress 'Disable the progress indication.' cand --no-progress 'Disable the progress indication'
cand --print-errors 'Print path with errors.' cand --print-errors 'Print path with errors'
cand -D 'Only directories will be displayed.' cand -D 'Only directories will be displayed'
cand --only-dir 'Only directories will be displayed.' cand --only-dir 'Only directories will be displayed'
cand -F 'Only files will be displayed. (Finds your largest files)' cand -F 'Only files will be displayed. (Finds your largest files)'
cand --only-file 'Only files will be displayed. (Finds your largest files)' cand --only-file 'Only files will be displayed. (Finds your largest files)'
cand -j 'Output the directory tree as json to the current directory' cand -j 'Output the directory tree as json to the current directory'
cand --output-json 'Output the directory tree as json to the current directory' cand --output-json 'Output the directory tree as json to the current directory'
cand -h 'Print help' cand -h 'Print help (see more with ''--help'')'
cand --help 'Print help' cand --help 'Print help (see more with ''--help'')'
cand -V 'Print version' cand -V 'Print version'
cand --version 'Print version' cand --version 'Print version'
} }
+28 -13
View File
@@ -1,18 +1,33 @@
complete -c dust -s d -l depth -d 'Depth to show' -r complete -c dust -s d -l depth -d 'Depth to show' -r
complete -c dust -s T -l threads -d 'Number of threads to use' -r complete -c dust -s T -l threads -d 'Number of threads to use' -r
complete -c dust -s n -l number-of-lines -d 'Number of lines of output to show. (Default is terminal_height - 10)' -r complete -c dust -l config -d 'Specify a config file to use' -r -F
complete -c dust -s X -l ignore-directory -d 'Exclude any file or directory with this name' -r -F complete -c dust -s n -l number-of-lines -d 'Display the \'n\' largest entries. (Default is terminal_height)' -r
complete -c dust -s X -l ignore-directory -d 'Exclude any file or directory with this path' -r -F
complete -c dust -s I -l ignore-all-in-file -d 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter' -r -F complete -c dust -s I -l ignore-all-in-file -d 'Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter' -r -F
complete -c dust -s z -l min-size -d 'Minimum size file to include in output' -r complete -c dust -s z -l min-size -d 'Minimum size file to include in output' -r
complete -c dust -s v -l invert-filter -d 'Exclude filepaths matching this regex. To ignore png files type: -v "\\.png$" ' -r complete -c dust -s v -l invert-filter -d 'Exclude filepaths matching this regex. To ignore png files type: -v "\\.png$"' -r
complete -c dust -s e -l filter -d 'Only include filepaths matching this regex. For png files type: -e "\\.png$" ' -r complete -c dust -s e -l filter -d 'Only include filepaths matching this regex. For png files type: -e "\\.png$"' -r
complete -c dust -s w -l terminal_width -d 'Specify width of output overriding the auto detection of terminal width' -r complete -c dust -s w -l terminal-width -d 'Specify width of output overriding the auto detection of terminal width' -r
complete -c dust -s o -l output-format -d 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size.' -r -f -a "{si '',b '',k '',m '',g '',t '',kb '',mb '',gb '',tb ''}" complete -c dust -s o -l output-format -d 'Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size' -r -f -a "si\t'SI prefix (powers of 1000)'
b\t'byte (B)'
k\t'kibibyte (KiB)'
m\t'mebibyte (MiB)'
g\t'gibibyte (GiB)'
t\t'tebibyte (TiB)'
kb\t'kilobyte (kB)'
mb\t'megabyte (MB)'
gb\t'gigabyte (GB)'
tb\t'terabyte (TB)'"
complete -c dust -s S -l stack-size -d 'Specify memory to use as stack size - use if you see: \'fatal runtime error: stack overflow\' (default low memory=1048576, high memory=1073741824)' -r complete -c dust -s S -l stack-size -d 'Specify memory to use as stack size - use if you see: \'fatal runtime error: stack overflow\' (default low memory=1048576, high memory=1073741824)' -r
complete -c dust -s M -l mtime -d '+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => [curr(n+1), currn), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)' -r complete -c dust -s M -l mtime -d '+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => [curr(n+1), currn), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)' -r
complete -c dust -s A -l atime -d 'just like -mtime, but based on file access time' -r complete -c dust -s A -l atime -d 'just like -mtime, but based on file access time' -r
complete -c dust -s y -l ctime -d 'just like -mtime, but based on file change time' -r complete -c dust -s y -l ctime -d 'just like -mtime, but based on file change time' -r
complete -c dust -l files0-from -d 'run dust on NUL-terminated file names specified in file; if argument is -, then read names from standard input' -r -F complete -c dust -l files0-from -d 'Read NUL-terminated paths from FILE (use `-` for stdin)' -r -F
complete -c dust -l files-from -d 'Read newline-terminated paths from FILE (use `-` for stdin)' -r -F
complete -c dust -l collapse -d 'Keep these directories collapsed' -r -F
complete -c dust -s m -l filetime -d 'Directory \'size\' is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time' -r -f -a "a\t'last accessed time'
c\t'last changed time'
m\t'last modified time'"
complete -c dust -s p -l full-paths -d 'Subdirectories will not have their path shortened' complete -c dust -s p -l full-paths -d 'Subdirectories will not have their path shortened'
complete -c dust -s L -l dereference-links -d 'dereference sym links - Treat sym links as directories and go into them' complete -c dust -s L -l dereference-links -d 'dereference sym links - Treat sym links as directories and go into them'
complete -c dust -s x -l limit-filesystem -d 'Only count the files and directories on the same filesystem as the supplied directory' complete -c dust -s x -l limit-filesystem -d 'Only count the files and directories on the same filesystem as the supplied directory'
@@ -25,12 +40,12 @@ complete -c dust -s B -l bars-on-right -d 'percent bars moved to right side of s
complete -c dust -s R -l screen-reader -d 'For screen readers. Removes bars. Adds new column: depth level (May want to use -p too for full path)' complete -c dust -s R -l screen-reader -d 'For screen readers. Removes bars. Adds new column: depth level (May want to use -p too for full path)'
complete -c dust -l skip-total -d 'No total row will be displayed' complete -c dust -l skip-total -d 'No total row will be displayed'
complete -c dust -s f -l filecount -d 'Directory \'size\' is number of child files instead of disk size' complete -c dust -s f -l filecount -d 'Directory \'size\' is number of child files instead of disk size'
complete -c dust -s i -l ignore_hidden -d 'Do not display hidden files' complete -c dust -s i -l ignore-hidden -d 'Do not display hidden files'
complete -c dust -s t -l file_types -d 'show only these file types' complete -c dust -s t -l file-types -d 'show only these file types'
complete -c dust -s P -l no-progress -d 'Disable the progress indication.' complete -c dust -s P -l no-progress -d 'Disable the progress indication'
complete -c dust -l print-errors -d 'Print path with errors.' complete -c dust -l print-errors -d 'Print path with errors'
complete -c dust -s D -l only-dir -d 'Only directories will be displayed.' complete -c dust -s D -l only-dir -d 'Only directories will be displayed'
complete -c dust -s F -l only-file -d 'Only files will be displayed. (Finds your largest files)' complete -c dust -s F -l only-file -d 'Only files will be displayed. (Finds your largest files)'
complete -c dust -s j -l output-json -d 'Output the directory tree as json to the current directory' complete -c dust -s j -l output-json -d 'Output the directory tree as json to the current directory'
complete -c dust -s h -l help -d 'Print help' complete -c dust -s h -l help -d 'Print help (see more with \'--help\')'
complete -c dust -s V -l version -d 'Print version' complete -c dust -s V -l version -d 'Print version'
+6 -1
View File
@@ -25,4 +25,9 @@ skip-total=true
ignore-hidden=true ignore-hidden=true
# print sizes in powers of 1000 (e.g., 1.1G) # print sizes in powers of 1000 (e.g., 1.1G)
iso=true output-format="si"
number-of-lines=5
# To keep the .git directory collapsed
collapse=[".git"]
Executable
+233
View File
@@ -0,0 +1,233 @@
#!/usr/bin/env bash
# dust installer script
# Usage: curl -sSfL https://raw.githubusercontent.com/bootandy/dust/main/install.sh | sh
set -e
REPO="bootandy/dust"
BINARY_NAME="dust"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1"
exit 1
}
# Detect OS
detect_os() {
case "$(uname -s)" in
Linux*) OS="linux" ;;
Darwin*) OS="darwin" ;;
MINGW*|MSYS*|CYGWIN*) OS="windows" ;;
*) error "Unsupported operating system: $(uname -s)" ;;
esac
}
# Detect architecture
detect_arch() {
ARCH=$(uname -m)
case "$ARCH" in
x86_64|amd64) ARCH="x86_64" ;;
aarch64|arm64) ARCH="aarch64" ;;
armv7l) ARCH="arm" ;;
i686|i386) ARCH="i686" ;;
*) error "Unsupported architecture: $ARCH" ;;
esac
}
# Get the latest release version
get_latest_version() {
info "Fetching latest version..."
# Try using curl
if command -v curl >/dev/null 2>&1; then
VERSION=$(curl -sSf "https://api.github.com/repos/$REPO/releases/latest" | grep '"tag_name"' | sed -E 's/.*"v?([^"]+)".*/\1/')
# Try using wget
elif command -v wget >/dev/null 2>&1; then
VERSION=$(wget -qO- "https://api.github.com/repos/$REPO/releases/latest" | grep '"tag_name"' | sed -E 's/.*"v?([^"]+)".*/\1/')
else
error "Neither curl nor wget is available. Please install one of them."
fi
if [ -z "$VERSION" ]; then
error "Failed to fetch latest version"
fi
info "Latest version: v$VERSION"
}
# Determine target triple
get_target() {
if [ "$OS" = "linux" ]; then
if [ "$ARCH" = "x86_64" ]; then
TARGET="x86_64-unknown-linux-musl"
elif [ "$ARCH" = "aarch64" ]; then
TARGET="aarch64-unknown-linux-musl"
elif [ "$ARCH" = "arm" ]; then
TARGET="arm-unknown-linux-musleabi"
elif [ "$ARCH" = "i686" ]; then
TARGET="i686-unknown-linux-musl"
else
error "Unsupported Linux architecture: $ARCH"
fi
elif [ "$OS" = "darwin" ]; then
if [ "$ARCH" = "x86_64" ]; then
TARGET="x86_64-apple-darwin"
elif [ "$ARCH" = "aarch64" ]; then
# For Apple Silicon, use x86_64 with Rosetta if native build not available
TARGET="x86_64-apple-darwin"
warn "Using x86_64 binary (will run via Rosetta 2 on Apple Silicon)"
else
error "Unsupported macOS architecture: $ARCH"
fi
elif [ "$OS" = "windows" ]; then
if [ "$ARCH" = "x86_64" ]; then
TARGET="x86_64-pc-windows-msvc"
elif [ "$ARCH" = "i686" ]; then
TARGET="i686-pc-windows-msvc"
else
error "Unsupported Windows architecture: $ARCH"
fi
else
error "Unsupported OS: $OS"
fi
info "Target platform: $TARGET"
}
# Download and extract
download_and_install() {
# Construct download URL
if [ "$OS" = "windows" ]; then
ARCHIVE_NAME="dust-v${VERSION}-${TARGET}.zip"
ARCHIVE_EXT="zip"
else
ARCHIVE_NAME="dust-v${VERSION}-${TARGET}.tar.gz"
ARCHIVE_EXT="tar.gz"
fi
DOWNLOAD_URL="https://github.com/$REPO/releases/download/v${VERSION}/${ARCHIVE_NAME}"
info "Downloading from: $DOWNLOAD_URL"
# Create temporary directory
TMP_DIR=$(mktemp -d)
cd "$TMP_DIR"
# Download
if command -v curl >/dev/null 2>&1; then
curl -sSfL "$DOWNLOAD_URL" -o "$ARCHIVE_NAME" || error "Download failed"
elif command -v wget >/dev/null 2>&1; then
wget -q "$DOWNLOAD_URL" -O "$ARCHIVE_NAME" || error "Download failed"
fi
# Extract
info "Extracting archive..."
if [ "$ARCHIVE_EXT" = "tar.gz" ]; then
tar -xzf "$ARCHIVE_NAME" || error "Extraction failed"
elif [ "$ARCHIVE_EXT" = "zip" ]; then
unzip -q "$ARCHIVE_NAME" || error "Extraction failed"
fi
# Find the binary (it might be in a subdirectory)
if [ "$OS" = "windows" ]; then
BINARY_PATH=$(find . -name "${BINARY_NAME}.exe" | head -n 1)
else
BINARY_PATH=$(find . -name "$BINARY_NAME" -type f | head -n 1)
fi
if [ -z "$BINARY_PATH" ]; then
error "Binary not found in archive"
fi
# Determine installation directory
if [ -n "$DUST_INSTALL" ]; then
INSTALL_DIR="$DUST_INSTALL"
elif [ -w "/usr/local/bin" ]; then
INSTALL_DIR="/usr/local/bin"
elif [ -w "$HOME/.local/bin" ]; then
INSTALL_DIR="$HOME/.local/bin"
mkdir -p "$INSTALL_DIR"
else
INSTALL_DIR="$HOME/.local/bin"
mkdir -p "$INSTALL_DIR"
fi
# Install binary
info "Installing to $INSTALL_DIR..."
if [ -w "$INSTALL_DIR" ]; then
cp "$BINARY_PATH" "$INSTALL_DIR/" || error "Installation failed"
chmod +x "$INSTALL_DIR/$BINARY_NAME" || true
else
# Try with sudo
warn "Installing with sudo (requires administrator privileges)..."
sudo cp "$BINARY_PATH" "$INSTALL_DIR/" || error "Installation failed"
sudo chmod +x "$INSTALL_DIR/$BINARY_NAME" || true
fi
# Clean up
cd - > /dev/null
rm -rf "$TMP_DIR"
info "${GREEN}${NC} dust v$VERSION installed successfully!"
# Check if install directory is in PATH
case ":$PATH:" in
*:$INSTALL_DIR:*)
;;
*)
warn "⚠️ $INSTALL_DIR is not in your PATH"
warn " Add the following to your shell config (~/.bashrc, ~/.zshrc, etc.):"
echo ""
echo " export PATH=\"$INSTALL_DIR:\$PATH\""
echo ""
;;
esac
# Show version
if command -v "$BINARY_NAME" >/dev/null 2>&1; then
info "Version check:"
"$BINARY_NAME" --version || true
fi
}
# Main execution
main() {
info "dust installer"
echo ""
# Check for required tools
if ! command -v tar >/dev/null 2>&1 && ! command -v unzip >/dev/null 2>&1; then
error "Neither tar nor unzip is available. Please install one of them."
fi
detect_os
detect_arch
get_latest_version
get_target
download_and_install
echo ""
info "Installation complete! Try running: ${GREEN}dust${NC}"
}
# Allow version to be specified via environment variable
if [ -n "$DUST_VERSION" ]; then
VERSION="$DUST_VERSION"
fi
main
+78 -32
View File
@@ -1,30 +1,33 @@
.ie \n(.g .ds Aq \(aq .ie \n(.g .ds Aq \(aq
.el .ds Aq ' .el .ds Aq '
.TH Dust 1 "Dust 1.0.0" .TH Dust 1 "Dust 1.2.4"
.SH NAME .SH NAME
Dust \- Like du but more intuitive Dust \- Like du but more intuitive
.SH SYNOPSIS .SH SYNOPSIS
\fBdust\fR [\fB\-d\fR|\fB\-\-depth\fR] [\fB\-T\fR|\fB\-\-threads\fR] [\fB\-n\fR|\fB\-\-number\-of\-lines\fR] [\fB\-p\fR|\fB\-\-full\-paths\fR] [\fB\-X\fR|\fB\-\-ignore\-directory\fR] [\fB\-I\fR|\fB\-\-ignore\-all\-in\-file\fR] [\fB\-L\fR|\fB\-\-dereference\-links\fR] [\fB\-x\fR|\fB\-\-limit\-filesystem\fR] [\fB\-s\fR|\fB\-\-apparent\-size\fR] [\fB\-r\fR|\fB\-\-reverse\fR] [\fB\-c\fR|\fB\-\-no\-colors\fR] [\fB\-C\fR|\fB\-\-force\-colors\fR] [\fB\-b\fR|\fB\-\-no\-percent\-bars\fR] [\fB\-B\fR|\fB\-\-bars\-on\-right\fR] [\fB\-z\fR|\fB\-\-min\-size\fR] [\fB\-R\fR|\fB\-\-screen\-reader\fR] [\fB\-\-skip\-total\fR] [\fB\-f\fR|\fB\-\-filecount\fR] [\fB\-i\fR|\fB\-\-ignore_hidden\fR] [\fB\-v\fR|\fB\-\-invert\-filter\fR] [\fB\-e\fR|\fB\-\-filter\fR] [\fB\-t\fR|\fB\-\-file_types\fR] [\fB\-w\fR|\fB\-\-terminal_width\fR] [\fB\-P\fR|\fB\-\-no\-progress\fR] [\fB\-\-print\-errors\fR] [\fB\-D\fR|\fB\-\-only\-dir\fR] [\fB\-F\fR|\fB\-\-only\-file\fR] [\fB\-o\fR|\fB\-\-output\-format\fR] [\fB\-S\fR|\fB\-\-stack\-size\fR] [\fB\-j\fR|\fB\-\-output\-json\fR] [\fB\-M\fR|\fB\-\-mtime\fR] [\fB\-A\fR|\fB\-\-atime\fR] [\fB\-y\fR|\fB\-\-ctime\fR] [\fB\-\-files0\-from\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIPATH\fR] \fBdust\fR [\fB\-d\fR|\fB\-\-depth\fR] [\fB\-T\fR|\fB\-\-threads\fR] [\fB\-\-config\fR] [\fB\-n\fR|\fB\-\-number\-of\-lines\fR] [\fB\-p\fR|\fB\-\-full\-paths\fR] [\fB\-X\fR|\fB\-\-ignore\-directory\fR] [\fB\-I\fR|\fB\-\-ignore\-all\-in\-file\fR] [\fB\-L\fR|\fB\-\-dereference\-links\fR] [\fB\-x\fR|\fB\-\-limit\-filesystem\fR] [\fB\-s\fR|\fB\-\-apparent\-size\fR] [\fB\-r\fR|\fB\-\-reverse\fR] [\fB\-c\fR|\fB\-\-no\-colors\fR] [\fB\-C\fR|\fB\-\-force\-colors\fR] [\fB\-b\fR|\fB\-\-no\-percent\-bars\fR] [\fB\-B\fR|\fB\-\-bars\-on\-right\fR] [\fB\-z\fR|\fB\-\-min\-size\fR] [\fB\-R\fR|\fB\-\-screen\-reader\fR] [\fB\-\-skip\-total\fR] [\fB\-f\fR|\fB\-\-filecount\fR] [\fB\-i\fR|\fB\-\-ignore\-hidden\fR] [\fB\-v\fR|\fB\-\-invert\-filter\fR] [\fB\-e\fR|\fB\-\-filter\fR] [\fB\-t\fR|\fB\-\-file\-types\fR] [\fB\-w\fR|\fB\-\-terminal\-width\fR] [\fB\-P\fR|\fB\-\-no\-progress\fR] [\fB\-\-print\-errors\fR] [\fB\-D\fR|\fB\-\-only\-dir\fR] [\fB\-F\fR|\fB\-\-only\-file\fR] [\fB\-o\fR|\fB\-\-output\-format\fR] [\fB\-S\fR|\fB\-\-stack\-size\fR] [\fB\-j\fR|\fB\-\-output\-json\fR] [\fB\-M\fR|\fB\-\-mtime\fR] [\fB\-A\fR|\fB\-\-atime\fR] [\fB\-y\fR|\fB\-\-ctime\fR] [\fB\-\-files0\-from\fR] [\fB\-\-files\-from\fR] [\fB\-\-collapse\fR] [\fB\-m\fR|\fB\-\-filetime\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIPATH\fR]
.SH DESCRIPTION .SH DESCRIPTION
Like du but more intuitive Like du but more intuitive
.SH OPTIONS .SH OPTIONS
.TP .TP
\fB\-d\fR, \fB\-\-depth\fR=\fIDEPTH\fR \fB\-d\fR, \fB\-\-depth\fR \fI<DEPTH>\fR
Depth to show Depth to show
.TP .TP
\fB\-T\fR, \fB\-\-threads\fR \fB\-T\fR, \fB\-\-threads\fR \fI<THREADS>\fR
Number of threads to use Number of threads to use
.TP .TP
\fB\-n\fR, \fB\-\-number\-of\-lines\fR=\fINUMBER\fR \fB\-\-config\fR \fI<FILE>\fR
Number of lines of output to show. (Default is terminal_height \- 10) Specify a config file to use
.TP
\fB\-n\fR, \fB\-\-number\-of\-lines\fR \fI<NUMBER>\fR
Display the \*(Aqn\*(Aq largest entries. (Default is terminal_height)
.TP .TP
\fB\-p\fR, \fB\-\-full\-paths\fR \fB\-p\fR, \fB\-\-full\-paths\fR
Subdirectories will not have their path shortened Subdirectories will not have their path shortened
.TP .TP
\fB\-X\fR, \fB\-\-ignore\-directory\fR=\fIPATH\fR \fB\-X\fR, \fB\-\-ignore\-directory\fR \fI<PATH>\fR
Exclude any file or directory with this name Exclude any file or directory with this path
.TP .TP
\fB\-I\fR, \fB\-\-ignore\-all\-in\-file\fR=\fIFILE\fR \fB\-I\fR, \fB\-\-ignore\-all\-in\-file\fR \fI<FILE>\fR
Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by \-\-invert_filter Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by \-\-invert_filter
.TP .TP
\fB\-L\fR, \fB\-\-dereference\-links\fR \fB\-L\fR, \fB\-\-dereference\-links\fR
@@ -51,7 +54,7 @@ No percent bars or percentages will be displayed
\fB\-B\fR, \fB\-\-bars\-on\-right\fR \fB\-B\fR, \fB\-\-bars\-on\-right\fR
percent bars moved to right side of screen percent bars moved to right side of screen
.TP .TP
\fB\-z\fR, \fB\-\-min\-size\fR=\fIMIN_SIZE\fR \fB\-z\fR, \fB\-\-min\-size\fR \fI<MIN_SIZE>\fR
Minimum size file to include in output Minimum size file to include in output
.TP .TP
\fB\-R\fR, \fB\-\-screen\-reader\fR \fB\-R\fR, \fB\-\-screen\-reader\fR
@@ -63,65 +66,108 @@ No total row will be displayed
\fB\-f\fR, \fB\-\-filecount\fR \fB\-f\fR, \fB\-\-filecount\fR
Directory \*(Aqsize\*(Aq is number of child files instead of disk size Directory \*(Aqsize\*(Aq is number of child files instead of disk size
.TP .TP
\fB\-i\fR, \fB\-\-ignore_hidden\fR \fB\-i\fR, \fB\-\-ignore\-hidden\fR
Do not display hidden files Do not display hidden files
.TP .TP
\fB\-v\fR, \fB\-\-invert\-filter\fR=\fIREGEX\fR \fB\-v\fR, \fB\-\-invert\-filter\fR \fI<REGEX>\fR
Exclude filepaths matching this regex. To ignore png files type: \-v "\\.png$" Exclude filepaths matching this regex. To ignore png files type: \-v "\\.png$"
.TP .TP
\fB\-e\fR, \fB\-\-filter\fR=\fIREGEX\fR \fB\-e\fR, \fB\-\-filter\fR \fI<REGEX>\fR
Only include filepaths matching this regex. For png files type: \-e "\\.png$" Only include filepaths matching this regex. For png files type: \-e "\\.png$"
.TP .TP
\fB\-t\fR, \fB\-\-file_types\fR \fB\-t\fR, \fB\-\-file\-types\fR
show only these file types show only these file types
.TP .TP
\fB\-w\fR, \fB\-\-terminal_width\fR=\fIWIDTH\fR \fB\-w\fR, \fB\-\-terminal\-width\fR \fI<WIDTH>\fR
Specify width of output overriding the auto detection of terminal width Specify width of output overriding the auto detection of terminal width
.TP .TP
\fB\-P\fR, \fB\-\-no\-progress\fR \fB\-P\fR, \fB\-\-no\-progress\fR
Disable the progress indication. Disable the progress indication
.TP .TP
\fB\-\-print\-errors\fR \fB\-\-print\-errors\fR
Print path with errors. Print path with errors
.TP .TP
\fB\-D\fR, \fB\-\-only\-dir\fR \fB\-D\fR, \fB\-\-only\-dir\fR
Only directories will be displayed. Only directories will be displayed
.TP .TP
\fB\-F\fR, \fB\-\-only\-file\fR \fB\-F\fR, \fB\-\-only\-file\fR
Only files will be displayed. (Finds your largest files) Only files will be displayed. (Finds your largest files)
.TP .TP
\fB\-o\fR, \fB\-\-output\-format\fR=\fIFORMAT\fR \fB\-o\fR, \fB\-\-output\-format\fR \fI<FORMAT>\fR
Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size. Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size
.br .br
.br .br
[\fIpossible values: \fRsi, b, k, m, g, t, kb, mb, gb, tb] \fIPossible values:\fR
.RS 14
.IP \(bu 2
si: SI prefix (powers of 1000)
.IP \(bu 2
b: byte (B)
.IP \(bu 2
k: kibibyte (KiB)
.IP \(bu 2
m: mebibyte (MiB)
.IP \(bu 2
g: gibibyte (GiB)
.IP \(bu 2
t: tebibyte (TiB)
.IP \(bu 2
kb: kilobyte (kB)
.IP \(bu 2
mb: megabyte (MB)
.IP \(bu 2
gb: gigabyte (GB)
.IP \(bu 2
tb: terabyte (TB)
.RE
.TP .TP
\fB\-S\fR, \fB\-\-stack\-size\fR=\fISTACK_SIZE\fR \fB\-S\fR, \fB\-\-stack\-size\fR \fI<STACK_SIZE>\fR
Specify memory to use as stack size \- use if you see: \*(Aqfatal runtime error: stack overflow\*(Aq (default low memory=1048576, high memory=1073741824) Specify memory to use as stack size \- use if you see: \*(Aqfatal runtime error: stack overflow\*(Aq (default low memory=1048576, high memory=1073741824)
.TP .TP
\fB\-j\fR, \fB\-\-output\-json\fR \fB\-j\fR, \fB\-\-output\-json\fR
Output the directory tree as json to the current directory Output the directory tree as json to the current directory
.TP .TP
\fB\-M\fR, \fB\-\-mtime\fR \fB\-M\fR, \fB\-\-mtime\fR \fI<MTIME>\fR
+/\-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => [curr(n+1), currn), and \-n => (𝑐𝑢𝑟𝑟−𝑛, +∞) +/\-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => [curr(n+1), currn), and \-n => (𝑐𝑢𝑟𝑟−𝑛, +∞)
.TP .TP
\fB\-A\fR, \fB\-\-atime\fR \fB\-A\fR, \fB\-\-atime\fR \fI<ATIME>\fR
just like \-mtime, but based on file access time just like \-mtime, but based on file access time
.TP .TP
\fB\-y\fR, \fB\-\-ctime\fR \fB\-y\fR, \fB\-\-ctime\fR \fI<CTIME>\fR
just like \-mtime, but based on file change time just like \-mtime, but based on file change time
.TP .TP
\fB\-\-files0\-from\fR \fB\-\-files0\-from\fR \fI<FILES0_FROM>\fR
run dust on NUL\-terminated file names specified in file; if argument is \-, then read names from standard input Read NUL\-terminated paths from FILE (use `\-` for stdin)
.TP
\fB\-\-files\-from\fR \fI<FILES_FROM>\fR
Read newline\-terminated paths from FILE (use `\-` for stdin)
.TP
\fB\-\-collapse\fR \fI<COLLAPSE>\fR
Keep these directories collapsed
.TP
\fB\-m\fR, \fB\-\-filetime\fR \fI<FILETIME>\fR
Directory \*(Aqsize\*(Aq is max filetime of child files instead of disk size. while a/c/m for last accessed/changed/modified time
.br
.br
\fIPossible values:\fR
.RS 14
.IP \(bu 2
a: last accessed time
.IP \(bu 2
c: last changed time
.IP \(bu 2
m: last modified time
.RE
.TP .TP
\fB\-h\fR, \fB\-\-help\fR \fB\-h\fR, \fB\-\-help\fR
Print help Print help (see a summary with \*(Aq\-h\*(Aq)
.TP .TP
\fB\-V\fR, \fB\-\-version\fR \fB\-V\fR, \fB\-\-version\fR
Print version Print version
.TP .TP
[\fIPATH\fR] [\fIPATH\fR]
Input files or directories
.SH VERSION .SH VERSION
v1.0.0 v1.2.4
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 107 KiB

+256 -292
View File
@@ -1,297 +1,261 @@
use clap::{builder::PossibleValue, value_parser, Arg, Command}; use std::fmt;
use clap::{Parser, ValueEnum, ValueHint};
// For single thread mode set this variable on your command line: // For single thread mode set this variable on your command line:
// export RAYON_NUM_THREADS=1 // export RAYON_NUM_THREADS=1
pub fn build_cli() -> Command { /// Like du but more intuitive
Command::new("Dust") #[derive(Debug, Parser)]
.about("Like du but more intuitive") #[command(name("Dust"), version)]
.version(env!("CARGO_PKG_VERSION")) pub struct Cli {
.arg( /// Depth to show
Arg::new("depth") #[arg(short, long)]
.short('d') pub depth: Option<usize>,
.long("depth")
.value_name("DEPTH") /// Number of threads to use
.value_parser(value_parser!(usize)) #[arg(short('T'), long)]
.help("Depth to show") pub threads: Option<usize>,
.num_args(1)
) /// Specify a config file to use
.arg( #[arg(long, value_name("FILE"), value_hint(ValueHint::FilePath))]
Arg::new("threads") pub config: Option<String>,
.short('T')
.long("threads") /// Display the 'n' largest entries. (Default is terminal_height)
.value_parser(value_parser!(usize)) #[arg(short, long, value_name("NUMBER"))]
.help("Number of threads to use") pub number_of_lines: Option<usize>,
.num_args(1)
) /// Subdirectories will not have their path shortened
.arg( #[arg(short('p'), long)]
Arg::new("number_of_lines") pub full_paths: bool,
.short('n')
.long("number-of-lines") /// Exclude any file or directory with this path
.value_name("NUMBER") #[arg(short('X'), long, value_name("PATH"), value_hint(ValueHint::AnyPath))]
.value_parser(value_parser!(usize)) pub ignore_directory: Option<Vec<String>>,
.help("Number of lines of output to show. (Default is terminal_height - 10)")
.num_args(1) /// Exclude any file or directory with a regex matching that listed in this
) /// file, the file entries will be added to the ignore regexs provided by
.arg( /// --invert_filter
Arg::new("display_full_paths") #[arg(short('I'), long, value_name("FILE"), value_hint(ValueHint::FilePath))]
.short('p') pub ignore_all_in_file: Option<String>,
.long("full-paths")
.action(clap::ArgAction::SetTrue) /// dereference sym links - Treat sym links as directories and go into them
.help("Subdirectories will not have their path shortened"), #[arg(short('L'), long)]
) pub dereference_links: bool,
.arg(
Arg::new("ignore_directory") /// Only count the files and directories on the same filesystem as the
.short('X') /// supplied directory
.long("ignore-directory") #[arg(short('x'), long)]
.value_name("PATH") pub limit_filesystem: bool,
.value_hint(clap::ValueHint::AnyPath)
.action(clap::ArgAction::Append) /// Use file length instead of blocks
.help("Exclude any file or directory with this name"), #[arg(short('s'), long)]
) pub apparent_size: bool,
.arg(
Arg::new("ignore_all_in_file") /// Print tree upside down (biggest highest)
.short('I') #[arg(short, long)]
.long("ignore-all-in-file") pub reverse: bool,
.value_name("FILE")
.value_hint(clap::ValueHint::FilePath) /// No colors will be printed (Useful for commands like: watch)
.value_parser(value_parser!(String)) #[arg(short('c'), long)]
.help("Exclude any file or directory with a regex matching that listed in this file, the file entries will be added to the ignore regexs provided by --invert_filter"), pub no_colors: bool,
)
.arg( /// Force colors print
Arg::new("dereference_links") #[arg(short('C'), long)]
.short('L') pub force_colors: bool,
.long("dereference-links")
.action(clap::ArgAction::SetTrue) /// No percent bars or percentages will be displayed
.help("dereference sym links - Treat sym links as directories and go into them"), #[arg(short('b'), long)]
) pub no_percent_bars: bool,
.arg(
Arg::new("limit_filesystem") /// percent bars moved to right side of screen
.short('x') #[arg(short('B'), long)]
.long("limit-filesystem") pub bars_on_right: bool,
.action(clap::ArgAction::SetTrue)
.help("Only count the files and directories on the same filesystem as the supplied directory"), /// Minimum size file to include in output
) #[arg(short('z'), long)]
.arg( pub min_size: Option<String>,
Arg::new("display_apparent_size")
.short('s') /// For screen readers. Removes bars. Adds new column: depth level (May want
.long("apparent-size") /// to use -p too for full path)
.action(clap::ArgAction::SetTrue) #[arg(short('R'), long)]
.help("Use file length instead of blocks"), pub screen_reader: bool,
)
.arg( /// No total row will be displayed
Arg::new("reverse") #[arg(long)]
.short('r') pub skip_total: bool,
.long("reverse")
.action(clap::ArgAction::SetTrue) /// Directory 'size' is number of child files instead of disk size
.help("Print tree upside down (biggest highest)"), #[arg(short, long)]
) pub filecount: bool,
.arg(
Arg::new("no_colors") /// Do not display hidden files
.short('c') // Do not use 'h' this is used by 'help'
.long("no-colors") #[arg(short, long)]
.action(clap::ArgAction::SetTrue) pub ignore_hidden: bool,
.help("No colors will be printed (Useful for commands like: watch)"),
) /// Exclude filepaths matching this regex. To ignore png files type: -v
.arg( /// "\.png$"
Arg::new("force_colors") #[arg(
.short('C') short('v'),
.long("force-colors") long,
.action(clap::ArgAction::SetTrue) value_name("REGEX"),
.help("Force colors print"), conflicts_with("filter"),
) conflicts_with("file_types")
.arg( )]
Arg::new("no_bars") pub invert_filter: Option<Vec<String>>,
.short('b')
.long("no-percent-bars") /// Only include filepaths matching this regex. For png files type: -e
.action(clap::ArgAction::SetTrue) /// "\.png$"
.help("No percent bars or percentages will be displayed"), #[arg(short('e'), long, value_name("REGEX"), conflicts_with("file_types"))]
) pub filter: Option<Vec<String>>,
.arg(
Arg::new("bars_on_right") /// show only these file types
.short('B') #[arg(short('t'), long, conflicts_with("depth"), conflicts_with("only_dir"))]
.long("bars-on-right") pub file_types: bool,
.action(clap::ArgAction::SetTrue)
.help("percent bars moved to right side of screen"), /// Specify width of output overriding the auto detection of terminal width
) #[arg(short('w'), long, value_name("WIDTH"))]
.arg( pub terminal_width: Option<usize>,
Arg::new("min_size")
.short('z') /// Disable the progress indication.
.long("min-size") #[arg(short('P'), long)]
.value_name("MIN_SIZE") pub no_progress: bool,
.num_args(1)
.help("Minimum size file to include in output"), /// Print path with errors.
) #[arg(long)]
.arg( pub print_errors: bool,
Arg::new("screen_reader")
.short('R') /// Only directories will be displayed.
.long("screen-reader") #[arg(
.action(clap::ArgAction::SetTrue) short('D'),
.help("For screen readers. Removes bars. Adds new column: depth level (May want to use -p too for full path)"), long,
) conflicts_with("only_file"),
.arg( conflicts_with("file_types")
Arg::new("skip_total") )]
.long("skip-total") pub only_dir: bool,
.action(clap::ArgAction::SetTrue)
.help("No total row will be displayed"), /// Only files will be displayed. (Finds your largest files)
) #[arg(short('F'), long, conflicts_with("only_dir"))]
.arg( pub only_file: bool,
Arg::new("by_filecount")
.short('f') /// Changes output display size. si will print sizes in powers of 1000. b k
.long("filecount") /// m g t kb mb gb tb will print the whole tree in that size.
.action(clap::ArgAction::SetTrue) #[arg(short, long, value_enum, value_name("FORMAT"), ignore_case(true))]
.help("Directory 'size' is number of child files instead of disk size"), pub output_format: Option<OutputFormat>,
)
.arg( /// Specify memory to use as stack size - use if you see: 'fatal runtime
Arg::new("ignore_hidden") /// error: stack overflow' (default low memory=1048576, high
.short('i') // Do not use 'h' this is used by 'help' /// memory=1073741824)
.long("ignore_hidden") #[arg(short('S'), long)]
.action(clap::ArgAction::SetTrue) pub stack_size: Option<usize>,
.help("Do not display hidden files"),
) /// Input files or directories.
.arg( #[arg(value_name("PATH"), value_hint(ValueHint::AnyPath))]
Arg::new("invert_filter") pub params: Option<Vec<String>>,
.short('v')
.long("invert-filter") /// Output the directory tree as json to the current directory
.value_name("REGEX") #[arg(short('j'), long)]
.action(clap::ArgAction::Append) pub output_json: bool,
.conflicts_with("filter")
.conflicts_with("types") /// +/-n matches files modified more/less than n days ago , and n matches
.help("Exclude filepaths matching this regex. To ignore png files type: -v \"\\.png$\" "), /// files modified exactly n days ago, days are rounded down.That is +n =>
) /// (−∞, curr(n+1)), n => [curr(n+1), currn), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)
.arg( #[arg(short('M'), long, allow_hyphen_values(true))]
Arg::new("filter") pub mtime: Option<String>,
.short('e')
.long("filter") /// just like -mtime, but based on file access time
.value_name("REGEX") #[arg(short('A'), long, allow_hyphen_values(true))]
.action(clap::ArgAction::Append) pub atime: Option<String>,
.conflicts_with("types")
.help("Only include filepaths matching this regex. For png files type: -e \"\\.png$\" "), /// just like -mtime, but based on file change time
) #[arg(short('y'), long, allow_hyphen_values(true))]
.arg( pub ctime: Option<String>,
Arg::new("types")
.short('t') /// Read NUL-terminated paths from FILE (use `-` for stdin).
.long("file_types") #[arg(long, value_hint(ValueHint::AnyPath), conflicts_with("files_from"))]
.conflicts_with("depth") pub files0_from: Option<String>,
.conflicts_with("only_dir")
.action(clap::ArgAction::SetTrue) /// Read newline-terminated paths from FILE (use `-` for stdin).
.help("show only these file types"), #[arg(long, value_hint(ValueHint::AnyPath), conflicts_with("files0_from"))]
) pub files_from: Option<String>,
.arg(
Arg::new("width") /// Keep these directories collapsed
.short('w') #[arg(long, value_hint(ValueHint::AnyPath))]
.long("terminal_width") pub collapse: Option<Vec<String>>,
.value_name("WIDTH")
.value_parser(value_parser!(usize)) /// Directory 'size' is max filetime of child files instead of disk size.
.num_args(1) /// while a/c/m for last accessed/changed/modified time
.help("Specify width of output overriding the auto detection of terminal width"), #[arg(short('m'), long, value_enum)]
) pub filetime: Option<FileTime>,
.arg( }
Arg::new("disable_progress")
.short('P') #[derive(Clone, Copy, Debug, ValueEnum)]
.long("no-progress") #[value(rename_all = "lower")]
.action(clap::ArgAction::SetTrue) pub enum OutputFormat {
.help("Disable the progress indication."), /// SI prefix (powers of 1000)
) SI,
.arg(
Arg::new("print_errors") /// byte (B)
.long("print-errors") B,
.action(clap::ArgAction::SetTrue)
.help("Print path with errors."), /// kibibyte (KiB)
) #[value(name = "k", alias("kib"))]
.arg( KiB,
Arg::new("only_dir")
.short('D') /// mebibyte (MiB)
.long("only-dir") #[value(name = "m", alias("mib"))]
.conflicts_with("only_file") MiB,
.conflicts_with("types")
.action(clap::ArgAction::SetTrue) /// gibibyte (GiB)
.help("Only directories will be displayed."), #[value(name = "g", alias("gib"))]
) GiB,
.arg(
Arg::new("only_file") /// tebibyte (TiB)
.short('F') #[value(name = "t", alias("tib"))]
.long("only-file") TiB,
.conflicts_with("only_dir")
.action(clap::ArgAction::SetTrue) /// kilobyte (kB)
.help("Only files will be displayed. (Finds your largest files)"), KB,
)
.arg( /// megabyte (MB)
Arg::new("output_format") MB,
.short('o')
.long("output-format") /// gigabyte (GB)
.value_name("FORMAT") GB,
.value_parser([
PossibleValue::new("si"), /// terabyte (TB)
PossibleValue::new("b"), TB,
PossibleValue::new("k").alias("kib"), }
PossibleValue::new("m").alias("mib"),
PossibleValue::new("g").alias("gib"), impl fmt::Display for OutputFormat {
PossibleValue::new("t").alias("tib"), fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
PossibleValue::new("kb"), match self {
PossibleValue::new("mb"), Self::SI => write!(f, "si"),
PossibleValue::new("gb"), Self::B => write!(f, "b"),
PossibleValue::new("tb"), Self::KiB => write!(f, "k"),
]) Self::MiB => write!(f, "m"),
.ignore_case(true) Self::GiB => write!(f, "g"),
.help("Changes output display size. si will print sizes in powers of 1000. b k m g t kb mb gb tb will print the whole tree in that size.") Self::TiB => write!(f, "t"),
) Self::KB => write!(f, "kb"),
.arg( Self::MB => write!(f, "mb"),
Arg::new("stack_size") Self::GB => write!(f, "gb"),
.short('S') Self::TB => write!(f, "tb"),
.long("stack-size") }
.value_name("STACK_SIZE") }
.value_parser(value_parser!(usize)) }
.num_args(1)
.help("Specify memory to use as stack size - use if you see: 'fatal runtime error: stack overflow' (default low memory=1048576, high memory=1073741824)"), #[derive(Clone, Copy, Debug, ValueEnum)]
) pub enum FileTime {
.arg( /// last accessed time
Arg::new("params") #[value(name = "a", alias("accessed"))]
.value_name("PATH") Accessed,
.value_hint(clap::ValueHint::AnyPath)
.value_parser(value_parser!(String)) /// last changed time
.num_args(1..) #[value(name = "c", alias("changed"))]
) Changed,
.arg(
Arg::new("output_json") /// last modified time
.short('j') #[value(name = "m", alias("modified"))]
.long("output-json") Modified,
.action(clap::ArgAction::SetTrue)
.help("Output the directory tree as json to the current directory"),
)
.arg(
Arg::new("mtime")
.short('M')
.long("mtime")
.num_args(1)
.allow_hyphen_values(true)
.value_parser(value_parser!(String))
.help("+/-n matches files modified more/less than n days ago , and n matches files modified exactly n days ago, days are rounded down.That is +n => (−∞, curr(n+1)), n => [curr(n+1), currn), and -n => (𝑐𝑢𝑟𝑟−𝑛, +∞)")
)
.arg(
Arg::new("atime")
.short('A')
.long("atime")
.num_args(1)
.allow_hyphen_values(true)
.value_parser(value_parser!(String))
.help("just like -mtime, but based on file access time")
)
.arg(
Arg::new("ctime")
.short('y')
.long("ctime")
.num_args(1)
.allow_hyphen_values(true)
.value_parser(value_parser!(String))
.help("just like -mtime, but based on file change time")
)
.arg(
Arg::new("files0_from")
.long("files0-from")
.value_hint(clap::ValueHint::AnyPath)
.value_parser(value_parser!(String))
.num_args(1)
.help("run dust on NUL-terminated file names specified in file; if argument is -, then read names from standard input"),
)
} }
+189 -90
View File
@@ -1,20 +1,19 @@
use crate::node::FileTime;
use chrono::{Local, TimeZone}; use chrono::{Local, TimeZone};
use clap::ArgMatches;
use config_file::FromConfigFile; use config_file::FromConfigFile;
use regex::Regex; use regex::Regex;
use serde::Deserialize; use serde::Deserialize;
use std::io::IsTerminal;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use crate::dir_walker::Operater; use crate::cli::Cli;
use crate::dir_walker::Operator;
use crate::display::get_number_format; use crate::display::get_number_format;
pub static DAY_SECONDS: i64 = 24 * 60 * 60; pub static DAY_SECONDS: i64 = 24 * 60 * 60;
#[derive(Deserialize, Default)] #[derive(Deserialize, Default)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct Config { pub struct Config {
pub display_full_paths: Option<bool>, pub display_full_paths: Option<bool>,
pub display_apparent_size: Option<bool>, pub display_apparent_size: Option<bool>,
@@ -37,68 +36,82 @@ pub struct Config {
pub output_json: Option<bool>, pub output_json: Option<bool>,
pub print_errors: Option<bool>, pub print_errors: Option<bool>,
pub files0_from: Option<String>, pub files0_from: Option<String>,
pub number_of_lines: Option<usize>,
pub files_from: Option<String>,
pub collapse: Option<Vec<String>>,
} }
impl Config { impl Config {
pub fn get_files_from(&self, options: &ArgMatches) -> Option<String> { pub fn get_files0_from(&self, options: &Cli) -> Option<String> {
let from_file = options.get_one::<String>("files0_from"); let from_file = &options.files0_from;
match from_file { match from_file {
None => self.files0_from.as_ref().map(|x| x.to_string()), None => self.files0_from.as_ref().map(|x| x.to_string()),
Some(x) => Some(x.to_string()), Some(x) => Some(x.to_string()),
} }
} }
pub fn get_no_colors(&self, options: &ArgMatches) -> bool {
Some(true) == self.no_colors || options.get_flag("no_colors") pub fn get_files_from(&self, options: &Cli) -> Option<String> {
let from_file = &options.files_from;
match from_file {
None => self.files_from.as_ref().map(|x| x.to_string()),
Some(x) => Some(x.to_string()),
}
} }
pub fn get_force_colors(&self, options: &ArgMatches) -> bool { pub fn get_no_colors(&self, options: &Cli) -> bool {
Some(true) == self.force_colors || options.get_flag("force_colors") Some(true) == self.no_colors || options.no_colors
} }
pub fn get_disable_progress(&self, options: &ArgMatches) -> bool { pub fn get_force_colors(&self, options: &Cli) -> bool {
Some(true) == self.disable_progress Some(true) == self.force_colors || options.force_colors
|| options.get_flag("disable_progress")
|| !std::io::stdout().is_terminal()
} }
pub fn get_apparent_size(&self, options: &ArgMatches) -> bool { pub fn get_disable_progress(&self, options: &Cli) -> bool {
Some(true) == self.display_apparent_size || options.get_flag("display_apparent_size") Some(true) == self.disable_progress || options.no_progress
} }
pub fn get_ignore_hidden(&self, options: &ArgMatches) -> bool { pub fn get_apparent_size(&self, options: &Cli) -> bool {
Some(true) == self.ignore_hidden || options.get_flag("ignore_hidden") Some(true) == self.display_apparent_size || options.apparent_size
} }
pub fn get_full_paths(&self, options: &ArgMatches) -> bool { pub fn get_ignore_hidden(&self, options: &Cli) -> bool {
Some(true) == self.display_full_paths || options.get_flag("display_full_paths") Some(true) == self.ignore_hidden || options.ignore_hidden
} }
pub fn get_reverse(&self, options: &ArgMatches) -> bool { pub fn get_full_paths(&self, options: &Cli) -> bool {
Some(true) == self.reverse || options.get_flag("reverse") Some(true) == self.display_full_paths || options.full_paths
} }
pub fn get_no_bars(&self, options: &ArgMatches) -> bool { pub fn get_reverse(&self, options: &Cli) -> bool {
Some(true) == self.no_bars || options.get_flag("no_bars") Some(true) == self.reverse || options.reverse
} }
pub fn get_output_format(&self, options: &ArgMatches) -> String { pub fn get_no_bars(&self, options: &Cli) -> bool {
let out_fmt = options.get_one::<String>("output_format"); Some(true) == self.no_bars || options.no_percent_bars
}
pub fn get_output_format(&self, options: &Cli) -> String {
let out_fmt = options.output_format;
(match out_fmt { (match out_fmt {
None => match &self.output_format { None => match &self.output_format {
None => "".to_string(), None => "".to_string(),
Some(x) => x.to_string(), Some(x) => x.to_string(),
}, },
Some(x) => x.into(), Some(x) => x.to_string(),
}) })
.to_lowercase() .to_lowercase()
} }
pub fn get_skip_total(&self, options: &ArgMatches) -> bool {
Some(true) == self.skip_total || options.get_flag("skip_total") pub fn get_filetime(&self, options: &Cli) -> Option<FileTime> {
options.filetime.map(FileTime::from)
} }
pub fn get_screen_reader(&self, options: &ArgMatches) -> bool {
Some(true) == self.screen_reader || options.get_flag("screen_reader") pub fn get_skip_total(&self, options: &Cli) -> bool {
Some(true) == self.skip_total || options.skip_total
} }
pub fn get_depth(&self, options: &ArgMatches) -> usize { pub fn get_screen_reader(&self, options: &Cli) -> bool {
if let Some(v) = options.get_one::<usize>("depth") { Some(true) == self.screen_reader || options.screen_reader
return *v; }
pub fn get_depth(&self, options: &Cli) -> usize {
if let Some(v) = options.depth {
return v;
} }
self.depth.unwrap_or(usize::MAX) self.depth.unwrap_or(usize::MAX)
} }
pub fn get_min_size(&self, options: &ArgMatches) -> Option<usize> { pub fn get_min_size(&self, options: &Cli) -> Option<usize> {
let size_from_param = options.get_one::<String>("min_size"); let size_from_param = options.min_size.as_ref();
self._get_min_size(size_from_param) self._get_min_size(size_from_param)
} }
fn _get_min_size(&self, min_size: Option<&String>) -> Option<usize> { fn _get_min_size(&self, min_size: Option<&String>) -> Option<usize> {
@@ -112,63 +125,71 @@ impl Config {
size_from_param size_from_param
} }
} }
pub fn get_only_dir(&self, options: &ArgMatches) -> bool { pub fn get_only_dir(&self, options: &Cli) -> bool {
Some(true) == self.only_dir || options.get_flag("only_dir") Some(true) == self.only_dir || options.only_dir
} }
pub fn get_print_errors(&self, options: &ArgMatches) -> bool { pub fn get_print_errors(&self, options: &Cli) -> bool {
Some(true) == self.print_errors || options.get_flag("print_errors") Some(true) == self.print_errors || options.print_errors
} }
pub fn get_only_file(&self, options: &ArgMatches) -> bool { pub fn get_only_file(&self, options: &Cli) -> bool {
Some(true) == self.only_file || options.get_flag("only_file") Some(true) == self.only_file || options.only_file
} }
pub fn get_bars_on_right(&self, options: &ArgMatches) -> bool { pub fn get_bars_on_right(&self, options: &Cli) -> bool {
Some(true) == self.bars_on_right || options.get_flag("bars_on_right") Some(true) == self.bars_on_right || options.bars_on_right
} }
pub fn get_custom_stack_size(&self, options: &ArgMatches) -> Option<usize> { pub fn get_custom_stack_size(&self, options: &Cli) -> Option<usize> {
let from_cmd_line = options.get_one::<usize>("stack_size"); let from_cmd_line = options.stack_size;
if from_cmd_line.is_none() { if from_cmd_line.is_none() {
self.stack_size self.stack_size
} else { } else {
from_cmd_line.copied() from_cmd_line
} }
} }
pub fn get_threads(&self, options: &ArgMatches) -> Option<usize> { pub fn get_threads(&self, options: &Cli) -> Option<usize> {
let from_cmd_line = options.get_one::<usize>("threads"); let from_cmd_line = options.threads;
if from_cmd_line.is_none() { if from_cmd_line.is_none() {
self.threads self.threads
} else { } else {
from_cmd_line.copied() from_cmd_line
} }
} }
pub fn get_output_json(&self, options: &ArgMatches) -> bool { pub fn get_output_json(&self, options: &Cli) -> bool {
Some(true) == self.output_json || options.get_flag("output_json") Some(true) == self.output_json || options.output_json
} }
pub fn get_modified_time_operator(&self, options: &ArgMatches) -> (Operater, i64) { pub fn get_number_of_lines(&self, options: &Cli) -> Option<usize> {
get_filter_time_operator( let from_cmd_line = options.number_of_lines;
options.get_one::<String>("mtime"), if from_cmd_line.is_none() {
get_current_date_epoch_seconds(), self.number_of_lines
) } else {
from_cmd_line
}
} }
pub fn get_accessed_time_operator(&self, options: &ArgMatches) -> (Operater, i64) { pub fn get_modified_time_operator(&self, options: &Cli) -> Option<(Operator, i64)> {
get_filter_time_operator( get_filter_time_operator(options.mtime.as_ref(), get_current_date_epoch_seconds())
options.get_one::<String>("atime"),
get_current_date_epoch_seconds(),
)
} }
pub fn get_created_time_operator(&self, options: &ArgMatches) -> (Operater, i64) { pub fn get_accessed_time_operator(&self, options: &Cli) -> Option<(Operator, i64)> {
get_filter_time_operator( get_filter_time_operator(options.atime.as_ref(), get_current_date_epoch_seconds())
options.get_one::<String>("ctime"), }
get_current_date_epoch_seconds(),
) pub fn get_changed_time_operator(&self, options: &Cli) -> Option<(Operator, i64)> {
get_filter_time_operator(options.ctime.as_ref(), get_current_date_epoch_seconds())
}
pub fn get_collapse(&self, options: &Cli) -> Option<Vec<String>> {
if self.collapse.is_none() {
options.collapse.clone()
} else {
self.collapse.clone()
}
} }
} }
fn get_current_date_epoch_seconds() -> i64 { fn get_current_date_epoch_seconds() -> i64 {
// calcurate current date epoch seconds // calculate current date epoch seconds
let now = Local::now(); let now = Local::now();
let current_date = now.date_naive(); let current_date = now.date_naive();
@@ -182,7 +203,7 @@ fn get_current_date_epoch_seconds() -> i64 {
fn get_filter_time_operator( fn get_filter_time_operator(
option_value: Option<&String>, option_value: Option<&String>,
current_date_epoch_seconds: i64, current_date_epoch_seconds: i64,
) -> (Operater, i64) { ) -> Option<(Operator, i64)> {
match option_value { match option_value {
Some(val) => { Some(val) => {
let time = current_date_epoch_seconds let time = current_date_epoch_seconds
@@ -192,12 +213,12 @@ fn get_filter_time_operator(
.abs() .abs()
* DAY_SECONDS; * DAY_SECONDS;
match val.chars().next().expect("Value should not be empty") { match val.chars().next().expect("Value should not be empty") {
'+' => (Operater::LessThan, time - DAY_SECONDS), '+' => Some((Operator::LessThan, time - DAY_SECONDS)),
'-' => (Operater::GreaterThan, time), '-' => Some((Operator::GreaterThan, time)),
_ => (Operater::Equal, time - DAY_SECONDS), _ => Some((Operator::Equal, time - DAY_SECONDS)),
} }
} }
None => (Operater::GreaterThan, 0), None => None,
} }
} }
@@ -216,7 +237,7 @@ fn convert_min_size(input: &str) -> Option<usize> {
match number_format { match number_format {
Some((multiple, _)) => Some(parsed_digits * (multiple as usize)), Some((multiple, _)) => Some(parsed_digits * (multiple as usize)),
None => { None => {
if letters.eq("") { if letters.is_empty() {
Some(parsed_digits) Some(parsed_digits)
} else { } else {
eprintln!("Ignoring invalid min-size: {input}"); eprintln!("Ignoring invalid min-size: {input}");
@@ -232,19 +253,36 @@ fn convert_min_size(input: &str) -> Option<usize> {
} }
} }
fn get_config_locations(base: &Path) -> Vec<PathBuf> { fn get_config_locations(base: PathBuf) -> Vec<PathBuf> {
vec![ vec![
base.join(".dust.toml"), base.join(".dust.toml"),
base.join(".config").join("dust").join("config.toml"), base.join(".config").join("dust").join("config.toml"),
] ]
} }
pub fn get_config() -> Config { pub fn get_config(conf_path: Option<&String>) -> Config {
if let Some(home) = directories::BaseDirs::new() { match conf_path {
for path in get_config_locations(home.home_dir()) { Some(path_str) => {
let path = Path::new(path_str);
if path.exists() { if path.exists() {
if let Ok(config) = Config::from_config_file(path) { match Config::from_config_file(path) {
return config; Ok(config) => return config,
Err(e) => {
eprintln!("Ignoring invalid config file '{}': {}", &path.display(), e)
}
}
} else {
eprintln!("Config file {:?} doesn't exists", &path.display());
}
}
None => {
if let Some(home) = std::env::home_dir() {
for path in get_config_locations(home) {
if path.exists()
&& let Ok(config) = Config::from_config_file(&path)
{
return config;
}
} }
} }
} }
@@ -259,7 +297,7 @@ mod tests {
#[allow(unused_imports)] #[allow(unused_imports)]
use super::*; use super::*;
use chrono::{Datelike, Timelike}; use chrono::{Datelike, Timelike};
use clap::{value_parser, Arg, ArgMatches, Command}; use clap::Parser;
#[test] #[test]
fn test_get_current_date_epoch_seconds() { fn test_get_current_date_epoch_seconds() {
@@ -328,14 +366,75 @@ mod tests {
assert_eq!(c.get_depth(&args), 5); assert_eq!(c.get_depth(&args), 5);
} }
fn get_args(args: Vec<&str>) -> ArgMatches { fn get_args(args: Vec<&str>) -> Cli {
Command::new("Dust") Cli::parse_from(args)
.arg( }
Arg::new("depth")
.long("depth") #[test]
.num_args(1) fn test_get_filetime() {
.value_parser(value_parser!(usize)), // No config and no flag.
) let c = Config::default();
.get_matches_from(args) let args = get_filetime_args(vec!["dust"]);
assert_eq!(c.get_filetime(&args), None);
// Config is not defined and flag is defined as access time
let c = Config::default();
let args = get_filetime_args(vec!["dust", "--filetime", "a"]);
assert_eq!(c.get_filetime(&args), Some(FileTime::Accessed));
let c = Config::default();
let args = get_filetime_args(vec!["dust", "--filetime", "accessed"]);
assert_eq!(c.get_filetime(&args), Some(FileTime::Accessed));
// Config is not defined and flag is defined as modified time
let c = Config::default();
let args = get_filetime_args(vec!["dust", "--filetime", "m"]);
assert_eq!(c.get_filetime(&args), Some(FileTime::Modified));
let c = Config::default();
let args = get_filetime_args(vec!["dust", "--filetime", "modified"]);
assert_eq!(c.get_filetime(&args), Some(FileTime::Modified));
// Config is not defined and flag is defined as changed time
let c = Config::default();
let args = get_filetime_args(vec!["dust", "--filetime", "c"]);
assert_eq!(c.get_filetime(&args), Some(FileTime::Changed));
let c = Config::default();
let args = get_filetime_args(vec!["dust", "--filetime", "changed"]);
assert_eq!(c.get_filetime(&args), Some(FileTime::Changed));
}
fn get_filetime_args(args: Vec<&str>) -> Cli {
Cli::parse_from(args)
}
#[test]
fn test_get_number_of_lines() {
// No config and no flag.
let c = Config::default();
let args = get_args(vec![]);
assert_eq!(c.get_number_of_lines(&args), None);
// Config is not defined and flag is defined.
let c = Config::default();
let args = get_args(vec!["dust", "--number-of-lines", "5"]);
assert_eq!(c.get_number_of_lines(&args), Some(5));
// Config is defined and flag is not defined.
let c = Config {
number_of_lines: Some(3),
..Default::default()
};
let args = get_args(vec![]);
assert_eq!(c.get_number_of_lines(&args), Some(3));
// Both config and flag are defined.
let c = Config {
number_of_lines: Some(3),
..Default::default()
};
let args = get_args(vec!["dust", "--number-of-lines", "5"]);
assert_eq!(c.get_number_of_lines(&args), Some(5));
} }
} }
+180 -79
View File
@@ -1,19 +1,21 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fs; use std::fs;
use std::io::Error;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
use crate::node::Node; use crate::node::Node;
use crate::progress::ORDERING;
use crate::progress::Operation; use crate::progress::Operation;
use crate::progress::PAtomicInfo; use crate::progress::PAtomicInfo;
use crate::progress::RuntimeErrors; use crate::progress::RuntimeErrors;
use crate::progress::ORDERING;
use crate::utils::is_filtered_out_due_to_file_time; use crate::utils::is_filtered_out_due_to_file_time;
use crate::utils::is_filtered_out_due_to_invert_regex; use crate::utils::is_filtered_out_due_to_invert_regex;
use crate::utils::is_filtered_out_due_to_regex; use crate::utils::is_filtered_out_due_to_regex;
use rayon::iter::ParallelBridge; use rayon::iter::ParallelBridge;
use rayon::prelude::ParallelIterator; use rayon::prelude::ParallelIterator;
use regex::Regex; use regex::Regex;
use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::collections::HashSet; use std::collections::HashSet;
@@ -21,10 +23,11 @@ use std::collections::HashSet;
use crate::node::build_node; use crate::node::build_node;
use std::fs::DirEntry; use std::fs::DirEntry;
use crate::node::FileTime;
use crate::platform::get_metadata; use crate::platform::get_metadata;
#[derive(Debug)] #[derive(Debug)]
pub enum Operater { pub enum Operator {
Equal = 0, Equal = 0,
LessThan = 1, LessThan = 1,
GreaterThan = 2, GreaterThan = 2,
@@ -35,11 +38,12 @@ pub struct WalkData<'a> {
pub filter_regex: &'a [Regex], pub filter_regex: &'a [Regex],
pub invert_filter_regex: &'a [Regex], pub invert_filter_regex: &'a [Regex],
pub allowed_filesystems: HashSet<u64>, pub allowed_filesystems: HashSet<u64>,
pub filter_modified_time: (Operater, i64), pub filter_modified_time: Option<(Operator, i64)>,
pub filter_accessed_time: (Operater, i64), pub filter_accessed_time: Option<(Operator, i64)>,
pub filter_changed_time: (Operater, i64), pub filter_changed_time: Option<(Operator, i64)>,
pub use_apparent_size: bool, pub use_apparent_size: bool,
pub by_filecount: bool, pub by_filecount: bool,
pub by_filetime: &'a Option<FileTime>,
pub ignore_hidden: bool, pub ignore_hidden: bool,
pub follow_links: bool, pub follow_links: bool,
pub progress_data: Arc<PAtomicInfo>, pub progress_data: Arc<PAtomicInfo>,
@@ -57,24 +61,19 @@ pub fn walk_it(dirs: HashSet<PathBuf>, walk_data: &WalkData) -> Vec<Node> {
prog_data.state.store(Operation::PREPARING, ORDERING); prog_data.state.store(Operation::PREPARING, ORDERING);
clean_inodes(node, &mut inodes, walk_data.use_apparent_size) clean_inodes(node, &mut inodes, walk_data)
}) })
.collect(); .collect();
top_level_nodes top_level_nodes
} }
// Remove files which have the same inode, we don't want to double count them. // Remove files which have the same inode, we don't want to double count them.
fn clean_inodes( fn clean_inodes(x: Node, inodes: &mut HashSet<(u64, u64)>, walk_data: &WalkData) -> Option<Node> {
x: Node, if !walk_data.use_apparent_size
inodes: &mut HashSet<(u64, u64)>, && let Some(id) = x.inode_device
use_apparent_size: bool, && !inodes.insert(id)
) -> Option<Node> { {
if !use_apparent_size { return None;
if let Some(id) = x.inode_device {
if !inodes.insert(id) {
return None;
}
}
} }
// Sort Nodes so iteration order is predictable // Sort Nodes so iteration order is predictable
@@ -82,12 +81,25 @@ fn clean_inodes(
tmp.sort_by(sort_by_inode); tmp.sort_by(sort_by_inode);
let new_children: Vec<_> = tmp let new_children: Vec<_> = tmp
.into_iter() .into_iter()
.filter_map(|c| clean_inodes(c, inodes, use_apparent_size)) .filter_map(|c| clean_inodes(c, inodes, walk_data))
.collect(); .collect();
let actual_size = if walk_data.by_filetime.is_some() {
// If by_filetime is Some, directory 'size' is the maximum filetime among child files instead of disk size
new_children
.iter()
.map(|c| c.size)
.chain(std::iter::once(x.size))
.max()
.unwrap_or(0)
} else {
// If by_filetime is None, directory 'size' is the sum of disk sizes or file counts of child files
x.size + new_children.iter().map(|c| c.size).sum::<u64>()
};
Some(Node { Some(Node {
name: x.name, name: x.name,
size: x.size + new_children.iter().map(|c| c.size).sum::<u64>(), size: actual_size,
children: new_children, children: new_children,
inode_device: x.inode_device, inode_device: x.inode_device,
depth: x.depth, depth: x.depth,
@@ -112,20 +124,50 @@ fn sort_by_inode(a: &Node, b: &Node) -> std::cmp::Ordering {
} }
} }
fn ignore_file(entry: &DirEntry, walk_data: &WalkData) -> bool { // Check if `path` is inside ignored directory
let is_dot_file = entry.file_name().to_str().unwrap_or("").starts_with('.'); fn is_ignored_path(path: &Path, walk_data: &WalkData) -> bool {
let is_ignored_path = walk_data.ignore_directories.contains(&entry.path()); if walk_data.ignore_directories.contains(path) {
return true;
}
let size_inode_device = get_metadata(entry.path(), false); // Entry is inside an ignored absolute path
if let Some((_size, Some((_id, dev)), (modified_time, accessed_time, changed_time))) = // Absolute paths should be canonicalized before being added to `WalkData.ignore_directories`
size_inode_device for ignored_path in walk_data.ignore_directories.iter() {
{ if !ignored_path.is_absolute() {
if !walk_data.allowed_filesystems.is_empty() continue;
}
let absolute_entry_path = std::fs::canonicalize(path).unwrap_or_default();
if absolute_entry_path.starts_with(ignored_path) {
return true;
}
}
false
}
fn ignore_file(entry: &DirEntry, walk_data: &WalkData) -> bool {
if is_ignored_path(&entry.path(), walk_data) {
return true;
}
let is_dot_file = entry.file_name().to_str().unwrap_or("").starts_with('.');
let follow_links = walk_data.follow_links && entry.file_type().is_ok_and(|ft| ft.is_symlink());
if !walk_data.allowed_filesystems.is_empty() {
let size_inode_device = get_metadata(entry.path(), false, follow_links);
if let Some((_size, Some((_id, dev)), _gunk)) = size_inode_device
&& !walk_data.allowed_filesystems.contains(&dev) && !walk_data.allowed_filesystems.contains(&dev)
{ {
return true; return true;
} }
if entry.path().is_file() }
if walk_data.filter_accessed_time.is_some()
|| walk_data.filter_modified_time.is_some()
|| walk_data.filter_changed_time.is_some()
{
let size_inode_device = get_metadata(entry.path(), false, follow_links);
if let Some((_, _, (modified_time, accessed_time, changed_time))) = size_inode_device
&& entry.path().is_file()
&& [ && [
(&walk_data.filter_modified_time, modified_time), (&walk_data.filter_modified_time, modified_time),
(&walk_data.filter_accessed_time, accessed_time), (&walk_data.filter_accessed_time, accessed_time),
@@ -155,17 +197,13 @@ fn ignore_file(entry: &DirEntry, walk_data: &WalkData) -> bool {
return true; return true;
} }
(is_dot_file && walk_data.ignore_hidden) || is_ignored_path is_dot_file && walk_data.ignore_hidden
} }
fn walk(dir: PathBuf, walk_data: &WalkData, depth: usize) -> Option<Node> { fn walk(dir: PathBuf, walk_data: &WalkData, depth: usize) -> Option<Node> {
let prog_data = &walk_data.progress_data; let prog_data = &walk_data.progress_data;
let errors = &walk_data.errors; let errors = &walk_data.errors;
if errors.lock().unwrap().abort {
return None;
}
let children = if dir.is_dir() { let children = if dir.is_dir() {
let read_dir = fs::read_dir(&dir); let read_dir = fs::read_dir(&dir);
match read_dir { match read_dir {
@@ -182,37 +220,36 @@ fn walk(dir: PathBuf, walk_data: &WalkData, depth: usize) -> Option<Node> {
// return walk(entry.path(), walk_data, depth) // return walk(entry.path(), walk_data, depth)
if !ignore_file(entry, walk_data) { if !ignore_file(entry, walk_data)
if let Ok(data) = entry.file_type() { && let Ok(data) = entry.file_type()
if data.is_dir() {
|| (walk_data.follow_links && data.is_symlink()) if data.is_dir()
{ || (walk_data.follow_links && data.is_symlink())
return walk(entry.path(), walk_data, depth + 1); {
} return walk(entry.path(), walk_data, depth + 1);
let node = build_node(
entry.path(),
vec![],
data.is_symlink(),
data.is_file(),
depth,
walk_data,
);
prog_data.num_files.fetch_add(1, ORDERING);
if let Some(ref file) = node {
prog_data
.total_file_size
.fetch_add(file.size, ORDERING);
}
return node;
} }
let node = build_node(
entry.path(),
vec![],
data.is_symlink(),
data.is_file(),
depth,
walk_data,
);
prog_data.num_files.fetch_add(1, ORDERING);
if let Some(ref file) = node {
prog_data.total_file_size.fetch_add(file.size, ORDERING);
}
return node;
} }
} }
Err(ref failed) => { Err(ref failed) => {
let mut editable_error = errors.lock().unwrap(); if handle_error_and_retry(failed, &dir, walk_data) {
editable_error.no_permissions.insert(failed.to_string()); return walk(dir.clone(), walk_data, depth);
}
} }
} }
None None
@@ -220,21 +257,11 @@ fn walk(dir: PathBuf, walk_data: &WalkData, depth: usize) -> Option<Node> {
.collect() .collect()
} }
Err(failed) => { Err(failed) => {
let mut editable_error = errors.lock().unwrap(); if handle_error_and_retry(&failed, &dir, walk_data) {
match failed.kind() { return walk(dir, walk_data, depth);
std::io::ErrorKind::PermissionDenied => { } else {
editable_error vec![]
.no_permissions
.insert(dir.to_string_lossy().into());
}
std::io::ErrorKind::NotFound => {
editable_error.file_not_found.insert(failed.to_string());
}
_ => {
editable_error.unknown_error.insert(failed.to_string());
}
} }
vec![]
} }
} }
} else { } else {
@@ -245,7 +272,48 @@ fn walk(dir: PathBuf, walk_data: &WalkData, depth: usize) -> Option<Node> {
} }
vec![] vec![]
}; };
build_node(dir, children, false, false, depth, walk_data) let is_symlink = if walk_data.follow_links {
match fs::symlink_metadata(&dir) {
Ok(metadata) => metadata.file_type().is_symlink(),
Err(_) => false,
}
} else {
false
};
build_node(dir, children, is_symlink, false, depth, walk_data)
}
fn handle_error_and_retry(failed: &Error, dir: &Path, walk_data: &WalkData) -> bool {
let mut editable_error = walk_data.errors.lock().unwrap();
match failed.kind() {
std::io::ErrorKind::PermissionDenied => {
editable_error
.no_permissions
.insert(dir.to_string_lossy().into());
}
std::io::ErrorKind::InvalidInput => {
editable_error
.no_permissions
.insert(dir.to_string_lossy().into());
}
std::io::ErrorKind::NotFound => {
editable_error.file_not_found.insert(failed.to_string());
}
std::io::ErrorKind::Interrupted => {
editable_error.interrupted_error += 1;
// This does happen on some systems. It was set to 3 but sometimes dust runs would exceed this
// However, if there is no limit this results in infinite retrys and dust never finishes
if editable_error.interrupted_error > 999 {
panic!("Multiple Interrupted Errors occurred while scanning filesystem. Aborting");
} else {
return true;
}
}
_ => {
editable_error.unknown_error.insert(failed.to_string());
}
}
false
} }
mod tests { mod tests {
@@ -264,17 +332,43 @@ mod tests {
} }
} }
#[cfg(test)]
fn create_walker<'a>(use_apparent_size: bool) -> WalkData<'a> {
use crate::PIndicator;
let indicator = PIndicator::build_me();
WalkData {
ignore_directories: HashSet::new(),
filter_regex: &[],
invert_filter_regex: &[],
allowed_filesystems: HashSet::new(),
filter_modified_time: Some((Operator::GreaterThan, 0)),
filter_accessed_time: Some((Operator::GreaterThan, 0)),
filter_changed_time: Some((Operator::GreaterThan, 0)),
use_apparent_size,
by_filecount: false,
by_filetime: &None,
ignore_hidden: false,
follow_links: false,
progress_data: indicator.data.clone(),
errors: Arc::new(Mutex::new(RuntimeErrors::default())),
}
}
#[test] #[test]
#[allow(clippy::redundant_clone)] #[allow(clippy::redundant_clone)]
fn test_should_ignore_file() { fn test_should_ignore_file() {
let mut inodes = HashSet::new(); let mut inodes = HashSet::new();
let n = create_node(); let n = create_node();
let walkdata = create_walker(false);
// First time we insert the node // First time we insert the node
assert_eq!(clean_inodes(n.clone(), &mut inodes, false), Some(n.clone())); assert_eq!(
clean_inodes(n.clone(), &mut inodes, &walkdata),
Some(n.clone())
);
// Second time is a duplicate - we ignore it // Second time is a duplicate - we ignore it
assert_eq!(clean_inodes(n.clone(), &mut inodes, false), None); assert_eq!(clean_inodes(n.clone(), &mut inodes, &walkdata), None);
} }
#[test] #[test]
@@ -282,10 +376,17 @@ mod tests {
fn test_should_not_ignore_files_if_using_apparent_size() { fn test_should_not_ignore_files_if_using_apparent_size() {
let mut inodes = HashSet::new(); let mut inodes = HashSet::new();
let n = create_node(); let n = create_node();
let walkdata = create_walker(true);
// If using apparent size we include Nodes, even if duplicate inodes // If using apparent size we include Nodes, even if duplicate inodes
assert_eq!(clean_inodes(n.clone(), &mut inodes, true), Some(n.clone())); assert_eq!(
assert_eq!(clean_inodes(n.clone(), &mut inodes, true), Some(n.clone())); clean_inodes(n.clone(), &mut inodes, &walkdata),
Some(n.clone())
);
assert_eq!(
clean_inodes(n.clone(), &mut inodes, &walkdata),
Some(n.clone())
);
} }
#[test] #[test]
+84 -29
View File
@@ -1,27 +1,31 @@
use crate::display_node::DisplayNode; use crate::display_node::DisplayNode;
use crate::node::FileTime;
use ansi_term::Colour::Red;
use lscolors::{LsColors, Style}; use lscolors::{LsColors, Style};
use nu_ansi_term::Color::Red;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use stfu8::encode_u8; use stfu8::encode_u8;
use chrono::{DateTime, Local, TimeZone, Utc};
use std::cmp::max; use std::cmp::max;
use std::cmp::min; use std::cmp::min;
use std::fs; use std::fs;
use std::iter::repeat; use std::iter::repeat_n;
use std::path::Path; use std::path::Path;
use thousands::Separable; use thousands::Separable;
pub static UNITS: [char; 4] = ['T', 'G', 'M', 'K']; pub static UNITS: [char; 5] = ['P', 'T', 'G', 'M', 'K'];
static BLOCKS: [char; 5] = ['█', '▓', '▒', '░', ' ']; static BLOCKS: [char; 5] = ['█', '▓', '▒', '░', ' '];
const FILETIME_SHOW_LENGTH: usize = 19;
pub struct InitialDisplayData { pub struct InitialDisplayData {
pub short_paths: bool, pub short_paths: bool,
pub is_reversed: bool, pub is_reversed: bool,
pub colors_on: bool, pub colors_on: bool,
pub by_filecount: bool, pub by_filecount: bool,
pub by_filetime: Option<FileTime>,
pub is_screen_reader: bool, pub is_screen_reader: bool,
pub output_format: String, pub output_format: String,
pub bars_on_right: bool, pub bars_on_right: bool,
@@ -67,11 +71,7 @@ impl DisplayData {
fn percent_size(&self, node: &DisplayNode) -> f32 { fn percent_size(&self, node: &DisplayNode) -> f32 {
let result = node.size as f32 / self.base_size as f32; let result = node.size as f32 / self.base_size as f32;
if result.is_normal() { if result.is_normal() { result } else { 0.0 }
result
} else {
0.0
}
} }
} }
@@ -125,22 +125,16 @@ impl DrawData<'_> {
pub fn draw_it( pub fn draw_it(
idd: InitialDisplayData, idd: InitialDisplayData,
root_node: &DisplayNode,
no_percent_bars: bool, no_percent_bars: bool,
terminal_width: usize, terminal_width: usize,
root_node: &DisplayNode,
skip_total: bool, skip_total: bool,
) { ) {
let biggest = match skip_total {
false => root_node,
true => root_node
.get_children_from_node(false)
.next()
.unwrap_or(root_node),
};
let num_chars_needed_on_left_most = if idd.by_filecount { let num_chars_needed_on_left_most = if idd.by_filecount {
let max_size = biggest.size; let max_size = root_node.size;
max_size.separate_with_commas().chars().count() max_size.separate_with_commas().chars().count()
} else if idd.by_filetime.is_some() {
FILETIME_SHOW_LENGTH
} else { } else {
find_biggest_size_str(root_node, &idd.output_format) find_biggest_size_str(root_node, &idd.output_format)
}; };
@@ -161,12 +155,12 @@ pub fn draw_it(
allowed_width - longest_string_length - 7 allowed_width - longest_string_length - 7
}; };
let first_size_bar = repeat(BLOCKS[0]).take(max_bar_length).collect(); let first_size_bar = repeat_n(BLOCKS[0], max_bar_length).collect();
let display_data = DisplayData { let display_data = DisplayData {
initial: idd, initial: idd,
num_chars_needed_on_left_most, num_chars_needed_on_left_most,
base_size: biggest.size, base_size: root_node.size,
longest_string_length, longest_string_length,
ls_colors: LsColors::from_env().unwrap_or_default(), ls_colors: LsColors::from_env().unwrap_or_default(),
}; };
@@ -275,7 +269,7 @@ fn clean_indentation_string(s: &str) -> String {
is is
} }
fn get_printable_name<P: AsRef<Path>>(dir_name: &P, short_paths: bool) -> String { pub fn get_printable_name<P: AsRef<Path>>(dir_name: &P, short_paths: bool) -> String {
let dir_name = dir_name.as_ref(); let dir_name = dir_name.as_ref();
let printable_name = { let printable_name = {
if short_paths { if short_paths {
@@ -304,12 +298,9 @@ fn pad_or_trim_filename(node: &DisplayNode, indent: &str, display_data: &Display
); );
// Add spaces after the filename so we can draw the % used bar chart. // Add spaces after the filename so we can draw the % used bar chart.
let name_and_padding = name name + " "
+ " " .repeat(display_data.longest_string_length - width)
.repeat(display_data.longest_string_length - width) .as_str()
.as_str();
name_and_padding
} }
fn maybe_trim_filename(name_in: String, indent: &str, display_data: &DisplayData) -> String { fn maybe_trim_filename(name_in: String, indent: &str, display_data: &DisplayData) -> String {
@@ -342,6 +333,8 @@ pub fn format_string(
if display_data.initial.is_screen_reader { if display_data.initial.is_screen_reader {
// if screen_reader then bars is 'depth' // if screen_reader then bars is 'depth'
format!("{pretty_name} {bars} {pretty_size}{percent}") format!("{pretty_name} {bars} {pretty_size}{percent}")
} else if display_data.initial.by_filetime.is_some() {
format!("{pretty_size} {indent}{pretty_name}")
} else { } else {
format!("{pretty_size} {indent} {pretty_name}{percent}") format!("{pretty_size} {indent} {pretty_name}{percent}")
} }
@@ -376,6 +369,8 @@ fn get_name_percent(
fn get_pretty_size(node: &DisplayNode, is_biggest: bool, display_data: &DisplayData) -> String { fn get_pretty_size(node: &DisplayNode, is_biggest: bool, display_data: &DisplayData) -> String {
let output = if display_data.initial.by_filecount { let output = if display_data.initial.by_filecount {
node.size.separate_with_commas() node.size.separate_with_commas()
} else if display_data.initial.by_filetime.is_some() {
get_pretty_file_modified_time(node.size as i64)
} else { } else {
human_readable_number(node.size, &display_data.initial.output_format) human_readable_number(node.size, &display_data.initial.output_format)
}; };
@@ -389,6 +384,14 @@ fn get_pretty_size(node: &DisplayNode, is_biggest: bool, display_data: &DisplayD
} }
} }
fn get_pretty_file_modified_time(timestamp: i64) -> String {
let datetime: DateTime<Utc> = Utc.timestamp_opt(timestamp, 0).unwrap();
let local_datetime = datetime.with_timezone(&Local);
local_datetime.format("%Y-%m-%dT%H:%M:%S").to_string()
}
fn get_pretty_name( fn get_pretty_name(
node: &DisplayNode, node: &DisplayNode,
name_and_padding: String, name_and_padding: String,
@@ -400,7 +403,7 @@ fn get_pretty_name(
.ls_colors .ls_colors
.style_for_path_with_metadata(&node.name, meta_result.as_ref().ok()); .style_for_path_with_metadata(&node.name, meta_result.as_ref().ok());
let ansi_style = directory_color let ansi_style = directory_color
.map(Style::to_ansi_term_style) .map(Style::to_nu_ansi_term_style)
.unwrap_or_default(); .unwrap_or_default();
let out = ansi_style.paint(name_and_padding); let out = ansi_style.paint(name_and_padding);
format!("{out}") format!("{out}")
@@ -436,6 +439,9 @@ pub fn get_number_format(output_str: &str) -> Option<(u64, char)> {
} }
pub fn human_readable_number(size: u64, output_str: &str) -> String { pub fn human_readable_number(size: u64, output_str: &str) -> String {
if output_str == "count" {
return size.to_string();
};
match get_number_format(output_str) { match get_number_format(output_str) {
Some((x, u)) => { Some((x, u)) => {
format!("{}{}", (size / x), u) format!("{}{}", (size / x), u)
@@ -469,6 +475,7 @@ mod tests {
is_reversed: false, is_reversed: false,
colors_on: false, colors_on: false,
by_filecount: false, by_filecount: false,
by_filetime: None,
is_screen_reader: false, is_screen_reader: false,
output_format: "".into(), output_format: "".into(),
bars_on_right: false, bars_on_right: false,
@@ -535,6 +542,13 @@ mod tests {
assert_eq!(s, "short 3 4.0K 100%"); assert_eq!(s, "short 3 4.0K 100%");
} }
#[test]
fn test_machine_readable_filecount() {
assert_eq!(human_readable_number(1, "count"), "1");
assert_eq!(human_readable_number(1000, "count"), "1000");
assert_eq!(human_readable_number(1024, "count"), "1024");
}
#[test] #[test]
fn test_human_readable_number() { fn test_human_readable_number() {
assert_eq!(human_readable_number(1, ""), "1B"); assert_eq!(human_readable_number(1, ""), "1B");
@@ -547,6 +561,14 @@ mod tests {
assert_eq!(human_readable_number(1024 * 1024 * 1024 - 1, ""), "1023M"); assert_eq!(human_readable_number(1024 * 1024 * 1024 - 1, ""), "1023M");
assert_eq!(human_readable_number(1024 * 1024 * 1024 * 20, ""), "20G"); assert_eq!(human_readable_number(1024 * 1024 * 1024 * 20, ""), "20G");
assert_eq!(human_readable_number(1024 * 1024 * 1024 * 1024, ""), "1.0T"); assert_eq!(human_readable_number(1024 * 1024 * 1024 * 1024, ""), "1.0T");
assert_eq!(
human_readable_number(1024 * 1024 * 1024 * 1024 * 234, ""),
"234T"
);
assert_eq!(
human_readable_number(1024 * 1024 * 1024 * 1024 * 1024, ""),
"1.0P"
);
} }
#[test] #[test]
@@ -579,7 +601,7 @@ mod tests {
size: 2_u64.pow(size), size: 2_u64.pow(size),
children: vec![], children: vec![],
}; };
let first_size_bar = repeat(BLOCKS[0]).take(13).collect(); let first_size_bar = repeat_n(BLOCKS[0], 13).collect();
let dd = DrawData { let dd = DrawData {
indent: "".into(), indent: "".into(),
percent_bar: first_size_bar, percent_bar: first_size_bar,
@@ -625,4 +647,37 @@ mod tests {
let bar = dd.generate_bar(&n, 5); let bar = dd.generate_bar(&n, 5);
assert_eq!(bar, "████▓▓▓▓▓▓▓▓▓"); assert_eq!(bar, "████▓▓▓▓▓▓▓▓▓");
} }
#[test]
fn test_get_pretty_file_modified_time() {
// Create a timestamp for 2023-07-12 00:00:00 in local time
let local_dt = Local.with_ymd_and_hms(2023, 7, 12, 0, 0, 0).unwrap();
let timestamp = local_dt.timestamp();
// Format expected output
let expected_output = local_dt.format("%Y-%m-%dT%H:%M:%S").to_string();
assert_eq!(get_pretty_file_modified_time(timestamp), expected_output);
// Test another timestamp
let local_dt = Local.with_ymd_and_hms(2020, 1, 1, 12, 0, 0).unwrap();
let timestamp = local_dt.timestamp();
let expected_output = local_dt.format("%Y-%m-%dT%H:%M:%S").to_string();
assert_eq!(get_pretty_file_modified_time(timestamp), expected_output);
// Test timestamp for epoch start (1970-01-01T00:00:00)
let local_dt = Local.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap();
let timestamp = local_dt.timestamp();
let expected_output = local_dt.format("%Y-%m-%dT%H:%M:%S").to_string();
assert_eq!(get_pretty_file_modified_time(timestamp), expected_output);
// Test a future timestamp
let local_dt = Local.with_ymd_and_hms(2030, 12, 25, 6, 30, 0).unwrap();
let timestamp = local_dt.timestamp();
let expected_output = local_dt.format("%Y-%m-%dT%H:%M:%S").to_string();
assert_eq!(get_pretty_file_modified_time(timestamp), expected_output);
}
} }
+33 -2
View File
@@ -1,8 +1,12 @@
use std::cell::RefCell;
use std::path::PathBuf; use std::path::PathBuf;
use serde::Serialize; use serde::ser::SerializeStruct;
use serde::{Serialize, Serializer};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize)] use crate::display::human_readable_number;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct DisplayNode { pub struct DisplayNode {
// Note: the order of fields in important here, for PartialEq and PartialOrd // Note: the order of fields in important here, for PartialEq and PartialOrd
pub size: u64, pub size: u64,
@@ -25,3 +29,30 @@ impl DisplayNode {
out out
} }
} }
// Only used for -j 'json' flag combined with -o 'output_type' flag
// Used to pass the output_type into the custom Serde serializer
thread_local! {
pub static OUTPUT_TYPE: RefCell<String> = const { RefCell::new(String::new()) };
}
/*
We need the custom Serialize incase someone uses the -o flag to pass a custom output type in
(show size in Mb / Gb etc).
Sadly this also necessitates a global variable OUTPUT_TYPE as we can not pass the output_type flag
into the serialize method
*/
impl Serialize for DisplayNode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let readable_size = OUTPUT_TYPE
.with(|output_type| human_readable_number(self.size, output_type.borrow().as_str()));
let mut state = serializer.serialize_struct("DisplayNode", 2)?;
state.serialize_field("size", &(readable_size))?;
state.serialize_field("name", &self.name)?;
state.serialize_field("children", &self.children)?;
state.end()
}
}
+100 -19
View File
@@ -1,7 +1,12 @@
use stfu8::encode_u8;
use crate::display::get_printable_name;
use crate::display_node::DisplayNode; use crate::display_node::DisplayNode;
use crate::node::FileTime;
use crate::node::Node; use crate::node::Node;
use std::collections::BinaryHeap; use std::collections::BinaryHeap;
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
@@ -12,40 +17,58 @@ pub struct AggregateData {
pub number_of_lines: usize, pub number_of_lines: usize,
pub depth: usize, pub depth: usize,
pub using_a_filter: bool, pub using_a_filter: bool,
pub short_paths: bool,
} }
pub fn get_biggest(top_level_nodes: Vec<Node>, display_data: AggregateData) -> Option<DisplayNode> { pub fn get_biggest(
if top_level_nodes.is_empty() { top_level_nodes: Vec<Node>,
// perhaps change this, bring back Error object? display_data: AggregateData,
return None; by_filetime: &Option<FileTime>,
} keep_collapsed: HashSet<PathBuf>,
) -> DisplayNode {
let mut heap = BinaryHeap::new(); let mut heap = BinaryHeap::new();
let number_top_level_nodes = top_level_nodes.len(); let number_top_level_nodes = top_level_nodes.len();
let root; let root;
if number_top_level_nodes > 1 { if number_top_level_nodes == 0 {
let size = top_level_nodes.iter().map(|node| node.size).sum(); root = total_node_builder(0, vec![])
root = Node { } else if number_top_level_nodes > 1 {
name: PathBuf::from("(total)"), let size = if by_filetime.is_some() {
size, top_level_nodes
children: top_level_nodes, .iter()
inode_device: None, .map(|node| node.size)
depth: 0, .max()
.unwrap_or(0)
} else {
top_level_nodes.iter().map(|node| node.size).sum()
}; };
// Always include the base nodes if we add a 'parent' (total) node
let nodes = handle_duplicate_top_level_names(top_level_nodes, display_data.short_paths);
root = total_node_builder(size, nodes);
heap = always_add_children(&display_data, &root, heap); heap = always_add_children(&display_data, &root, heap);
} else { } else {
root = top_level_nodes.into_iter().next().unwrap(); root = top_level_nodes.into_iter().next().unwrap();
heap = add_children(&display_data, &root, heap); heap = add_children(&display_data, &root, heap);
} }
Some(fill_remaining_lines(heap, &root, display_data)) fill_remaining_lines(heap, &root, display_data, keep_collapsed)
}
fn total_node_builder(size: u64, children: Vec<Node>) -> Node {
Node {
name: PathBuf::from("(total)"),
size,
children,
inode_device: None,
depth: 0,
}
} }
pub fn fill_remaining_lines<'a>( pub fn fill_remaining_lines<'a>(
mut heap: BinaryHeap<&'a Node>, mut heap: BinaryHeap<&'a Node>,
root: &'a Node, root: &'a Node,
display_data: AggregateData, display_data: AggregateData,
keep_collapsed: HashSet<PathBuf>,
) -> DisplayNode { ) -> DisplayNode {
let mut allowed_nodes = HashMap::new(); let mut allowed_nodes = HashMap::new();
@@ -53,10 +76,14 @@ pub fn fill_remaining_lines<'a>(
let line = heap.pop(); let line = heap.pop();
match line { match line {
Some(line) => { Some(line) => {
// If we are not doing only_file OR if we are doing
// only_file and it has no children (ie is a file not a dir)
if !display_data.only_file || line.children.is_empty() { if !display_data.only_file || line.children.is_empty() {
allowed_nodes.insert(line.name.as_path(), line); allowed_nodes.insert(line.name.as_path(), line);
} }
heap = add_children(&display_data, line, heap); if !keep_collapsed.contains(&line.name) {
heap = add_children(&display_data, line, heap);
}
} }
None => break, None => break,
} }
@@ -114,7 +141,7 @@ fn recursive_rebuilder(allowed_nodes: &HashMap<&Path, &Node>, current: &Node) ->
.map(|c| recursive_rebuilder(allowed_nodes, c)) .map(|c| recursive_rebuilder(allowed_nodes, c))
.collect(); .collect();
build_node(new_children, current) build_display_node(new_children, current)
} }
// Applies all allowed nodes as children to current node // Applies all allowed nodes as children to current node
@@ -127,10 +154,10 @@ fn flat_rebuilder(allowed_nodes: HashMap<&Path, &Node>, current: &Node) -> Displ
children: vec![], children: vec![],
}) })
.collect::<Vec<DisplayNode>>(); .collect::<Vec<DisplayNode>>();
build_node(new_children, current) build_display_node(new_children, current)
} }
fn build_node(mut new_children: Vec<DisplayNode>, current: &Node) -> DisplayNode { fn build_display_node(mut new_children: Vec<DisplayNode>, current: &Node) -> DisplayNode {
new_children.sort_by(|lhs, rhs| lhs.cmp(rhs).reverse()); new_children.sort_by(|lhs, rhs| lhs.cmp(rhs).reverse());
DisplayNode { DisplayNode {
name: current.name.clone(), name: current.name.clone(),
@@ -138,3 +165,57 @@ fn build_node(mut new_children: Vec<DisplayNode>, current: &Node) -> DisplayNode
children: new_children, children: new_children,
} }
} }
fn names_have_dup(top_level_nodes: &Vec<Node>) -> bool {
let mut stored = HashSet::new();
for node in top_level_nodes {
let name = get_printable_name(&node.name, true);
if stored.contains(&name) {
return true;
}
stored.insert(name);
}
false
}
fn handle_duplicate_top_level_names(top_level_nodes: Vec<Node>, short_paths: bool) -> Vec<Node> {
// If we have top level names that are the same - we need to tweak them:
if short_paths && names_have_dup(&top_level_nodes) {
let mut new_top_nodes = top_level_nodes.clone();
let mut dir_walk_up_count = 0;
while names_have_dup(&new_top_nodes) && dir_walk_up_count < 10 {
dir_walk_up_count += 1;
let mut newer = vec![];
for node in new_top_nodes.iter() {
let mut folders = node.name.iter().rev();
// Get parent folder (if second time round get grandparent and so on)
for _ in 0..dir_walk_up_count {
folders.next();
}
match folders.next() {
// Add (parent_name) to path of Node
Some(data) => {
let parent = encode_u8(data.as_encoded_bytes());
let current_node = node.name.display();
let n = Node {
name: PathBuf::from(format!("{current_node}({parent})")),
size: node.size,
children: node.children.clone(),
inode_device: node.inode_device,
depth: node.depth,
};
newer.push(n)
}
// Node does not have a parent
None => newer.push(node.clone()),
}
}
new_top_nodes = newer;
}
new_top_nodes
} else {
top_level_nodes
}
}
+21 -7
View File
@@ -1,4 +1,5 @@
use crate::display_node::DisplayNode; use crate::display_node::DisplayNode;
use crate::node::FileTime;
use crate::node::Node; use crate::node::Node;
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi::OsStr; use std::ffi::OsStr;
@@ -10,7 +11,11 @@ struct ExtensionNode<'a> {
extension: Option<&'a OsStr>, extension: Option<&'a OsStr>,
} }
pub fn get_all_file_types(top_level_nodes: &[Node], n: usize) -> Option<DisplayNode> { pub fn get_all_file_types(
top_level_nodes: &[Node],
n: usize,
by_filetime: &Option<FileTime>,
) -> DisplayNode {
let ext_nodes = { let ext_nodes = {
let mut extension_cumulative_sizes = HashMap::new(); let mut extension_cumulative_sizes = HashMap::new();
build_by_all_file_types(top_level_nodes, &mut extension_cumulative_sizes); build_by_all_file_types(top_level_nodes, &mut extension_cumulative_sizes);
@@ -44,20 +49,29 @@ pub fn get_all_file_types(top_level_nodes: &[Node], n: usize) -> Option<DisplayN
// ...then, aggregate the remaining nodes (if any) into a single "(others)" node // ...then, aggregate the remaining nodes (if any) into a single "(others)" node
if ext_nodes_iter.len() > 0 { if ext_nodes_iter.len() > 0 {
let actual_size = if by_filetime.is_some() {
ext_nodes_iter.map(|node| node.size).max().unwrap_or(0)
} else {
ext_nodes_iter.map(|node| node.size).sum()
};
displayed.push(DisplayNode { displayed.push(DisplayNode {
name: PathBuf::from("(others)"), name: PathBuf::from("(others)"),
size: ext_nodes_iter.map(|node| node.size).sum(), size: actual_size,
children: vec![], children: vec![],
}); });
} }
let result = DisplayNode { let actual_size: u64 = if by_filetime.is_some() {
name: PathBuf::from("(total)"), displayed.iter().map(|node| node.size).max().unwrap_or(0)
size: displayed.iter().map(|node| node.size).sum(), } else {
children: displayed, displayed.iter().map(|node| node.size).sum()
}; };
Some(result) DisplayNode {
name: PathBuf::from("(total)"),
size: actual_size,
children: displayed,
}
} }
fn build_by_all_file_types<'a>( fn build_by_all_file_types<'a>(
+237 -149
View File
@@ -10,9 +10,11 @@ mod platform;
mod progress; mod progress;
mod utils; mod utils;
use crate::cli::build_cli; use crate::cli::Cli;
use crate::config::Config;
use crate::display_node::DisplayNode;
use crate::progress::RuntimeErrors; use crate::progress::RuntimeErrors;
use clap::parser::ValuesRef; use clap::Parser;
use dir_walker::WalkData; use dir_walker::WalkData;
use display::InitialDisplayData; use display::InitialDisplayData;
use filter::AggregateData; use filter::AggregateData;
@@ -20,25 +22,26 @@ use progress::PIndicator;
use regex::Error; use regex::Error;
use std::collections::HashSet; use std::collections::HashSet;
use std::env; use std::env;
use std::fs::read_to_string; use std::fs::{read, read_to_string};
use std::io; use std::io;
use std::io::Read;
use std::panic; use std::panic;
use std::process; use std::process;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
use sysinfo::{System, SystemExt}; use sysinfo::System;
use utils::canonicalize_absolute_path;
use self::display::draw_it; use self::display::draw_it;
use config::get_config; use config::get_config;
use dir_walker::walk_it; use dir_walker::walk_it;
use display_node::OUTPUT_TYPE;
use filter::get_biggest; use filter::get_biggest;
use filter_type::get_all_file_types; use filter_type::get_all_file_types;
use regex::Regex; use regex::Regex;
use std::cmp::max; use std::cmp::max;
use std::path::PathBuf; use std::path::PathBuf;
use terminal_size::{terminal_size, Height, Width}; use terminal_size::{Height, Width, terminal_size};
use utils::get_filesystem_devices; use utils::get_filesystem_devices;
use utils::simplify_dir_names; use utils::simplify_dir_names;
@@ -66,7 +69,7 @@ fn should_init_color(no_color: bool, force_color: bool) -> bool {
{ {
// Required for windows 10 // Required for windows 10
// Fails to resolve for windows 8 so disable color // Fails to resolve for windows 8 so disable color
match ansi_term::enable_ansi_support() { match nu_ansi_term::enable_ansi_support() {
Ok(_) => true, Ok(_) => true,
Err(_) => { Err(_) => {
eprintln!("This version of Windows does not support ANSI colors"); eprintln!("This version of Windows does not support ANSI colors");
@@ -81,30 +84,27 @@ fn should_init_color(no_color: bool, force_color: bool) -> bool {
} }
fn get_height_of_terminal() -> usize { fn get_height_of_terminal() -> usize {
// Simplify once https://github.com/eminence/terminal-size/pull/41 is
// merged
terminal_size() terminal_size()
// Windows CI runners detect a terminal height of 0 // Windows CI runners detect a terminal height of 0
.map(|(_, Height(h))| max(h as usize, DEFAULT_NUMBER_OF_LINES)) .map(|(_, Height(h))| max(h.into(), DEFAULT_NUMBER_OF_LINES))
.unwrap_or(DEFAULT_NUMBER_OF_LINES) .unwrap_or(DEFAULT_NUMBER_OF_LINES)
- 10 - 10
} }
fn get_width_of_terminal() -> usize { fn get_width_of_terminal() -> usize {
// Simplify once https://github.com/eminence/terminal-size/pull/41 is
// merged
terminal_size() terminal_size()
.map(|(Width(w), _)| match cfg!(windows) { .map(|(Width(w), _)| match cfg!(windows) {
// Windows CI runners detect a very low terminal width // Windows CI runners detect a very low terminal width
true => max(w as usize, DEFAULT_TERMINAL_WIDTH), true => max(w.into(), DEFAULT_TERMINAL_WIDTH),
false => w as usize, false => w.into(),
}) })
.unwrap_or(DEFAULT_TERMINAL_WIDTH) .unwrap_or(DEFAULT_TERMINAL_WIDTH)
} }
fn get_regex_value(maybe_value: Option<ValuesRef<String>>) -> Vec<Regex> { fn get_regex_value(maybe_value: Option<&Vec<String>>) -> Vec<Regex> {
maybe_value maybe_value
.unwrap_or_default() .unwrap_or(&Vec::new())
.iter()
.map(|reg| { .map(|reg| {
Regex::new(reg).unwrap_or_else(|err| { Regex::new(reg).unwrap_or_else(|err| {
eprintln!("Ignoring bad value for regex {err:?}"); eprintln!("Ignoring bad value for regex {err:?}");
@@ -115,64 +115,37 @@ fn get_regex_value(maybe_value: Option<ValuesRef<String>>) -> Vec<Regex> {
} }
fn main() { fn main() {
let options = build_cli().get_matches(); let options = Cli::parse();
let config = get_config(); let config = get_config(options.config.as_ref());
let errors = RuntimeErrors::default(); let errors = RuntimeErrors::default();
let error_listen_for_ctrlc = Arc::new(Mutex::new(errors)); let error_listen_for_ctrlc = Arc::new(Mutex::new(errors));
let errors_for_rayon = error_listen_for_ctrlc.clone(); let errors_for_rayon = error_listen_for_ctrlc.clone();
let errors_final = error_listen_for_ctrlc.clone();
let is_in_listing = Arc::new(AtomicBool::new(false));
let cloned_is_in_listing = Arc::clone(&is_in_listing);
ctrlc::set_handler(move || { ctrlc::set_handler(move || {
error_listen_for_ctrlc.lock().unwrap().abort = true;
println!("\nAborting"); println!("\nAborting");
if cloned_is_in_listing.load(Ordering::Relaxed) { process::exit(1);
process::exit(1);
}
}) })
.expect("Error setting Ctrl-C handler"); .expect("Error setting Ctrl-C handler");
is_in_listing.store(true, Ordering::Relaxed); let target_dirs = if let Some(path) = config.get_files0_from(&options) {
let target_dirs = match config.get_files_from(&options) { read_paths_from_source(&path, true)
Some(path) => { } else if let Some(path) = config.get_files_from(&options) {
if path == "-" { read_paths_from_source(&path, false)
let mut targets_to_add = io::stdin() } else {
.lines() match options.params {
.map_while(Result::ok) Some(ref values) => values.clone(),
.collect::<Vec<String>>();
if targets_to_add.is_empty() {
eprintln!("No input provided, defaulting to current directory");
targets_to_add.push(".".to_owned());
}
targets_to_add
} else {
// read file
match read_to_string(path) {
Ok(file_content) => file_content.lines().map(|x| x.to_string()).collect(),
Err(e) => {
eprintln!("Error reading file: {e}");
vec![".".to_owned()]
}
}
}
}
None => match options.get_many::<String>("params") {
Some(values) => values.cloned().collect(),
None => vec![".".to_owned()], None => vec![".".to_owned()],
}, }
}; };
is_in_listing.store(false, Ordering::Relaxed);
let summarize_file_types = options.get_flag("types"); let summarize_file_types = options.file_types;
let filter_regexs = get_regex_value(options.get_many("filter")); let filter_regexs = get_regex_value(options.filter.as_ref());
let invert_filter_regexs = get_regex_value(options.get_many("invert_filter")); let invert_filter_regexs = get_regex_value(options.invert_filter.as_ref());
let terminal_width: usize = match options.get_one::<usize>("width") { let terminal_width: usize = match options.terminal_width {
Some(&val) => val, Some(val) => val,
None => get_width_of_terminal(), None => get_width_of_terminal(),
}; };
@@ -181,8 +154,8 @@ fn main() {
// If depth is set, then we set the default number_of_lines to be max // If depth is set, then we set the default number_of_lines to be max
// instead of screen height // instead of screen height
let number_of_lines = match options.get_one::<usize>("number_of_lines") { let number_of_lines = match config.get_number_of_lines(&options) {
Some(&val) => val, Some(val) => val,
None => { None => {
if depth != usize::MAX { if depth != usize::MAX {
usize::MAX usize::MAX
@@ -197,16 +170,17 @@ fn main() {
config.get_force_colors(&options), config.get_force_colors(&options),
); );
let ignore_directories = match options.get_many::<String>("ignore_directory") { let ignore_directories = match options.ignore_directory {
Some(values) => values Some(ref values) => values
.map(|v| v.as_str()) .iter()
.map(PathBuf::from) .map(PathBuf::from)
.map(canonicalize_absolute_path)
.collect::<Vec<PathBuf>>(), .collect::<Vec<PathBuf>>(),
None => vec![], None => vec![],
}; };
let ignore_from_file_result = match options.get_one::<String>("ignore_all_in_file") { let ignore_from_file_result = match options.ignore_all_in_file {
Some(val) => read_to_string(val) Some(ref val) => read_to_string(val)
.unwrap() .unwrap()
.lines() .lines()
.map(Regex::new) .map(Regex::new)
@@ -223,13 +197,17 @@ fn main() {
.chain(ignore_from_file) .chain(ignore_from_file)
.collect::<Vec<Regex>>(); .collect::<Vec<Regex>>();
let by_filecount = options.get_flag("by_filecount"); let by_filecount = options.filecount;
let limit_filesystem = options.get_flag("limit_filesystem"); let by_filetime = config.get_filetime(&options);
let follow_links = options.get_flag("dereference_links"); let limit_filesystem = options.limit_filesystem;
let follow_links = options.dereference_links;
let allowed_filesystems = if limit_filesystem {
get_filesystem_devices(&target_dirs, follow_links)
} else {
Default::default()
};
let allowed_filesystems = limit_filesystem
.then(|| get_filesystem_devices(&target_dirs))
.unwrap_or_default();
let simplified_dirs = simplify_dir_names(&target_dirs); let simplified_dirs = simplify_dir_names(&target_dirs);
let ignored_full_path: HashSet<PathBuf> = ignore_directories let ignored_full_path: HashSet<PathBuf> = ignore_directories
@@ -246,9 +224,22 @@ fn main() {
indicator.spawn(output_format.clone()) indicator.spawn(output_format.clone())
} }
let keep_collapsed: HashSet<PathBuf> = match config.get_collapse(&options) {
Some(ref collapse) => {
let mut combined_dirs = HashSet::new();
for collapse_dir in collapse {
for target_dir in target_dirs.iter() {
combined_dirs.insert(PathBuf::from(target_dir).join(collapse_dir));
}
}
combined_dirs
}
None => HashSet::new(),
};
let filter_modified_time = config.get_modified_time_operator(&options); let filter_modified_time = config.get_modified_time_operator(&options);
let filter_accessed_time = config.get_accessed_time_operator(&options); let filter_accessed_time = config.get_accessed_time_operator(&options);
let filter_changed_time = config.get_created_time_operator(&options); let filter_changed_time = config.get_changed_time_operator(&options);
let walk_data = WalkData { let walk_data = WalkData {
ignore_directories: ignored_full_path, ignore_directories: ignored_full_path,
@@ -260,40 +251,104 @@ fn main() {
filter_changed_time, filter_changed_time,
use_apparent_size: config.get_apparent_size(&options), use_apparent_size: config.get_apparent_size(&options),
by_filecount, by_filecount,
by_filetime: &by_filetime,
ignore_hidden, ignore_hidden,
follow_links, follow_links,
progress_data: indicator.data.clone(), progress_data: indicator.data.clone(),
errors: errors_for_rayon, errors: errors_for_rayon,
}; };
let threads_to_use = config.get_threads(&options); let threads_to_use = config.get_threads(&options);
let stack_size = config.get_custom_stack_size(&options); let stack_size = config.get_custom_stack_size(&options);
init_rayon(&stack_size, &threads_to_use);
let top_level_nodes = walk_it(simplified_dirs, &walk_data); init_rayon(&stack_size, &threads_to_use).install(|| {
let top_level_nodes = walk_it(simplified_dirs, &walk_data);
let tree = match summarize_file_types { let tree = match summarize_file_types {
true => get_all_file_types(&top_level_nodes, number_of_lines), true => get_all_file_types(&top_level_nodes, number_of_lines, walk_data.by_filetime),
false => { false => {
let agg_data = AggregateData { let agg_data = AggregateData {
min_size: config.get_min_size(&options), min_size: config.get_min_size(&options),
only_dir: config.get_only_dir(&options), only_dir: config.get_only_dir(&options),
only_file: config.get_only_file(&options), only_file: config.get_only_file(&options),
number_of_lines, number_of_lines,
depth, depth,
using_a_filter: !filter_regexs.is_empty() || !invert_filter_regexs.is_empty(), using_a_filter: !filter_regexs.is_empty() || !invert_filter_regexs.is_empty(),
}; short_paths: !config.get_full_paths(&options),
get_biggest(top_level_nodes, agg_data) };
get_biggest(
top_level_nodes,
agg_data,
walk_data.by_filetime,
keep_collapsed,
)
}
};
// Must have stopped indicator before we print to stderr
indicator.stop();
let print_errors = config.get_print_errors(&options);
let final_errors = walk_data.errors.lock().unwrap();
print_any_errors(print_errors, &final_errors);
if tree.children.is_empty() && !final_errors.file_not_found.is_empty() {
std::process::exit(1)
} else {
print_output(
config,
options,
tree,
walk_data.by_filecount,
is_colors,
terminal_width,
)
} }
}; });
}
// Must have stopped indicator before we print to stderr fn print_output(
indicator.stop(); config: Config,
options: Cli,
tree: DisplayNode,
by_filecount: bool,
is_colors: bool,
terminal_width: usize,
) {
let output_format = config.get_output_format(&options);
if errors_final.lock().unwrap().abort { if config.get_output_json(&options) {
return; OUTPUT_TYPE.with(|wrapped| {
if by_filecount {
wrapped.replace("count".to_string());
} else {
wrapped.replace(output_format);
}
});
println!("{}", serde_json::to_string(&tree).unwrap());
} else {
let idd = InitialDisplayData {
short_paths: !config.get_full_paths(&options),
is_reversed: !config.get_reverse(&options),
colors_on: is_colors,
by_filecount,
by_filetime: config.get_filetime(&options),
is_screen_reader: config.get_screen_reader(&options),
output_format,
bars_on_right: config.get_bars_on_right(&options),
};
draw_it(
idd,
&tree,
config.get_no_bars(&options),
terminal_width,
config.get_skip_total(&options),
)
} }
}
let final_errors = walk_data.errors.lock().unwrap(); fn print_any_errors(print_errors: bool, final_errors: &RuntimeErrors) {
if !final_errors.file_not_found.is_empty() { if !final_errors.file_not_found.is_empty() {
let err = final_errors let err = final_errors
.file_not_found .file_not_found
@@ -301,17 +356,17 @@ fn main() {
.map(|a| a.as_ref()) .map(|a| a.as_ref())
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.join(", "); .join(", ");
eprintln!("No such file or directory: {}", err); eprintln!("No such file or directory: {err}");
} }
if !final_errors.no_permissions.is_empty() { if !final_errors.no_permissions.is_empty() {
if config.get_print_errors(&options) { if print_errors {
let err = final_errors let err = final_errors
.no_permissions .no_permissions
.iter() .iter()
.map(|a| a.as_ref()) .map(|a| a.as_ref())
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.join(", "); .join(", ");
eprintln!("Did not have permissions for directories: {}", err); eprintln!("Did not have permissions for directories: {err}");
} else { } else {
eprintln!( eprintln!(
"Did not have permissions for all directories (add --print-errors to see errors)" "Did not have permissions for all directories (add --print-errors to see errors)"
@@ -325,72 +380,105 @@ fn main() {
.map(|a| a.as_ref()) .map(|a| a.as_ref())
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.join(", "); .join(", ");
eprintln!("Unknown Error: {}", err); eprintln!("Unknown Error: {err}");
} }
}
if let Some(root_node) = tree { fn read_paths_from_source(path: &str, null_terminated: bool) -> Vec<String> {
let idd = InitialDisplayData { let from_stdin = path == "-";
short_paths: !config.get_full_paths(&options),
is_reversed: !config.get_reverse(&options), let result: Result<Vec<String>, Option<String>> = (|| {
colors_on: is_colors, // 1) read bytes
by_filecount, let bytes = if from_stdin {
is_screen_reader: config.get_screen_reader(&options), let mut b = Vec::new();
output_format, io::stdin().lock().read_to_end(&mut b).map_err(|_| None)?;
bars_on_right: config.get_bars_on_right(&options), b
} else {
read(path).map_err(|e| Some(e.to_string()))?
}; };
if config.get_output_json(&options) { let text = std::str::from_utf8(&bytes).map_err(|e| {
println!("{}", serde_json::to_string(&root_node).unwrap()); if from_stdin {
None
} else {
Some(e.to_string())
}
})?;
let items: Vec<String> = if null_terminated {
text.split('\0')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
} else { } else {
draw_it( text.lines().map(str::to_owned).collect()
idd, };
config.get_no_bars(&options), if from_stdin && items.is_empty() {
terminal_width, return Err(None);
&root_node, }
config.get_skip_total(&options), Ok(items)
) })();
match result {
Ok(v) => v,
Err(None) => {
eprintln!("No files provided, defaulting to current directory");
vec![".".to_owned()]
}
Err(Some(msg)) => {
eprintln!("Failed to read file: {msg}");
vec![".".to_owned()]
} }
} }
} }
fn init_rayon(stack_size: &Option<usize>, threads: &Option<usize>) { fn init_rayon(stack: &Option<usize>, threads: &Option<usize>) -> rayon::ThreadPool {
// Rayon seems to raise this error on 32-bit builds let stack_size = match stack {
// The global thread pool has not been initialized.: ThreadPoolBuildError { kind: GlobalPoolAlreadyInitialized } Some(s) => Some(*s),
if cfg!(target_pointer_width = "64") { None => {
let result = panic::catch_unwind(|| build_thread_pool(*stack_size, *threads)); // Do not increase the stack size on a 32 bit system, it will fail
if result.is_err() { if cfg!(target_pointer_width = "32") {
eprintln!("Problem initializing rayon, try: export RAYON_NUM_THREADS=1") None
} else {
let large_stack = usize::pow(1024, 3);
let mut sys = System::new_all();
sys.refresh_memory();
// Larger stack size if possible to handle cases with lots of nested directories
let available = sys.available_memory();
if available > (large_stack * threads.unwrap_or(1)).try_into().unwrap() {
Some(large_stack)
} else {
None
}
}
}
};
match build_thread_pool(stack_size, threads) {
Ok(pool) => pool,
Err(err) => {
eprintln!("Problem initializing rayon, try: export RAYON_NUM_THREADS=1");
if stack.is_none() && stack_size.is_some() {
// stack parameter was none, try with default stack size
if let Ok(pool) = build_thread_pool(None, threads) {
eprintln!("WARNING: not using large stack size, got error: {err}");
return pool;
}
}
panic!("{err}");
} }
} }
} }
fn build_thread_pool( fn build_thread_pool(
stack: Option<usize>, stack_size: Option<usize>,
threads: Option<usize>, threads: &Option<usize>,
) -> Result<(), rayon::ThreadPoolBuildError> { ) -> Result<rayon::ThreadPool, rayon::ThreadPoolBuildError> {
let mut pool = rayon::ThreadPoolBuilder::new(); let mut pool_builder = rayon::ThreadPoolBuilder::new();
if let Some(thread_count) = threads {
pool = pool.num_threads(thread_count);
}
let stack_size = match stack {
Some(s) => Some(s),
None => {
let large_stack = usize::pow(1024, 3);
let mut s = System::new();
s.refresh_memory();
// Larger stack size if possible to handle cases with lots of nested directories
let available = s.available_memory();
if available > large_stack.try_into().unwrap() {
Some(large_stack)
} else {
None
}
}
};
if let Some(stack_size_param) = stack_size { if let Some(stack_size_param) = stack_size {
pool = pool.stack_size(stack_size_param); pool_builder = pool_builder.stack_size(stack_size_param);
} }
pool.build_global() if let Some(thread_count) = threads {
pool_builder = pool_builder.num_threads(*thread_count);
}
pool_builder.build()
} }
+35 -10
View File
@@ -16,6 +16,23 @@ pub struct Node {
pub depth: usize, pub depth: usize,
} }
#[derive(Debug, PartialEq)]
pub enum FileTime {
Modified,
Accessed,
Changed,
}
impl From<crate::cli::FileTime> for FileTime {
fn from(time: crate::cli::FileTime) -> Self {
match time {
crate::cli::FileTime::Modified => Self::Modified,
crate::cli::FileTime::Accessed => Self::Accessed,
crate::cli::FileTime::Changed => Self::Changed,
}
}
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn build_node( pub fn build_node(
dir: PathBuf, dir: PathBuf,
@@ -27,22 +44,23 @@ pub fn build_node(
) -> Option<Node> { ) -> Option<Node> {
let use_apparent_size = walk_data.use_apparent_size; let use_apparent_size = walk_data.use_apparent_size;
let by_filecount = walk_data.by_filecount; let by_filecount = walk_data.by_filecount;
let by_filetime = &walk_data.by_filetime;
get_metadata(&dir, use_apparent_size).map(|data| { get_metadata(
let inode_device = if is_symlink && !use_apparent_size { &dir,
None use_apparent_size,
} else { walk_data.follow_links && is_symlink,
data.1 )
}; .map(|data| {
let inode_device = data.1;
let size = if is_filtered_out_due_to_regex(walk_data.filter_regex, &dir) let size = if is_filtered_out_due_to_regex(walk_data.filter_regex, &dir)
|| is_filtered_out_due_to_invert_regex(walk_data.invert_filter_regex, &dir) || is_filtered_out_due_to_invert_regex(walk_data.invert_filter_regex, &dir)
|| (is_symlink && !use_apparent_size)
|| by_filecount && !is_file || by_filecount && !is_file
|| [ || [
(&walk_data.filter_modified_time, data.2 .0), (&walk_data.filter_modified_time, data.2.0),
(&walk_data.filter_accessed_time, data.2 .1), (&walk_data.filter_accessed_time, data.2.1),
(&walk_data.filter_changed_time, data.2 .2), (&walk_data.filter_changed_time, data.2.2),
] ]
.iter() .iter()
.any(|(filter_time, actual_time)| { .any(|(filter_time, actual_time)| {
@@ -51,6 +69,13 @@ pub fn build_node(
0 0
} else if by_filecount { } else if by_filecount {
1 1
} else if by_filetime.is_some() {
match by_filetime {
Some(FileTime::Modified) => data.2.0.unsigned_abs(),
Some(FileTime::Accessed) => data.2.1.unsigned_abs(),
Some(FileTime::Changed) => data.2.2.unsigned_abs(),
None => unreachable!(),
}
} else { } else {
data.0 data.0
}; };
+33 -4
View File
@@ -17,19 +17,42 @@ type FileTime = (i64, i64, i64);
pub fn get_metadata<P: AsRef<Path>>( pub fn get_metadata<P: AsRef<Path>>(
path: P, path: P,
use_apparent_size: bool, use_apparent_size: bool,
follow_links: bool,
) -> Option<(u64, Option<InodeAndDevice>, FileTime)> { ) -> Option<(u64, Option<InodeAndDevice>, FileTime)> {
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
match path.as_ref().metadata() { let metadata = if follow_links {
path.as_ref().metadata()
} else {
path.as_ref().symlink_metadata()
};
match metadata {
Ok(md) => { Ok(md) => {
let file_size = md.len();
if use_apparent_size { if use_apparent_size {
Some(( Some((
md.len(), file_size,
Some((md.ino(), md.dev())), Some((md.ino(), md.dev())),
(md.mtime(), md.atime(), md.ctime()), (md.mtime(), md.atime(), md.ctime()),
)) ))
} else { } else {
// On NTFS mounts, the reported block count can be unexpectedly large.
// To avoid overestimating disk usage, cap the allocated size to what the
// file should occupy based on the file system I/O block size (blksize).
// Related: https://github.com/bootandy/dust/issues/295
let blksize = md.blksize();
let target_size = file_size.div_ceil(blksize) * blksize;
let reported_size = md.blocks() * get_block_size();
// File systems can pre-allocate more space for a file than what would be necessary
let pre_allocation_buffer = blksize * 65536;
let max_size = target_size + pre_allocation_buffer;
let allocated_size = if reported_size > max_size {
target_size
} else {
reported_size
};
Some(( Some((
md.blocks() * get_block_size(), allocated_size,
Some((md.ino(), md.dev())), Some((md.ino(), md.dev())),
(md.mtime(), md.atime(), md.ctime()), (md.mtime(), md.atime(), md.ctime()),
)) ))
@@ -43,6 +66,7 @@ pub fn get_metadata<P: AsRef<Path>>(
pub fn get_metadata<P: AsRef<Path>>( pub fn get_metadata<P: AsRef<Path>>(
path: P, path: P,
use_apparent_size: bool, use_apparent_size: bool,
follow_links: bool,
) -> Option<(u64, Option<InodeAndDevice>, FileTime)> { ) -> Option<(u64, Option<InodeAndDevice>, FileTime)> {
// On windows opening the file to get size, file ID and volume can be very // On windows opening the file to get size, file ID and volume can be very
// expensive because 1) it causes a few system calls, and more importantly 2) it can cause // expensive because 1) it causes a few system calls, and more importantly 2) it can cause
@@ -142,7 +166,12 @@ pub fn get_metadata<P: AsRef<Path>>(
use std::os::windows::fs::MetadataExt; use std::os::windows::fs::MetadataExt;
let path = path.as_ref(); let path = path.as_ref();
match path.metadata() { let metadata = if follow_links {
path.metadata()
} else {
path.symlink_metadata()
};
match metadata {
Ok(ref md) => { Ok(ref md) => {
const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x20; const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x20;
const FILE_ATTRIBUTE_READONLY: u32 = 0x01; const FILE_ATTRIBUTE_READONLY: u32 = 0x01;
+18 -10
View File
@@ -3,14 +3,19 @@ use std::{
io::Write, io::Write,
path::Path, path::Path,
sync::{ sync::{
atomic::{AtomicU64, AtomicU8, AtomicUsize, Ordering},
mpsc::{self, RecvTimeoutError, Sender},
Arc, RwLock, Arc, RwLock,
atomic::{AtomicU8, AtomicUsize, Ordering},
mpsc::{self, RecvTimeoutError, Sender},
}, },
thread::JoinHandle, thread::JoinHandle,
time::Duration, time::Duration,
}; };
#[cfg(not(target_has_atomic = "64"))]
use portable_atomic::AtomicU64;
#[cfg(target_has_atomic = "64")]
use std::sync::atomic::AtomicU64;
use crate::display::human_readable_number; use crate::display::human_readable_number;
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
@@ -73,7 +78,7 @@ pub struct RuntimeErrors {
pub no_permissions: HashSet<String>, pub no_permissions: HashSet<String>,
pub file_not_found: HashSet<String>, pub file_not_found: HashSet<String>,
pub unknown_error: HashSet<String>, pub unknown_error: HashSet<String>,
pub abort: bool, pub interrupted_error: i32,
} }
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
@@ -113,7 +118,7 @@ impl PIndicator {
let time_info_thread = std::thread::spawn(move || { let time_info_thread = std::thread::spawn(move || {
let mut progress_char_i: usize = 0; let mut progress_char_i: usize = 0;
let mut stdout = std::io::stdout(); let mut stderr = std::io::stderr();
let mut msg = "".to_string(); let mut msg = "".to_string();
// While the timeout triggers we go round the loop // While the timeout triggers we go round the loop
@@ -122,7 +127,8 @@ impl PIndicator {
receiver.recv_timeout(Duration::from_millis(SPINNER_SLEEP_TIME)) receiver.recv_timeout(Duration::from_millis(SPINNER_SLEEP_TIME))
{ {
// Clear the text written by 'write!'& Return at the start of line // Clear the text written by 'write!'& Return at the start of line
print!("\r{:width$}", " ", width = msg.len()); let clear = format!("\r{:width$}", " ", width = msg.len());
write!(stderr, "{clear}").unwrap();
let prog_char = PROGRESS_CHARS[progress_char_i]; let prog_char = PROGRESS_CHARS[progress_char_i];
msg = match data.state.load(ORDERING) { msg = match data.state.load(ORDERING) {
@@ -131,15 +137,17 @@ impl PIndicator {
_ => panic!("Unknown State"), _ => panic!("Unknown State"),
}; };
write!(stdout, "\r{msg}").unwrap(); write!(stderr, "\r{msg}").unwrap();
stdout.flush().unwrap(); stderr.flush().unwrap();
progress_char_i += 1; progress_char_i += 1;
progress_char_i %= PROGRESS_CHARS_LEN; progress_char_i %= PROGRESS_CHARS_LEN;
} }
print!("\r{:width$}", " ", width = msg.len());
print!("\r"); let clear = format!("\r{:width$}", " ", width = msg.len());
stdout.flush().unwrap(); write!(stderr, "{clear}").unwrap();
write!(stderr, "\r").unwrap();
stderr.flush().unwrap();
}); });
self.thread = Some((stop_handler, time_info_thread)) self.thread = Some((stop_handler, time_info_thread))
} }
+36 -9
View File
@@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
use crate::config::DAY_SECONDS; use crate::config::DAY_SECONDS;
use crate::dir_walker::Operater; use crate::dir_walker::Operator;
use crate::platform; use crate::platform;
use regex::Regex; use regex::Regex;
@@ -34,13 +34,25 @@ pub fn simplify_dir_names<P: AsRef<Path>>(dirs: &[P]) -> HashSet<PathBuf> {
top_level_names top_level_names
} }
pub fn get_filesystem_devices<P: AsRef<Path>>(paths: &[P]) -> HashSet<u64> { pub fn get_filesystem_devices<P: AsRef<Path>>(paths: &[P], follow_links: bool) -> HashSet<u64> {
use std::fs;
// Gets the device ids for the filesystems which are used by the argument paths // Gets the device ids for the filesystems which are used by the argument paths
paths paths
.iter() .iter()
.filter_map(|p| match get_metadata(p, false) { .filter_map(|p| {
Some((_size, Some((_id, dev)), _time)) => Some(dev), let follow_links = if follow_links {
_ => None, // slow path: If dereference-links is set, then we check if the file is a symbolic link
match fs::symlink_metadata(p) {
Ok(metadata) => metadata.file_type().is_symlink(),
Err(_) => false,
}
} else {
false
};
match get_metadata(p, false, follow_links) {
Some((_size, Some((_id, dev)), _time)) => Some(dev),
_ => None,
}
}) })
.collect() .collect()
} }
@@ -55,6 +67,17 @@ pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
path.as_ref().components().collect() path.as_ref().components().collect()
} }
// Canonicalize the path only if it is an absolute path
pub fn canonicalize_absolute_path(path: PathBuf) -> PathBuf {
if !path.is_absolute() {
return path;
}
match std::fs::canonicalize(&path) {
Ok(canonicalized_path) => canonicalized_path,
Err(_) => path,
}
}
pub fn is_filtered_out_due_to_regex(filter_regex: &[Regex], dir: &Path) -> bool { pub fn is_filtered_out_due_to_regex(filter_regex: &[Regex], dir: &Path) -> bool {
if filter_regex.is_empty() { if filter_regex.is_empty() {
false false
@@ -65,13 +88,17 @@ pub fn is_filtered_out_due_to_regex(filter_regex: &[Regex], dir: &Path) -> bool
} }
} }
pub fn is_filtered_out_due_to_file_time(filter_time: &(Operater, i64), actual_time: i64) -> bool { pub fn is_filtered_out_due_to_file_time(
filter_time: &Option<(Operator, i64)>,
actual_time: i64,
) -> bool {
match filter_time { match filter_time {
(Operater::Equal, bound_time) => { None => false,
Some((Operator::Equal, bound_time)) => {
!(actual_time >= *bound_time && actual_time < *bound_time + DAY_SECONDS) !(actual_time >= *bound_time && actual_time < *bound_time + DAY_SECONDS)
} }
(Operater::GreaterThan, bound_time) => actual_time < *bound_time, Some((Operator::GreaterThan, bound_time)) => actual_time < *bound_time,
(Operater::LessThan, bound_time) => actual_time > *bound_time, Some((Operator::LessThan, bound_time)) => actual_time > *bound_time,
} }
} }
View File
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
tests/test_dir_files_from/a_file
tests/test_dir_files_from/hello_file
+1
View File
@@ -0,0 +1 @@
hello
+7 -4
View File
@@ -1,8 +1,8 @@
use assert_cmd::Command; use assert_cmd::{Command, cargo_bin_cmd};
use std::ffi::OsStr; use std::ffi::OsStr;
use std::process::Output; use std::process::Output;
use std::sync::Once; use std::sync::Once;
use std::{fs, io, str}; use std::{io, str};
static INIT: Once = Once::new(); static INIT: Once = Once::new();
static UNREADABLE_DIR_PATH: &str = "/tmp/unreadable_dir"; static UNREADABLE_DIR_PATH: &str = "/tmp/unreadable_dir";
@@ -38,6 +38,7 @@ fn copy_test_data(dir: &str) {
fn create_unreadable_directory() -> io::Result<()> { fn create_unreadable_directory() -> io::Result<()> {
#[cfg(unix)] #[cfg(unix)]
{ {
use std::fs;
use std::fs::Permissions; use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
fs::create_dir_all(UNREADABLE_DIR_PATH)?; fs::create_dir_all(UNREADABLE_DIR_PATH)?;
@@ -60,9 +61,11 @@ fn initialize() {
fn run_cmd<T: AsRef<OsStr>>(command_args: &[T]) -> Output { fn run_cmd<T: AsRef<OsStr>>(command_args: &[T]) -> Output {
initialize(); initialize();
let mut to_run = &mut Command::cargo_bin("dust").unwrap(); let mut to_run = cargo_bin_cmd!("dust");
// Hide progress bar
to_run.arg("-P");
for p in command_args { for p in command_args {
to_run = to_run.arg(p); to_run.arg(p);
} }
to_run.unwrap() to_run.unwrap()
} }
+91 -8
View File
@@ -1,4 +1,4 @@
use assert_cmd::Command; use assert_cmd::cargo_bin_cmd;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::str; use std::str;
@@ -9,14 +9,16 @@ use std::str;
*/ */
fn build_command<T: AsRef<OsStr>>(command_args: Vec<T>) -> String { fn build_command<T: AsRef<OsStr>>(command_args: Vec<T>) -> String {
let mut cmd = &mut Command::cargo_bin("dust").unwrap(); let mut cmd = cargo_bin_cmd!("dust");
// Hide progress bar
cmd.arg("-P");
for p in command_args { for p in command_args {
cmd = cmd.arg(p); cmd.arg(p);
} }
let finished = &cmd.unwrap(); let finished = &cmd.unwrap();
let stderr = str::from_utf8(&finished.stderr).unwrap(); assert_eq!(str::from_utf8(&finished.stderr).unwrap(), "");
assert_eq!(stderr, "");
str::from_utf8(&finished.stdout).unwrap().into() str::from_utf8(&finished.stdout).unwrap().into()
} }
@@ -59,6 +61,14 @@ pub fn test_d_flag_works() {
assert!(!output.contains("hello_file")); assert!(!output.contains("hello_file"));
} }
#[test]
pub fn test_d0_works_on_multiple() {
// We should see the top level directory but not the sub dirs / files:
let output = build_command(vec!["-d", "0", "tests/test_dir/", "tests/test_dir2"]);
assert!(output.contains("test_dir "));
assert!(output.contains("test_dir2"));
}
#[test] #[test]
pub fn test_threads_flag_works() { pub fn test_threads_flag_works() {
let output = build_command(vec!["-T", "1", "tests/test_dir/"]); let output = build_command(vec!["-T", "1", "tests/test_dir/"]);
@@ -93,10 +103,60 @@ pub fn test_ignore_all_in_file() {
assert!(!output.contains(".secret")); assert!(!output.contains(".secret"));
} }
#[test]
pub fn test_files_from_flag_file() {
let output = build_command(vec![
"--files-from",
"tests/test_dir_files_from/files_from.txt",
]);
assert!(output.contains("a_file"));
assert!(output.contains("hello_file"));
}
#[test]
pub fn test_files0_from_flag_file() {
let output = build_command(vec![
"--files0-from",
"tests/test_dir_files_from/files0_from.txt",
]);
assert!(output.contains("a_file"));
assert!(output.contains("hello_file"));
}
#[test]
pub fn test_files_from_flag_stdin() {
let mut cmd = cargo_bin_cmd!("dust");
cmd.arg("-P").arg("--files-from").arg("-");
let input = b"tests/test_dir_files_from/a_file\ntests/test_dir_files_from/hello_file\n";
cmd.write_stdin(input.as_ref());
let finished = &cmd.unwrap();
let stderr = std::str::from_utf8(&finished.stderr).unwrap();
assert_eq!(stderr, "");
let output = std::str::from_utf8(&finished.stdout).unwrap();
assert!(output.contains("a_file"));
assert!(output.contains("hello_file"));
}
#[test]
pub fn test_files0_from_flag_stdin() {
let mut cmd = cargo_bin_cmd!("dust");
cmd.arg("-P").arg("--files0-from").arg("-");
let input = b"tests/test_dir_files_from/a_file\0tests/test_dir_files_from/hello_file\0";
cmd.write_stdin(input.as_ref());
let finished = &cmd.unwrap();
let stderr = std::str::from_utf8(&finished.stderr).unwrap();
assert_eq!(stderr, "");
let output = std::str::from_utf8(&finished.stdout).unwrap();
assert!(output.contains("a_file"));
assert!(output.contains("hello_file"));
}
#[test] #[test]
pub fn test_with_bad_param() { pub fn test_with_bad_param() {
let mut cmd = Command::cargo_bin("dust").unwrap(); let mut cmd = cargo_bin_cmd!("dust");
let result = cmd.arg("bad_place").unwrap(); cmd.arg("-P").arg("bad_place");
let output_error = cmd.unwrap_err();
let result = output_error.as_output().unwrap();
let stderr = str::from_utf8(&result.stderr).unwrap(); let stderr = str::from_utf8(&result.stderr).unwrap();
assert!(stderr.contains("No such file or directory")); assert!(stderr.contains("No such file or directory"));
} }
@@ -254,3 +314,26 @@ pub fn test_force_color() {
assert!(output.contains("\x1B[31m")); assert!(output.contains("\x1B[31m"));
assert!(output.contains("\x1B[0m")); assert!(output.contains("\x1B[0m"));
} }
#[test]
pub fn test_collapse() {
let output = build_command(vec!["--collapse", "many", "tests/test_dir/"]);
assert!(output.contains("many"));
assert!(!output.contains("hello_file"));
}
#[test]
pub fn test_handle_duplicate_names() {
// Check that even if we run on a multiple directories with the same name
// we still show the distinct parent dir in the output
let output = build_command(vec![
"tests/test_dir_matching/dave/dup_name",
"tests/test_dir_matching/andy/dup_name",
"ci",
]);
assert!(output.contains("andy"));
assert!(output.contains("dave"));
assert!(output.contains("ci"));
assert!(output.contains("dup_name"));
assert!(!output.contains("test_dir_matching"));
}
+5 -5
View File
@@ -1,4 +1,4 @@
use assert_cmd::Command; use assert_cmd::{Command, cargo_bin_cmd};
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
@@ -44,7 +44,7 @@ pub fn test_soft_sym_link() {
let b = format!(" ┌── {}", file_path_s); let b = format!(" ┌── {}", file_path_s);
let a = format!("─┴ {}", dir_s); let a = format!("─┴ {}", dir_s);
let mut cmd = Command::cargo_bin("dust").unwrap(); let mut cmd = cargo_bin_cmd!("dust");
// Mac test runners create long filenames in tmp directories // Mac test runners create long filenames in tmp directories
let output = cmd let output = cmd
.args(["-p", "-c", "-s", "-w", "999", dir_s]) .args(["-p", "-c", "-s", "-w", "999", dir_s])
@@ -72,7 +72,7 @@ pub fn test_hard_sym_link() {
let file_output = format!(" ┌── {}", file_path_s); let file_output = format!(" ┌── {}", file_path_s);
let dirs_output = format!("─┴ {}", dir_s); let dirs_output = format!("─┴ {}", dir_s);
let mut cmd = Command::cargo_bin("dust").unwrap(); let mut cmd = cargo_bin_cmd!("dust");
// Mac test runners create long filenames in tmp directories // Mac test runners create long filenames in tmp directories
let output = cmd.args(["-p", "-c", "-w", "999", dir_s]).unwrap().stdout; let output = cmd.args(["-p", "-c", "-w", "999", dir_s]).unwrap().stdout;
@@ -96,7 +96,7 @@ pub fn test_hard_sym_link_no_dup_multi_arg() {
let link_name = dir_link.path().join("the_link"); let link_name = dir_link.path().join("the_link");
let link_name_s = link_it(link_name, file_path_s, false); let link_name_s = link_it(link_name, file_path_s, false);
let mut cmd = Command::cargo_bin("dust").unwrap(); let mut cmd = cargo_bin_cmd!("dust");
// Mac test runners create long filenames in tmp directories // Mac test runners create long filenames in tmp directories
let output = cmd let output = cmd
@@ -123,7 +123,7 @@ pub fn test_recursive_sym_link() {
let a = format!("─┬ {}", dir_s); let a = format!("─┬ {}", dir_s);
let b = format!(" └── {}", link_name_s); let b = format!(" └── {}", link_name_s);
let mut cmd = Command::cargo_bin("dust").unwrap(); let mut cmd = cargo_bin_cmd!("dust");
let output = cmd let output = cmd
.arg("-p") .arg("-p")
.arg("-c") .arg("-c")