Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ compare against the true GNU coreutils version on the Linux-based tests first.
| | dir | Briefly list directory contents | |
| ✓ | dircolors | Color setup for ls | ✓ |
| ✓ | dirname | Strip last file name component | ✓ |
| | du | Estimate file space usage | ✓ |
| ✓ | du | Estimate file space usage | ✓ |
| ✓ | echo | Print a line of text | ✓ |
| ✓ | env | Run a command in a modified environment | ✓ |
| ✓ | expand | Convert tabs to spaces | ✓ |
Expand Down
174 changes: 174 additions & 0 deletions common/du/args.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
module du

import common

pub fn parse_args(args []string) Settings {
mut s := Settings{}
mut operands := []string{}

for i := 0; i < args.len; i++ {
a := args[i]
if a == '--' {
operands << args[i + 1..]
break
}
if a == '--help' {
print_help()
exit(0)
}
if a == '--version' {
println('du (V coreutils) ${common.version}')
exit(0)
}
if !a.starts_with('-') || a == '-' {
operands << a
continue
}

if a.starts_with('--') {
name := a.all_before('=')
val := a.all_after('=')
has_val := a.contains('=')
match name {
'--all' {
s.all = true
}
'--bytes' {
s.mode = .bytes
}
'--summarize' {
s.summary = true
}
'--total' {
s.total = true
}
'--human-readable' {
s.mode = .human
}
'--si' {
s.mode = .si_human
}
'--apparent-size' {
s.mode = .bytes
}
'--block-size' {
value, next := take_value(args, i, val, has_val, name)
i = next
s.mode = .blocks
s.block_size = value.int()
}
'--max-depth' {
value, next := take_value(args, i, val, has_val, name)
i = next
s.max_depth = value.int()
}
else {
common.exit_with_error_message('du', "unrecognized option '${a}'")
}
}
continue
}

// Clusters such as -sh and -ab are ordinary usage, so short options
// are expanded here; a value-taking option in a cluster takes the
// rest of the same word as its value.
mut j := 1
for j < a.len {
c := a[j]
match c {
`a` {
s.all = true
}
`b` {
s.mode = .bytes
}
`c` {
s.total = true
}
`h` {
s.mode = .human
}
`H` {
s.mode = .si_human
}
`k` {
s.mode = .kibibytes
}
`m` {
s.mode = .mebibytes
}
`g` {
s.mode = .gibibytes
}
`s` {
s.summary = true
}
`D`, `d` {
rest := a[j + 1..]
mut value := rest
if rest == '' {
if i + 1 >= args.len {
common.exit_with_error_message('du', "option requires an argument -- '${c.ascii_str()}'")
}
i++
value = args[i]
}
s.max_depth = value.int()
j = a.len
continue
}
`B` {
rest := a[j + 1..]
mut value := rest
if rest == '' {
if i + 1 >= args.len {
common.exit_with_error_message('du', "option requires an argument -- '${c.ascii_str()}'")
}
i++
value = args[i]
}
s.mode = .blocks
s.block_size = value.int()
j = a.len
continue
}
else {
common.exit_with_error_message('du', "invalid option -- '${c.ascii_str()}'")
}
}
j++
}
}

s.operands = operands
return s
}

fn take_value(args []string, i int, inline_val string, has_val bool, name string) (string, int) {
if has_val {
return inline_val, i
}
if i + 1 >= args.len {
common.exit_with_error_message('du', "option '${name}' requires an argument")
}
return args[i + 1], i + 1
}

fn print_help() {
println('Usage: du [OPTION]... [FILE]...')
println('Summarize device usage of the set of FILEs, recursively for directories.')
println('')
println(' -a, --all write counts for all files, not just directories')
println(' --apparent-size print apparent sizes rather than device usage')
println(' -B, --block-size=SIZE scale sizes by SIZE before printing them')
println(' -b, --bytes equivalent to --apparent-size --block-size=1')
println(' -c, --total produce a grand total')
println(' -d, --max-depth=N print the total for a directory only if it is')
println(' N or fewer levels below the command line argument')
println(' --files0-from=F summarize device usage of the NUL-terminated file')
println(' --si like -h, but use powers of 1000 not 1024')
println(' -s, --summarize display only a total for each argument')
println(' -h, --human-readable print sizes in human readable format')
println(' --help display this help and exit')
println(' --version output version information and exit')
}
189 changes: 189 additions & 0 deletions common/du/du.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
module du

import os

// Block-size output mode. -b is a mode rather than a flag because it means
// "one byte per block", which overrides -k/-B the way -i does in df.
pub enum Mode {
human
si_human
kibibytes
mebibytes
gibibytes
blocks
bytes
}

pub struct Settings {
pub mut:
mode Mode = .kibibytes
block_size int = 1024
summary bool // -s: one line per operand only
all bool // -a: include every file, not just directories
total bool // -c, --total: add a grand total line
max_depth int = max_depth_unset
operands []string
}

const max_depth_unset = -1

// report walks the operands and returns the table text plus whether any path
// failed. GNU reports a failure and keeps going with the other operands, then
// exits 1, so the two are kept separate rather than folded into an error.
pub fn report(set Settings) (string, bool) {
mut lines := []string{}
mut grand := u64(0)
mut failed := false

mut roots := []string{}
if set.operands.len == 0 {
roots = ['.']
} else {
roots = set.operands.clone()
}

for root in roots {
st := os.lstat(root) or {
lines << 'du: cannot access ${root}: ${reason(err)}'
failed = true
continue
}
mut size := u64(0)
if st.get_filetype() == .directory {
size = walk_dir(root, 0, set, mut lines)
lines << row(size, root, set)
} else {
// A file named on the command line is always shown, -a or not.
size = st.size
lines << row(size, root, set)
}
grand += size
}

if set.total {
lines << row(grand, 'total', set)
}
return lines.join('\n'), failed
}

// walk_dir returns the total size of everything under dir, appending one line
// per entry the depth and -a settings ask for.
fn walk_dir(dir string, depth int, set Settings, mut lines []string) u64 {
entries := os.ls(dir) or { return 0 }.sorted()

mut sum := u64(0)
for name in entries {
child := os.join_path(dir, name)
st := os.lstat(child) or {
lines << 'du: cannot access ${child}: ${reason(err)}'
continue
}
if st.get_filetype() == .directory {
sub := walk_dir(child, depth + 1, set, mut lines)
sum += sub
if !set.summary && within_depth(depth + 1, set) {
lines << row(sub, child, set)
}
} else {
sum += st.size
// -s asks for one line per operand, so files under it are not
// listed even with -a.
if set.all && !set.summary && within_depth(depth + 1, set) {
lines << row(st.size, child, set)
}
}
}
return sum
}

// within_depth reports whether an entry this far below the operand should be
// printed. The operand itself is depth 0 and is always printed.
fn within_depth(depth int, set Settings) bool {
return set.max_depth < 0 || depth <= set.max_depth
}

// human_readable prints at most four significant characters: one decimal
// below 10 and none at or above it. si selects powers of 1000 over 1024.
//
// This duplicates common/df's copy deliberately. Each util in this repository
// has to merge to main on its own, so depending on another util's unmerged
// module would block the pull request rather than share the code. Once both
// have landed this is worth folding into one place.
pub fn human_readable(bytes u64, si bool) string {
base := if si { u64(1000) } else { u64(1024) }
// --si keeps the SI 'k' but the conventional M, G, T for the rest;
// --human-readable uses K for the first unit.
units := if si { 'kMGTPEZY' } else { 'KMGTPEZY' }

mut value := f64(bytes)
mut idx := 0
for idx < units.len && value >= f64(base) {
value /= f64(base)
idx++
}

mut body := if idx > 0 && value < 10.0 {
'${value:.1f}'
} else {
'${value:.0f}'
}
if idx > 0 {
body += units[idx - 1].ascii_str()
}
return body
}

fn row(size u64, path string, set Settings) string {
return '${format_size(size, set)}\t${path}'
}

pub fn format_size(bytes u64, set Settings) string {
match set.mode {
.bytes {
return bytes.str()
}
.human {
return human_readable(bytes, false)
}
.si_human {
return human_readable(bytes, true)
}
.blocks {
if set.block_size <= 0 {
return bytes.str()
}
return (bytes / u64(set.block_size)).str()
}
else {
mut div := u64(1024)
match set.mode {
.mebibytes { div = u64(1024) * 1024 }
.gibibytes { div = u64(1024) * 1024 * 1024 }
else {}
}
return ceil_div(bytes, div).str()
}
}
}

// ceil_div rounds up, because a path of one byte still occupies a whole block
// and GNU reports that as one block rather than zero.
fn ceil_div(bytes u64, div u64) u64 {
if div == 0 {
return bytes
}
return (bytes + div - 1) / div
}

// reason maps a V error message onto the wording GNU uses, so the diagnostics
// read like the rest of this port's output.
fn reason(err IError) string {
m := err.msg()
if m.contains('cannot find the file') {
return 'No such file or directory'
}
if m.contains('Access is denied') {
return 'Permission denied'
}
return m
}
11 changes: 11 additions & 0 deletions src/du/du.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import common.du as dumod
import os

fn main() {
set := dumod.parse_args(os.args[1..])
output, failed := dumod.report(set)
println(output)
if failed {
exit(1)
}
}
Loading
Loading