Compare commits

..

14 Commits

Author SHA1 Message Date
andy.boot
3428c5c35c version: increment version 2024-07-05 21:17:45 +01:00
Maksim Bondarenkov
4fc1897678 deps: update winapi-util to 0.1.8 (#410) 2024-07-03 21:41:52 +01:00
andy.boot
08b9c756ee fix: total_ordering of sort_by_inode
Before sort_by_inode could result in unstable ordering. This change
ensures we will always have a reliable total ordering
2024-06-30 21:57:58 +01:00
n4n5
394231683d feat: Add flag to read from file/stdin (#405)
* from standard

* improve

* happy clippy

* explicit arg option

* fix problem
2024-06-27 23:51:47 +01:00
Jan Chren ~rindeal
a06a001886 test_exact_output: simplify array handling 2024-06-27 21:47:18 +01:00
Jan Chren ~rindeal
fd9e97bcfa test_exact_output: refactor unreadable directory handling 2024-06-27 21:47:18 +01:00
Jan Chren ~rindeal
3ed95ee399 streamline func APIs processing target_dirs 2024-06-27 21:47:18 +01:00
Jan Chren ~rindeal
58c9f6d509 Fix -x option behavior
This PR addresses an issue where `target_dirs` were being optimized out\
before being passed to the `get_filesystem_devices()` function.
The changes in this PR ensure that `target_dirs` are passed directly
to `get_filesystem_devices()`, and only then are they simplified.

Example of a command that currently doesn't work correctly:
```sh
dust -x $(findmnt -S tmpfs -o target -n)
```

It should show the usage of all tmpfs mounts, but it currently shows
just the the root mountpoints like `/run`, since all the mountpoints
nested under it are optimized out.
2024-06-27 21:47:18 +01:00
wugeer
3f2f7a8bb2 Formatting 2024-06-27 21:11:11 +01:00
andy.boot
b7176cf887 tests: Cleanup test_exact_ouput 2024-06-24 23:36:09 +01:00
wugeer
d65f41097e feat: Added the ability to filter the corresponding files based on the access time, modify time, and change time of the file for statistics 2024-06-24 23:35:25 +01:00
n4n5
08e4240b41 add cli 2024-06-19 21:48:55 +01:00
n4n5
028ca1fdc7 eprint problematic folders 2024-06-19 21:48:55 +01:00
andy.boot
4f6255971b refactor: cleanup -j / --output-json flag
Stop -j writing to a file, print to stdout instead.
2024-06-19 21:39:09 +01:00
18 changed files with 560 additions and 136 deletions

18
Cargo.lock generated
View File

@@ -100,9 +100,9 @@ dependencies = [
[[package]]
name = "autocfg"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "bitflags"
@@ -135,9 +135,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "cc"
version = "1.0.95"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
[[package]]
name = "cfg-if"
@@ -510,9 +510,9 @@ dependencies = [
[[package]]
name = "num-traits"
version = "0.2.18"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
@@ -919,11 +919,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.6"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
dependencies = [
"winapi",
"windows-sys 0.52.0",
]
[[package]]

View File

@@ -1,7 +1,7 @@
[package]
name = "du-dust"
description = "A more intuitive version of du"
version = "1.0.0"
version = "1.1.0"
authors = ["bootandy <bootandy@gmail.com>", "nebkor <code@ardent.nebcorp.com>"]
edition = "2021"
readme = "README.md"

View File

@@ -92,6 +92,7 @@ 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 -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 --files0-from=FILE (Reads null-terminated file paths from FILE); If FILE is - then read from stdin
```
## Config file

View File

@@ -37,6 +37,13 @@ _dust() {
'--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)' \
'-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: ' \
'--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+[+/-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=[+/-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 => (𝑐𝑢𝑟𝑟𝑛, +∞)]: : ' \
'-A+[just like -mtime, but based on file access time]: : ' \
'--atime=[just like -mtime, but based on file access time]: : ' \
'-y+[just like -mtime, but based on file change time]: : ' \
'--ctime=[just like -mtime, but based on file change time]: : ' \
'--files0-from=[run dust on NUL-terminated file names specified in file; if argument is -, then read names from standard input]: :_files' \
'-p[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]' \
@@ -66,6 +73,7 @@ _dust() {
'(-d --depth -D --only-dir)--file_types[show only these file types]' \
'-P[Disable the progress indication.]' \
'--no-progress[Disable the progress indication.]' \
'--print-errors[Print path with errors.]' \
'(-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.]' \
'(-D --only-dir)-F[Only files will be displayed. (Finds your largest files)]' \

View File

@@ -43,6 +43,13 @@ Register-ArgumentCompleter -Native -CommandName 'dust' -ScriptBlock {
[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('-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('--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('-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('--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('-A', 'A ', [CompletionResultType]::ParameterName, 'just like -mtime, but based on file access time')
[CompletionResult]::new('--atime', 'atime', [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('--ctime', 'ctime', [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('-p', 'p', [CompletionResultType]::ParameterName, 'Subdirectories will not have their path shortened')
[CompletionResult]::new('--full-paths', 'full-paths', [CompletionResultType]::ParameterName, 'Subdirectories will not have their path shortened')
[CompletionResult]::new('-L', 'L ', [CompletionResultType]::ParameterName, 'dereference sym links - Treat sym links as directories and go into them')
@@ -72,6 +79,7 @@ Register-ArgumentCompleter -Native -CommandName 'dust' -ScriptBlock {
[CompletionResult]::new('--file_types', 'file_types', [CompletionResultType]::ParameterName, 'show only these file types')
[CompletionResult]::new('-P', 'P ', [CompletionResultType]::ParameterName, 'Disable the progress indication.')
[CompletionResult]::new('--no-progress', 'no-progress', [CompletionResultType]::ParameterName, 'Disable the progress indication.')
[CompletionResult]::new('--print-errors', 'print-errors', [CompletionResultType]::ParameterName, 'Print path with errors.')
[CompletionResult]::new('-D', 'D ', [CompletionResultType]::ParameterName, 'Only directories will be displayed.')
[CompletionResult]::new('--only-dir', 'only-dir', [CompletionResultType]::ParameterName, 'Only directories will be displayed.')
[CompletionResult]::new('-F', 'F ', [CompletionResultType]::ParameterName, 'Only files will be displayed. (Finds your largest files)')

View File

@@ -19,7 +19,7 @@ _dust() {
case "${cmd}" in
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 -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 --only-dir --only-file --output-format --stack-size --output-json --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 -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]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
@@ -135,6 +135,34 @@ _dust() {
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--mtime)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-M)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--atime)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-A)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--ctime)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-y)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--files0-from)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;

View File

@@ -40,6 +40,13 @@ set edit:completion:arg-completer[dust] = {|@words|
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 --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 --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 => (𝑐𝑢𝑟𝑟𝑛, +∞)'
cand -A '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 --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 -p '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'
@@ -69,6 +76,7 @@ set edit:completion:arg-completer[dust] = {|@words|
cand --file_types 'show only these file types'
cand -P 'Disable the progress indication.'
cand --no-progress 'Disable the progress indication.'
cand --print-errors 'Print path with errors.'
cand -D 'Only directories will be displayed.'
cand --only-dir 'Only directories will be displayed.'
cand -F 'Only files will be displayed. (Finds your largest files)'

View File

@@ -9,6 +9,10 @@ complete -c dust -s e -l filter -d 'Only include filepaths matching this regex.
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 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 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 -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 -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 x -l limit-filesystem -d 'Only count the files and directories on the same filesystem as the supplied directory'
@@ -24,6 +28,7 @@ complete -c dust -s f -l filecount -d 'Directory \'size\' is number of child fil
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 P -l no-progress -d 'Disable the progress indication.'
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 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'

View File

@@ -4,7 +4,7 @@
.SH NAME
Dust \- Like du but more intuitive
.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\-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\-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\-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]
.SH DESCRIPTION
Like du but more intuitive
.SH OPTIONS
@@ -81,6 +81,9 @@ Specify width of output overriding the auto detection of terminal width
\fB\-P\fR, \fB\-\-no\-progress\fR
Disable the progress indication.
.TP
\fB\-\-print\-errors\fR
Print path with errors.
.TP
\fB\-D\fR, \fB\-\-only\-dir\fR
Only directories will be displayed.
.TP
@@ -100,6 +103,18 @@ Specify memory to use as stack size \- use if you see: \*(Aqfatal runtime error:
\fB\-j\fR, \fB\-\-output\-json\fR
Output the directory tree as json to the current directory
.TP
\fB\-M\fR, \fB\-\-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 => (𝑐𝑢𝑟𝑟𝑛, +∞)
.TP
\fB\-A\fR, \fB\-\-atime\fR
just like \-mtime, but based on file access time
.TP
\fB\-y\fR, \fB\-\-ctime\fR
just like \-mtime, but based on file change time
.TP
\fB\-\-files0\-from\fR
run dust on NUL\-terminated file names specified in file; if argument is \-, then read names from standard input
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help
.TP

View File

@@ -193,6 +193,12 @@ pub fn build_cli() -> Command {
.action(clap::ArgAction::SetTrue)
.help("Disable the progress indication."),
)
.arg(
Arg::new("print_errors")
.long("print-errors")
.action(clap::ArgAction::SetTrue)
.help("Print path with errors."),
)
.arg(
Arg::new("only_dir")
.short('D')
@@ -253,4 +259,39 @@ pub fn build_cli() -> Command {
.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"),
)
}

View File

@@ -1,3 +1,4 @@
use chrono::{Local, TimeZone};
use clap::ArgMatches;
use config_file::FromConfigFile;
use regex::Regex;
@@ -6,8 +7,11 @@ use std::io::IsTerminal;
use std::path::Path;
use std::path::PathBuf;
use crate::dir_walker::Operater;
use crate::display::get_number_format;
pub static DAY_SECONDS: i64 = 24 * 60 * 60;
#[derive(Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
@@ -31,9 +35,18 @@ pub struct Config {
pub stack_size: Option<usize>,
pub threads: Option<usize>,
pub output_json: Option<bool>,
pub print_errors: Option<bool>,
pub files0_from: Option<String>,
}
impl Config {
pub fn get_files_from(&self, options: &ArgMatches) -> Option<String> {
let from_file = options.get_one::<String>("files0_from");
match from_file {
None => self.files0_from.as_ref().map(|x| 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")
}
@@ -102,6 +115,10 @@ impl Config {
pub fn get_only_dir(&self, options: &ArgMatches) -> bool {
Some(true) == self.only_dir || options.get_flag("only_dir")
}
pub fn get_print_errors(&self, options: &ArgMatches) -> bool {
Some(true) == self.print_errors || options.get_flag("print_errors")
}
pub fn get_only_file(&self, options: &ArgMatches) -> bool {
Some(true) == self.only_file || options.get_flag("only_file")
}
@@ -127,6 +144,61 @@ impl Config {
pub fn get_output_json(&self, options: &ArgMatches) -> bool {
Some(true) == self.output_json || options.get_flag("output_json")
}
pub fn get_modified_time_operator(&self, options: &ArgMatches) -> (Operater, i64) {
get_filter_time_operator(
options.get_one::<String>("mtime"),
get_current_date_epoch_seconds(),
)
}
pub fn get_accessed_time_operator(&self, options: &ArgMatches) -> (Operater, i64) {
get_filter_time_operator(
options.get_one::<String>("atime"),
get_current_date_epoch_seconds(),
)
}
pub fn get_created_time_operator(&self, options: &ArgMatches) -> (Operater, i64) {
get_filter_time_operator(
options.get_one::<String>("ctime"),
get_current_date_epoch_seconds(),
)
}
}
fn get_current_date_epoch_seconds() -> i64 {
// calcurate current date epoch seconds
let now = Local::now();
let current_date = now.date_naive();
let current_date_time = current_date.and_hms_opt(0, 0, 0).unwrap();
Local
.from_local_datetime(&current_date_time)
.unwrap()
.timestamp()
}
fn get_filter_time_operator(
option_value: Option<&String>,
current_date_epoch_seconds: i64,
) -> (Operater, i64) {
match option_value {
Some(val) => {
let time = current_date_epoch_seconds
- val
.parse::<i64>()
.unwrap_or_else(|_| panic!("invalid data format"))
.abs()
* DAY_SECONDS;
match val.chars().next().expect("Value should not be empty") {
'+' => (Operater::LessThan, time - DAY_SECONDS),
'-' => (Operater::GreaterThan, time),
_ => (Operater::Equal, time - DAY_SECONDS),
}
}
None => (Operater::GreaterThan, 0),
}
}
fn convert_min_size(input: &str) -> Option<usize> {
@@ -186,8 +258,22 @@ pub fn get_config() -> Config {
mod tests {
#[allow(unused_imports)]
use super::*;
use chrono::{Datelike, Timelike};
use clap::{value_parser, Arg, ArgMatches, Command};
#[test]
fn test_get_current_date_epoch_seconds() {
let epoch_seconds = get_current_date_epoch_seconds();
let dt = Local.timestamp_opt(epoch_seconds, 0).unwrap();
assert_eq!(dt.hour(), 0);
assert_eq!(dt.minute(), 0);
assert_eq!(dt.second(), 0);
assert_eq!(dt.date_naive().day(), Local::now().date_naive().day());
assert_eq!(dt.date_naive().month(), Local::now().date_naive().month());
assert_eq!(dt.date_naive().year(), Local::now().date_naive().year());
}
#[test]
fn test_conversion() {
assert_eq!(convert_min_size("55"), Some(55));

View File

@@ -1,3 +1,4 @@
use std::cmp::Ordering;
use std::fs;
use std::sync::Arc;
use std::sync::Mutex;
@@ -7,6 +8,7 @@ use crate::progress::Operation;
use crate::progress::PAtomicInfo;
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_invert_regex;
use crate::utils::is_filtered_out_due_to_regex;
use rayon::iter::ParallelBridge;
@@ -20,11 +22,22 @@ use crate::node::build_node;
use std::fs::DirEntry;
use crate::platform::get_metadata;
#[derive(Debug)]
pub enum Operater {
Equal = 0,
LessThan = 1,
GreaterThan = 2,
}
pub struct WalkData<'a> {
pub ignore_directories: HashSet<PathBuf>,
pub filter_regex: &'a [Regex],
pub invert_filter_regex: &'a [Regex],
pub allowed_filesystems: HashSet<u64>,
pub filter_modified_time: (Operater, i64),
pub filter_accessed_time: (Operater, i64),
pub filter_changed_time: (Operater, i64),
pub use_apparent_size: bool,
pub by_filecount: bool,
pub ignore_hidden: bool,
@@ -83,29 +96,47 @@ fn clean_inodes(
fn sort_by_inode(a: &Node, b: &Node) -> std::cmp::Ordering {
// Sorting by inode is quicker than by sorting by name/size
if let Some(x) = a.inode_device {
if let Some(y) = b.inode_device {
match (a.inode_device, b.inode_device) {
(Some(x), Some(y)) => {
if x.0 != y.0 {
return x.0.cmp(&y.0);
x.0.cmp(&y.0)
} else if x.1 != y.1 {
return x.1.cmp(&y.1);
x.1.cmp(&y.1)
} else {
a.name.cmp(&b.name)
}
}
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(None, None) => a.name.cmp(&b.name),
}
a.name.cmp(&b.name)
}
fn ignore_file(entry: &DirEntry, walk_data: &WalkData) -> bool {
let is_dot_file = entry.file_name().to_str().unwrap_or("").starts_with('.');
let is_ignored_path = walk_data.ignore_directories.contains(&entry.path());
if !walk_data.allowed_filesystems.is_empty() {
let size_inode_device = get_metadata(&entry.path(), false);
if let Some((_size, Some((_id, dev)))) = size_inode_device {
if !walk_data.allowed_filesystems.contains(&dev) {
return true;
}
let size_inode_device = get_metadata(entry.path(), false);
if let Some((_size, Some((_id, dev)), (modified_time, accessed_time, changed_time))) =
size_inode_device
{
if !walk_data.allowed_filesystems.is_empty()
&& !walk_data.allowed_filesystems.contains(&dev)
{
return true;
}
if entry.path().is_file()
&& [
(&walk_data.filter_modified_time, modified_time),
(&walk_data.filter_accessed_time, accessed_time),
(&walk_data.filter_changed_time, changed_time),
]
.iter()
.any(|(filter_time, actual_time)| {
is_filtered_out_due_to_file_time(filter_time, *actual_time)
})
{
return true;
}
}
@@ -130,6 +161,7 @@ fn ignore_file(entry: &DirEntry, walk_data: &WalkData) -> bool {
fn walk(dir: PathBuf, walk_data: &WalkData, depth: usize) -> Option<Node> {
let prog_data = &walk_data.progress_data;
let errors = &walk_data.errors;
if errors.lock().unwrap().abort {
return None;
}
@@ -142,44 +174,46 @@ fn walk(dir: PathBuf, walk_data: &WalkData, depth: usize) -> Option<Node> {
.into_iter()
.par_bridge()
.filter_map(|entry| {
if let Ok(ref entry) = entry {
// uncommenting the below line gives simpler code but
// rayon doesn't parallelize as well giving a 3X performance drop
// hence we unravel the recursion a bit
match entry {
Ok(ref entry) => {
// uncommenting the below line gives simpler code but
// rayon doesn't parallelize as well giving a 3X performance drop
// hence we unravel the recursion a bit
// return walk(entry.path(), walk_data, depth)
// return walk(entry.path(), walk_data, depth)
if !ignore_file(entry, walk_data) {
if let Ok(data) = entry.file_type() {
if data.is_dir()
|| (walk_data.follow_links && data.is_symlink())
{
return walk(entry.path(), walk_data, depth + 1);
if !ignore_file(entry, walk_data) {
if let Ok(data) = entry.file_type() {
if data.is_dir()
|| (walk_data.follow_links && data.is_symlink())
{
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![],
walk_data.filter_regex,
walk_data.invert_filter_regex,
walk_data.use_apparent_size,
data.is_symlink(),
data.is_file(),
walk_data.by_filecount,
depth,
);
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;
}
}
} else {
let mut editable_error = errors.lock().unwrap();
editable_error.no_permissions = true
Err(ref failed) => {
let mut editable_error = errors.lock().unwrap();
editable_error.no_permissions.insert(failed.to_string());
}
}
None
})
@@ -189,7 +223,9 @@ fn walk(dir: PathBuf, walk_data: &WalkData, depth: usize) -> Option<Node> {
let mut editable_error = errors.lock().unwrap();
match failed.kind() {
std::io::ErrorKind::PermissionDenied => {
editable_error.no_permissions = true;
editable_error
.no_permissions
.insert(dir.to_string_lossy().into());
}
std::io::ErrorKind::NotFound => {
editable_error.file_not_found.insert(failed.to_string());
@@ -209,20 +245,11 @@ fn walk(dir: PathBuf, walk_data: &WalkData, depth: usize) -> Option<Node> {
}
vec![]
};
build_node(
dir,
children,
walk_data.filter_regex,
walk_data.invert_filter_regex,
walk_data.use_apparent_size,
false,
false,
walk_data.by_filecount,
depth,
)
build_node(dir, children, false, false, depth, walk_data)
}
mod tests {
#[allow(unused_imports)]
use super::*;
@@ -260,4 +287,41 @@ mod tests {
assert_eq!(clean_inodes(n.clone(), &mut inodes, true), Some(n.clone()));
assert_eq!(clean_inodes(n.clone(), &mut inodes, true), Some(n.clone()));
}
#[test]
fn test_total_ordering_of_sort_by_inode() {
use std::str::FromStr;
let a = Node {
name: PathBuf::from_str("a").unwrap(),
size: 0,
children: vec![],
inode_device: Some((3, 66310)),
depth: 0,
};
let b = Node {
name: PathBuf::from_str("b").unwrap(),
size: 0,
children: vec![],
inode_device: None,
depth: 0,
};
let c = Node {
name: PathBuf::from_str("c").unwrap(),
size: 0,
children: vec![],
inode_device: Some((1, 66310)),
depth: 0,
};
assert_eq!(sort_by_inode(&a, &b), Ordering::Greater);
assert_eq!(sort_by_inode(&a, &c), Ordering::Greater);
assert_eq!(sort_by_inode(&c, &b), Ordering::Greater);
assert_eq!(sort_by_inode(&b, &a), Ordering::Less);
assert_eq!(sort_by_inode(&c, &a), Ordering::Less);
assert_eq!(sort_by_inode(&b, &c), Ordering::Less);
}
}

View File

@@ -21,8 +21,11 @@ use regex::Error;
use std::collections::HashSet;
use std::env;
use std::fs::read_to_string;
use std::io;
use std::panic;
use std::process;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::Mutex;
use sysinfo::{System, SystemExt};
@@ -119,17 +122,49 @@ fn main() {
let error_listen_for_ctrlc = Arc::new(Mutex::new(errors));
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 || {
error_listen_for_ctrlc.lock().unwrap().abort = true;
println!("\nAborting");
if cloned_is_in_listing.load(Ordering::Relaxed) {
process::exit(1);
}
})
.expect("Error setting Ctrl-C handler");
let target_dirs = match options.get_many::<String>("params") {
Some(values) => values.map(|v| v.as_str()).collect::<Vec<&str>>(),
None => vec!["."],
is_in_listing.store(true, Ordering::Relaxed);
let target_dirs = match config.get_files_from(&options) {
Some(path) => {
if path == "-" {
let mut targets_to_add = io::stdin()
.lines()
.map_while(Result::ok)
.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()],
},
};
is_in_listing.store(false, Ordering::Relaxed);
let summarize_file_types = options.get_flag("types");
@@ -192,10 +227,10 @@ fn main() {
let limit_filesystem = options.get_flag("limit_filesystem");
let follow_links = options.get_flag("dereference_links");
let simplified_dirs = simplify_dir_names(target_dirs);
let allowed_filesystems = limit_filesystem
.then(|| get_filesystem_devices(simplified_dirs.iter()))
.then(|| get_filesystem_devices(&target_dirs))
.unwrap_or_default();
let simplified_dirs = simplify_dir_names(&target_dirs);
let ignored_full_path: HashSet<PathBuf> = ignore_directories
.into_iter()
@@ -211,11 +246,18 @@ fn main() {
indicator.spawn(output_format.clone())
}
let filter_modified_time = config.get_modified_time_operator(&options);
let filter_accessed_time = config.get_accessed_time_operator(&options);
let filter_changed_time = config.get_created_time_operator(&options);
let walk_data = WalkData {
ignore_directories: ignored_full_path,
filter_regex: &filter_regexs,
invert_filter_regex: &invert_filter_regexs,
allowed_filesystems,
filter_modified_time,
filter_accessed_time,
filter_changed_time,
use_apparent_size: config.get_apparent_size(&options),
by_filecount,
ignore_hidden,
@@ -252,7 +294,6 @@ fn main() {
}
let final_errors = walk_data.errors.lock().unwrap();
let failed_permissions = final_errors.no_permissions;
if !final_errors.file_not_found.is_empty() {
let err = final_errors
.file_not_found
@@ -262,8 +303,20 @@ fn main() {
.join(", ");
eprintln!("No such file or directory: {}", err);
}
if failed_permissions {
eprintln!("Did not have permissions for all directories");
if !final_errors.no_permissions.is_empty() {
if config.get_print_errors(&options) {
let err = final_errors
.no_permissions
.iter()
.map(|a| a.as_ref())
.collect::<Vec<&str>>()
.join(", ");
eprintln!("Did not have permissions for directories: {}", err);
} else {
eprintln!(
"Did not have permissions for all directories (add --print-errors to see errors)"
);
}
}
if !final_errors.unknown_error.is_empty() {
let err = final_errors

View File

@@ -1,8 +1,9 @@
use crate::dir_walker::WalkData;
use crate::platform::get_metadata;
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_regex;
use regex::Regex;
use std::cmp::Ordering;
use std::path::PathBuf;
@@ -19,14 +20,14 @@ pub struct Node {
pub fn build_node(
dir: PathBuf,
children: Vec<Node>,
filter_regex: &[Regex],
invert_filter_regex: &[Regex],
use_apparent_size: bool,
is_symlink: bool,
is_file: bool,
by_filecount: bool,
depth: usize,
walk_data: &WalkData,
) -> Option<Node> {
let use_apparent_size = walk_data.use_apparent_size;
let by_filecount = walk_data.by_filecount;
get_metadata(&dir, use_apparent_size).map(|data| {
let inode_device = if is_symlink && !use_apparent_size {
None
@@ -34,11 +35,19 @@ pub fn build_node(
data.1
};
let size = if is_filtered_out_due_to_regex(filter_regex, &dir)
|| is_filtered_out_due_to_invert_regex(invert_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_symlink && !use_apparent_size)
|| by_filecount && !is_file
{
|| [
(&walk_data.filter_modified_time, data.2 .0),
(&walk_data.filter_accessed_time, data.2 .1),
(&walk_data.filter_changed_time, data.2 .2),
]
.iter()
.any(|(filter_time, actual_time)| {
is_filtered_out_due_to_file_time(filter_time, *actual_time)
}) {
0
} else if by_filecount {
1

View File

@@ -10,15 +10,29 @@ fn get_block_size() -> u64 {
512
}
type InodeAndDevice = (u64, u64);
type FileTime = (i64, i64, i64);
#[cfg(target_family = "unix")]
pub fn get_metadata(d: &Path, use_apparent_size: bool) -> Option<(u64, Option<(u64, u64)>)> {
pub fn get_metadata<P: AsRef<Path>>(
path: P,
use_apparent_size: bool,
) -> Option<(u64, Option<InodeAndDevice>, FileTime)> {
use std::os::unix::fs::MetadataExt;
match d.metadata() {
match path.as_ref().metadata() {
Ok(md) => {
if use_apparent_size {
Some((md.len(), Some((md.ino(), md.dev()))))
Some((
md.len(),
Some((md.ino(), md.dev())),
(md.mtime(), md.atime(), md.ctime()),
))
} else {
Some((md.blocks() * get_block_size(), Some((md.ino(), md.dev()))))
Some((
md.blocks() * get_block_size(),
Some((md.ino(), md.dev())),
(md.mtime(), md.atime(), md.ctime()),
))
}
}
Err(_e) => None,
@@ -26,7 +40,10 @@ pub fn get_metadata(d: &Path, use_apparent_size: bool) -> Option<(u64, Option<(u
}
#[cfg(target_family = "windows")]
pub fn get_metadata(d: &Path, use_apparent_size: bool) -> Option<(u64, Option<(u64, u64)>)> {
pub fn get_metadata<P: AsRef<Path>>(
path: P,
use_apparent_size: bool,
) -> Option<(u64, Option<InodeAndDevice>, FileTime)> {
// 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
// windows defender to scan the file.
@@ -65,7 +82,7 @@ pub fn get_metadata(d: &Path, use_apparent_size: bool) -> Option<(u64, Option<(u
use std::io;
use winapi_util::Handle;
fn handle_from_path_limited<P: AsRef<Path>>(path: P) -> io::Result<Handle> {
fn handle_from_path_limited(path: &Path) -> io::Result<Handle> {
use std::fs::OpenOptions;
use std::os::windows::fs::OpenOptionsExt;
const FILE_READ_ATTRIBUTES: u32 = 0x0080;
@@ -91,30 +108,41 @@ pub fn get_metadata(d: &Path, use_apparent_size: bool) -> Option<(u64, Option<(u
}
fn get_metadata_expensive(
d: &Path,
path: &Path,
use_apparent_size: bool,
) -> Option<(u64, Option<(u64, u64)>)> {
) -> Option<(u64, Option<InodeAndDevice>, FileTime)> {
use winapi_util::file::information;
let h = handle_from_path_limited(d).ok()?;
let h = handle_from_path_limited(path).ok()?;
let info = information(&h).ok()?;
if use_apparent_size {
use filesize::PathExt;
Some((
d.size_on_disk().ok()?,
path.size_on_disk().ok()?,
Some((info.file_index(), info.volume_serial_number())),
(
info.last_write_time().unwrap() as i64,
info.last_access_time().unwrap() as i64,
info.creation_time().unwrap() as i64,
),
))
} else {
Some((
info.file_size(),
Some((info.file_index(), info.volume_serial_number())),
(
info.last_write_time().unwrap() as i64,
info.last_access_time().unwrap() as i64,
info.creation_time().unwrap() as i64,
),
))
}
}
use std::os::windows::fs::MetadataExt;
match d.metadata() {
let path = path.as_ref();
match path.metadata() {
Ok(ref md) => {
const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x20;
const FILE_ATTRIBUTE_READONLY: u32 = 0x01;
@@ -142,11 +170,19 @@ pub fn get_metadata(d: &Path, use_apparent_size: bool) -> Option<(u64, Option<(u
|| md.file_attributes() == FILE_ATTRIBUTE_NORMAL)
&& !((attr_filtered & IS_PROBABLY_ONEDRIVE != 0) && use_apparent_size)
{
Some((md.len(), None))
Some((
md.len(),
None,
(
md.last_write_time() as i64,
md.last_access_time() as i64,
md.creation_time() as i64,
),
))
} else {
get_metadata_expensive(d, use_apparent_size)
get_metadata_expensive(path, use_apparent_size)
}
}
_ => get_metadata_expensive(d, use_apparent_size),
_ => get_metadata_expensive(path, use_apparent_size),
}
}

View File

@@ -70,7 +70,7 @@ impl PAtomicInfo {
#[derive(Default)]
pub struct RuntimeErrors {
pub no_permissions: bool,
pub no_permissions: HashSet<String>,
pub file_not_found: HashSet<String>,
pub unknown_error: HashSet<String>,
pub abort: bool,

View File

@@ -2,13 +2,16 @@ use platform::get_metadata;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use crate::config::DAY_SECONDS;
use crate::dir_walker::Operater;
use crate::platform;
use regex::Regex;
pub fn simplify_dir_names<P: AsRef<Path>>(filenames: Vec<P>) -> HashSet<PathBuf> {
let mut top_level_names: HashSet<PathBuf> = HashSet::with_capacity(filenames.len());
pub fn simplify_dir_names<P: AsRef<Path>>(dirs: &[P]) -> HashSet<PathBuf> {
let mut top_level_names: HashSet<PathBuf> = HashSet::with_capacity(dirs.len());
for t in filenames {
for t in dirs {
let top_level_name = normalize_path(t);
let mut can_add = true;
let mut to_remove: Vec<PathBuf> = Vec::new();
@@ -31,12 +34,12 @@ pub fn simplify_dir_names<P: AsRef<Path>>(filenames: Vec<P>) -> HashSet<PathBuf>
top_level_names
}
pub fn get_filesystem_devices<'a, P: IntoIterator<Item = &'a PathBuf>>(paths: P) -> HashSet<u64> {
pub fn get_filesystem_devices<P: AsRef<Path>>(paths: &[P]) -> HashSet<u64> {
// Gets the device ids for the filesystems which are used by the argument paths
paths
.into_iter()
.iter()
.filter_map(|p| match get_metadata(p, false) {
Some((_size, Some((_id, dev)))) => Some(dev),
Some((_size, Some((_id, dev)), _time)) => Some(dev),
_ => None,
})
.collect()
@@ -62,6 +65,16 @@ 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 {
match filter_time {
(Operater::Equal, bound_time) => {
!(actual_time >= *bound_time && actual_time < *bound_time + DAY_SECONDS)
}
(Operater::GreaterThan, bound_time) => actual_time < *bound_time,
(Operater::LessThan, bound_time) => actual_time > *bound_time,
}
}
pub fn is_filtered_out_due_to_invert_regex(filter_regex: &[Regex], dir: &Path) -> bool {
filter_regex
.iter()
@@ -82,15 +95,15 @@ mod tests {
fn test_simplify_dir() {
let mut correct = HashSet::new();
correct.insert(PathBuf::from("a"));
assert_eq!(simplify_dir_names(vec!["a"]), correct);
assert_eq!(simplify_dir_names(&["a"]), correct);
}
#[test]
fn test_simplify_dir_rm_subdir() {
let mut correct = HashSet::new();
correct.insert(["a", "b"].iter().collect::<PathBuf>());
assert_eq!(simplify_dir_names(vec!["a/b/c", "a/b", "a/b/d/f"]), correct);
assert_eq!(simplify_dir_names(vec!["a/b", "a/b/c", "a/b/d/f"]), correct);
assert_eq!(simplify_dir_names(&["a/b/c", "a/b", "a/b/d/f"]), correct);
assert_eq!(simplify_dir_names(&["a/b", "a/b/c", "a/b/d/f"]), correct);
}
#[test]
@@ -99,7 +112,7 @@ mod tests {
correct.insert(["a", "b"].iter().collect::<PathBuf>());
correct.insert(PathBuf::from("c"));
assert_eq!(
simplify_dir_names(vec![
simplify_dir_names(&[
"a/b",
"a/b//",
"a/././b///",
@@ -118,14 +131,14 @@ mod tests {
correct.insert(PathBuf::from("b"));
correct.insert(["c", "a", "b"].iter().collect::<PathBuf>());
correct.insert(["a", "b"].iter().collect::<PathBuf>());
assert_eq!(simplify_dir_names(vec!["a/b", "c/a/b/", "b"]), correct);
assert_eq!(simplify_dir_names(&["a/b", "c/a/b/", "b"]), correct);
}
#[test]
fn test_simplify_dir_dots() {
let mut correct = HashSet::new();
correct.insert(PathBuf::from("src"));
assert_eq!(simplify_dir_names(vec!["src/."]), correct);
assert_eq!(simplify_dir_names(&["src/."]), correct);
}
#[test]
@@ -133,7 +146,7 @@ mod tests {
let mut correct = HashSet::new();
correct.insert(PathBuf::from("src"));
correct.insert(PathBuf::from("src_v2"));
assert_eq!(simplify_dir_names(vec!["src/", "src_v2"]), correct);
assert_eq!(simplify_dir_names(&["src/", "src_v2"]), correct);
}
#[test]

View File

@@ -1,9 +1,11 @@
use assert_cmd::Command;
use std::ffi::OsStr;
use std::str;
use std::process::Output;
use std::sync::Once;
use std::{fs, io, str};
static INIT: Once = Once::new();
static UNREADABLE_DIR_PATH: &str = "/tmp/unreadable_dir";
/**
* This file contains tests that verify the exact output of the command.
@@ -33,34 +35,58 @@ fn copy_test_data(dir: &str) {
.map_err(|err| eprintln!("Error copying directory for test setup\n{:?}", err));
}
fn create_unreadable_directory() -> io::Result<()> {
#[cfg(unix)]
{
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
fs::create_dir_all(UNREADABLE_DIR_PATH)?;
fs::set_permissions(UNREADABLE_DIR_PATH, Permissions::from_mode(0))?;
}
Ok(())
}
fn initialize() {
INIT.call_once(|| {
copy_test_data("tests/test_dir");
copy_test_data("tests/test_dir2");
copy_test_data("tests/test_dir_unicode");
if let Err(e) = create_unreadable_directory() {
panic!("Failed to create unreadable directory: {}", e);
}
});
}
fn exact_output_test<T: AsRef<OsStr>>(valid_outputs: Vec<String>, command_args: Vec<T>) {
fn run_cmd<T: AsRef<OsStr>>(command_args: &[T]) -> Output {
initialize();
let mut a = &mut Command::cargo_bin("dust").unwrap();
let mut to_run = &mut Command::cargo_bin("dust").unwrap();
for p in command_args {
a = a.arg(p);
to_run = to_run.arg(p);
}
to_run.unwrap()
}
let output = str::from_utf8(&a.unwrap().stdout).unwrap().to_owned();
fn exact_stdout_test<T: AsRef<OsStr>>(command_args: &[T], valid_stdout: Vec<String>) {
let to_run = run_cmd(command_args);
let will_fail = valid_outputs.iter().any(|i| output.contains(i));
let stdout_output = str::from_utf8(&to_run.stdout).unwrap().to_owned();
let will_fail = valid_stdout.iter().any(|i| stdout_output.contains(i));
if !will_fail {
eprintln!(
"output:\n{}\ndoes not contain any of:\n{}",
output,
valid_outputs.join("\n\n")
"output(stdout):\n{}\ndoes not contain any of:\n{}",
stdout_output,
valid_stdout.join("\n\n")
);
}
assert!(will_fail)
assert!(will_fail);
}
fn exact_stderr_test<T: AsRef<OsStr>>(command_args: &[T], valid_stderr: String) {
let to_run = run_cmd(command_args);
let stderr_output = str::from_utf8(&to_run.stderr).unwrap().trim();
assert_eq!(stderr_output, valid_stderr);
}
// "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
@@ -68,20 +94,20 @@ fn exact_output_test<T: AsRef<OsStr>>(valid_outputs: Vec<String>, command_args:
#[test]
pub fn test_main_basic() {
// -c is no color mode - This makes testing much simpler
exact_output_test(main_output(), vec!["-c", "-B", "/tmp/test_dir/"])
exact_stdout_test(&["-c", "-B", "/tmp/test_dir/"], main_output());
}
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_main_multi_arg() {
let command_args = vec![
let command_args = [
"-c",
"-B",
"/tmp/test_dir/many/",
"/tmp/test_dir",
"/tmp/test_dir",
];
exact_output_test(main_output(), command_args);
exact_stdout_test(&command_args, main_output());
}
fn main_output() -> Vec<String> {
@@ -111,8 +137,8 @@ fn main_output() -> Vec<String> {
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_main_long_paths() {
let command_args = vec!["-c", "-p", "-B", "/tmp/test_dir/"];
exact_output_test(main_output_long_paths(), command_args);
let command_args = ["-c", "-p", "-B", "/tmp/test_dir/"];
exact_stdout_test(&command_args, main_output_long_paths());
}
fn main_output_long_paths() -> Vec<String> {
@@ -139,8 +165,8 @@ fn main_output_long_paths() -> Vec<String> {
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_substring_of_names_and_long_names() {
let command_args = vec!["-c", "-B", "/tmp/test_dir2"];
exact_output_test(no_substring_of_names_output(), command_args);
let command_args = ["-c", "-B", "/tmp/test_dir2"];
exact_stdout_test(&command_args, no_substring_of_names_output());
}
fn no_substring_of_names_output() -> Vec<String> {
@@ -173,8 +199,8 @@ fn no_substring_of_names_output() -> Vec<String> {
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_unicode_directories() {
let command_args = vec!["-c", "-B", "/tmp/test_dir_unicode"];
exact_output_test(unicode_dir(), command_args);
let command_args = ["-c", "-B", "/tmp/test_dir_unicode"];
exact_stdout_test(&command_args, unicode_dir());
}
fn unicode_dir() -> Vec<String> {
@@ -200,8 +226,8 @@ fn unicode_dir() -> Vec<String> {
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_apparent_size() {
let command_args = vec!["-c", "-s", "-b", "/tmp/test_dir"];
exact_output_test(apparent_size_output(), command_args);
let command_args = ["-c", "-s", "-b", "/tmp/test_dir"];
exact_stdout_test(&command_args, apparent_size_output());
}
fn apparent_size_output() -> Vec<String> {
@@ -222,3 +248,26 @@ fn apparent_size_output() -> Vec<String> {
vec![one_space_before, two_space_before]
}
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_permission_normal() {
let command_args = [UNREADABLE_DIR_PATH];
let permission_msg =
r#"Did not have permissions for all directories (add --print-errors to see errors)"#
.trim()
.to_string();
exact_stderr_test(&command_args, permission_msg);
}
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_permission_flag() {
// add the flag to CLI
let command_args = ["--print-errors", UNREADABLE_DIR_PATH];
let permission_msg = format!(
"Did not have permissions for directories: {}",
UNREADABLE_DIR_PATH
);
exact_stderr_test(&command_args, permission_msg);
}