ls: bring the listing, the long format and the option parser up to GNU - #201
Merged
Merged
Conversation
main does not build with V 0.5.2, and `make testfmt` is red too, so every
CI job has been failing. This gets both gates back to green without changing
any intended behaviour.
Compile fixes, one per cause:
- `v.mathutil` is gone; `min`/`max` now live in `math`. Affects basenc, cut
and tail.
- `for ; cond; post {}` and `for i := n; i; i-- {}` are no longer accepted:
the condition has to be `bool`. Affects cksum and expand.
- `time.sleep` takes a `time.Duration`, not a number of seconds. sleep now
converts explicitly and saturates instead of overflowing, so `sleep inf`
keeps waiting rather than wrapping around to roughly zero.
- `const` cannot hold the result of a function call, so printenv's NUL
terminator became a function.
- `setup_cp_command`/`setup_mv_command`/`setup_command` (head) were declared
with `?`, but every failure path in them calls the @[noreturn]
`common.exit_with_error_message`, so they never return an error and their
callers had no `err` to look at. They now return plain tuples.
- `C.statvfs` is already declared privately by vlib/builtin/cfns.c.v, so a
local declaration resolved to that one and failed to compile. stat binds a
distinct name to the libc call with #define.
- `struct utmpx` was declared twice, in common/readutmp_nix.c.v and again in
src/users/users.c.v with a different, shorter field list, which V now
rejects as a redeclaration. Declared once, with the full layout.
stat additionally needed more than a compile fix:
- src/stat/stat_to_vlib.v (now src/stat/modes.v) imported v.scanner and
v.pref to decode --printf escapes. Building it pulled the whole V compiler
into the program: `v build` of src/stat never finished, and taking the
machine down with it. The escapes are now decoded directly.
- `<bits/statx.h>` is a glibc internal header, so it is absent on musl based
distributions (issue vlang#145). The kernel uapi `<linux/stat.h>` is used instead.
- `statx()` wrote the kernel's 224 byte struct through a `voidptr` into a
V struct that was 160 bytes, so every call corrupted memory past the end of
the allocation; stat segfaulted on every invocation. The C struct is now
declared and used for real. `struct statvfs` has the same problem and its
V mirror now matches the ABI including the trailing spare.
- `$embed_file` no longer resolves inside a function body, so fstypes.txt is
embedded at module scope.
wc needed a fix of its own:
- `read_chunk` returned `?FileChunk`, but its caller inspected `err`, which
does not exist for an option. os.File.read also reports end of file as an
`os.Eof` error, and reading a zero-length chunk indexed `buffer[-1]`, so
`wc` on an empty file would have panicked. It now returns `!FileChunk` and
distinguishes exhaustion from a real read error.
- `\r` was matched both as "ignore" and as whitespace, which V rejects as a
duplicate case.
- `byte(bool)` is no longer a valid conversion; the single-count check counts
the selected options instead.
Finally, `v fmt -w .` over the 16 files that `v fmt -verify` rejected.
With this, `make` builds all 76 utilities and `make testfmt` passes.
Takes over vlang#183, whose author handed it on, and moves it onto current V so that it builds at all. The option parser used `v.mathutil`, which no longer exists, six helpers each shadowed `max` with a local and then called `max`, and a `C.readlink` that vlib/os now declares with a different signature. On top of that, the behaviour was compared against GNU ls 9.4 on the same machine over a matrix of 49 invocations. It started at 0 of 49 and is now 39 of them. Every number below was read off the reference. Where the first reading turned out to be wrong, the measurement is named rather than the correction. ## Things that were simply wrong * The type character came from `entry.exe`, which means "has the execute bit", so a 755 file was listed as `xrwxr-xr-x`. GNU has no `x` type at all. * A symlink's size was the size of its target, so a link to `/nonexistent` showed 0. It is the link's own size, which is the length of the target string: 12 for `/nonexistent`, 9 for `plain.txt`. * Every column was padded to the widest entry, so every line ended in spaces. GNU pads all but the last column of a row and nothing else. * Columns were laid out even with the output redirected. `ls | cat` gives one entry per line at GNU; only a terminal, or `-C`, gets a grid. * An empty directory lost its section header entirely, because it produces no entries to group by. `ls emptydir anotherdir` printed only the other one. * `os.join_path` rewrites a backslash in a name into a separator, so `./back\slash` became `./back/slash` and the entry failed to stat, printing as `?---------`. Joining on `/` directly is what fixed it. * An operand that could not be reached was listed anyway and the exit status stayed 0. GNU prints `cannot access` and exits 2. ## The long format `total` is the sum of the disk blocks in 1K units, not of the file lengths: a 200000 byte file on a 4K filesystem contributes 196, not 195. It has no colon. V's `os.Stat` has no `st_blocks`, so the field is read from a `C.stat` declared alongside the one vlib declares — the field lists have to match exactly or vlib's own call to `C.lstat` stops type checking. `-h` and `--si` were the same calculation with the base and the suffix swapped: `-h` divides by 1024 and writes `K`, `--si` divides by 1000 and writes `k`. The rounding is a ceiling, not to nearest. GNU prints 2500 bytes as 2.5K where nearest would give 2.4K, and 10240 as 10K while 10241 is 11K; one decimal is kept below ten and dropped at or above. All 33 sizes sampled from the reference fit that, and no nearest-rounding rule does. The day is padded with a space, `Oct 3`, because GNU uses `%e`; V's `custom_format` has no token for that (`_2` is passed through literally, and `2` gives an unpadded day). Anything older than six calendar months shows the year instead of the time of day. That boundary is calendar months, not a fixed count of days: with now = 2026-10-03 the reference showed a time for 2026-04-04 and a year for 2026-04-01. ## Quoting `--quoting-style` was not implemented and exited 2. Each style has its own rule and all of them were measured: * `literal` never quotes. There is no `none`; GNU 9.4 rejects it and lists the ten words it does accept. * `shell` quotes a name only when it holds something outside the set a shell treats literally, in single quotes, or in double quotes when the name itself contains a single quote. A newline stays inside the quotes: `'new<newline> line'`, with the quotes still open on the far side. * `shell-escape` closes the run at a byte that is not printable ASCII and writes `$'\n'`, `$'\t'` or a three digit octal escape, so a name with a tab is `'tab'$'\t''name'` and `ü` is `''$'\303\274''n'`. * `c` is always double quoted with C escapes. `-Q` is this, not shell quoting: `ls -Q` prints `"new\nline"`, which is worth knowing. The default follows the destination, shell-escape to a terminal and literal to a pipe. In the long listing the link target is quoted as well as the name. ## Added `-F` and `-d` were absent, and both are ordinary rather than exotic. `-F` marks directories, links, sockets, fifos and devices, executables and unknown types, and leaves an ordinary file alone; in the long listing it marks everything except a link, which already has `-> target` after it. `-d` lists the operand rather than its contents, so a bare `ls -d` prints `.`. An earlier comment on this PR said GNU sorts names case-insensitively, and the default arm did compare byte-wise. Measuring it settled the question the other way: with a directory holding `Apple BANANA ZZZ _under aPPle apple banana`, GNU prints them in that order under `LC_ALL=C`, `C.UTF-8` and `en_US.UTF-8` alike, which is byte order — folding case would have put `_under` second. The report appears to have come from a case-insensitive filesystem, where `Apple` and `apple` cannot both exist. The closure in `sort` is now bound through a typed variable. V 0.5.2 does not give the arms of a `match` of closures a type it will pass to `sorted_with_compare`: it looks for a function named after the variable instead and reports `Uint128.cmp`, which has nothing to do with this module. ## Testing 49 invocations over one directory holding a directory, an empty directory, a dangling and a working symlink, an executable, a 3000 and a 200000 byte file, names with a space, a single quote, a dollar, a backslash, a semicolon, an asterisk, a tab and an embedded newline, each with a distinct mtime so that `-t` compares times rather than nanosecond ties. Both binaries run in the same directory with the same arguments. 39 match. The ten that do not, and why: * `--time-style=iso`, `=long-iso` and `=full-iso` are not implemented. This is a gap, not a difference of opinion; the port has its own `time_iso` and compact formats but never read the option. `octal` is in this group only because the case was written with `--time-style` in it. * `-C`, `-x` and `-w` size every column to the widest name in the whole listing. GNU sizes each column to its own contents, which is how it fits six entries to a line where this fits five. * `-m` wraps at the width, but GNU picks where to break from the same column calculation as `-C`, so the two disagree about where the third line ends. * `-li -l` and `-AC` fail in the parser. A repeated short flag makes `flag.to_struct` panic, and a bundled one is not expanded. That is vlib's, not this module's. `entry_test.v` asserted the old human-readable sizes, including the `b` suffix GNU does not print and 1024-based maths under the `--si` flag. It now pins the values read off the reference, including the two places where a nearest rounding would disagree. ## Still the author's extras The icons, the checksum column, the bordered table format and `--table` are left as they were. They are not GNU options and are not compared here. Stacked on vlang#195 for the build fixes. `v fmt -verify .` passes over `src/ls`, `v -o bin/ls ./ls` builds, and both `src/ls` test files pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #183, whose author handed it on.
Takes over #183 and moves it onto current V so that it builds at all. The
option parser used
v.mathutil, which no longer exists, six helpers eachshadowed
maxwith a local and then calledmax, and aC.readlinkthatvlib/os now declares with a different signature. On top of that, the
behaviour was compared against GNU ls 9.4 on the same machine over a matrix
of 49 invocations. It started at 0 of 49 and is now 39 of them.
Every number below was read off the reference. Where the first reading
turned out to be wrong, the measurement is named rather than the correction.
Things that were simply wrong
entry.exe, which means "has the execute bit",so a 755 file was listed as
xrwxr-xr-x. GNU has noxtype at all./nonexistentshowed 0. It is the link's own size, which is the length of the target
string: 12 for
/nonexistent, 9 forplain.txt.GNU pads all but the last column of a row and nothing else.
ls | catgives oneentry per line at GNU; only a terminal, or
-C, gets a grid.entries to group by.
ls emptydir anotherdirprinted only the other one.os.join_pathrewrites a backslash in a name into a separator, so./back\slashbecame./back/slashand the entry failed to stat, printingas
?---------. Joining on/directly is what fixed it.stayed 0. GNU prints
cannot accessand exits 2.The long format
totalis the sum of the disk blocks in 1K units, not of the file lengths: a200000 byte file on a 4K filesystem contributes 196, not 195. It has no colon.
V's
os.Stathas nost_blocks, so the field is read from aC.statdeclaredalongside the one vlib declares — the field lists have to match exactly or
vlib's own call to
C.lstatstops type checking.-hand--siwere the same calculation with the base and the suffix swapped:-hdivides by 1024 and writesK,--sidivides by 1000 and writesk.The rounding is a ceiling, not to nearest. GNU prints 2500 bytes as 2.5K where
nearest would give 2.4K, and 10240 as 10K while 10241 is 11K; one decimal is
kept below ten and dropped at or above. All 33 sizes sampled from the reference
fit that, and no nearest-rounding rule does.
The day is padded with a space,
Oct 3, because GNU uses%e; V'scustom_formathas no token for that (_2is passed through literally, and2gives an unpadded day). Anything older than six calendar months shows theyear instead of the time of day. That boundary is calendar months, not a fixed
count of days: with now = 2026-10-03 the reference showed a time for 2026-04-04
and a year for 2026-04-01.
Quoting
--quoting-stylewas not implemented and exited 2. Each style has its own ruleand all of them were measured:
literalnever quotes. There is nonone; GNU 9.4 rejects it and lists theten words it does accept.
shellquotes a name only when it holds something outside the set a shelltreats literally, in single quotes, or in double quotes when the name itself
contains a single quote. A newline stays inside the quotes:
'new<newline> line', with the quotes still open on the far side.shell-escapecloses the run at a byte that is not printable ASCII andwrites
$'\n',$'\t'or a three digit octal escape, so a name with a tabis
'tab'$'\t''name'andüis''$'\303\274''n'.cis always double quoted with C escapes.-Qis this, not shell quoting:ls -Qprints"new\nline", which is worth knowing.The default follows the destination, shell-escape to a terminal and literal to a
pipe. In the long listing the link target is quoted as well as the name.
Added
-Fand-dwere absent, and both are ordinary rather than exotic.-Fmarksdirectories, links, sockets, fifos and devices, executables and unknown types,
and leaves an ordinary file alone; in the long listing it marks everything
except a link, which already has
-> targetafter it.-dlists the operandrather than its contents, so a bare
ls -dprints..An earlier comment on this PR said GNU sorts names case-insensitively, and the
default arm did compare byte-wise. Measuring it settled the question the other
way: with a directory holding
Apple BANANA ZZZ _under aPPle apple banana, GNUprints them in that order under
LC_ALL=C,C.UTF-8anden_US.UTF-8alike,which is byte order — folding case would have put
_undersecond. The reportappears to have come from a case-insensitive filesystem, where
Appleandapplecannot both exist.The closure in
sortis now bound through a typed variable. V 0.5.2 does notgive the arms of a
matchof closures a type it will pass tosorted_with_compare: it looks for a function named after the variable insteadand reports
Uint128.cmp, which has nothing to do with this module.Testing
49 invocations over one directory holding a directory, an empty directory, a
dangling and a working symlink, an executable, a 3000 and a 200000 byte file,
names with a space, a single quote, a dollar, a backslash, a semicolon, an
asterisk, a tab and an embedded newline, each with a distinct mtime so that
-tcompares times rather than nanosecond ties. Both binaries run in the samedirectory with the same arguments.
39 match. The ten that do not, and why:
--time-style=iso,=long-isoand=full-isoare not implemented. This isa gap, not a difference of opinion; the port has its own
time_isoandcompact formats but never read the option.
octalis in this group onlybecause the case was written with
--time-stylein it.-C,-xand-wsize every column to the widest name in the wholelisting. GNU sizes each column to its own contents, which is how it fits six
entries to a line where this fits five.
-mwraps at the width, but GNU picks where to break from the same columncalculation as
-C, so the two disagree about where the third line ends.-li -land-ACfail in the parser. A repeated short flag makesflag.to_structpanic, and a bundled one is not expanded. That is vlib's,not this module's.
entry_test.vasserted the old human-readable sizes, including thebsuffixGNU does not print and 1024-based maths under the
--siflag. It now pins thevalues read off the reference, including the two places where a nearest
rounding would disagree.
Still the author's extras
The icons, the checksum column, the bordered table format and
--tableareleft as they were. They are not GNU options and are not compared here.
Stacked on #195 for the build fixes.
v fmt -verify .passes oversrc/ls,v -o bin/ls ./lsbuilds, and bothsrc/lstest files pass.