[mike@maginot]

This commit is contained in:
2026-08-10 21:51:53 +02:00
parent d26a008763
commit 2bd9a4a169
4 changed files with 292 additions and 296 deletions
+156 -160
View File
@@ -1,7 +1,7 @@
# dx
Zählt, was in einem Verzeichnis liegt, und wie schwer es ist — eine Ebene tief,
ein Blick, sofort fertig.
Counts what lies in a directory, and how heavy it is — one level deep, one
glance, done at once.
```
$ dx ~/src -r -t 8
@@ -17,15 +17,15 @@ $ dx ~/src -r -t 8
… 16 more 1305 179M █▋ 16%
```
Links der Name, dann die Zahl der enthaltenen Dateien, dann die Summe ihrer
Größen, dann der Anteil am Ganzen. Die Kopfzeile ist das Verzeichnis selbst.
Verzeichnisse werden vollständig durchlaufen, aber nicht aufgeklappt — für
alles Weitere ist `ncdu` da.
On the left the name, then the number of files inside, then the sum of their
sizes, then the share of the whole. The header line is the directory itself.
Directories are walked through completely, but not unfolded — for everything
beyond that there is `ncdu`.
Im Terminal ist die Ausgabe farbig, nach der Palette aus
In a terminal the output is coloured, after the palette from
[mwxcol](../mwxcol/README.md).
## Bauen und installieren
## Building and installing
```sh
cd ~/src/dx
@@ -33,83 +33,82 @@ cd ~/src/dx
cp bin/dx ~/bin/dx
```
`build.sh` baut nach `./bin`, je ein Binary für macOS und Linux auf arm64 und
amd64, und legt `bin/dx` als Symlink auf das für diese Maschine. Für eine
einzelne Plattform:
`build.sh` builds into `./bin`, one binary each for macOS and Linux on arm64
and amd64, and puts `bin/dx` there as a symlink to the one for this machine.
For a single platform:
```sh
PLATFORMS="linux/amd64" ./build.sh
```
Jeder Lauf zählt die Patch-Nummer in `version.txt` um eins hoch und baut sie
per `-ldflags` in alle Binaries ein — `dx --version` sagt also, welcher Bau
läuft. Ein nacktes `go build` geht auch, meldet dann aber die Vorgabe aus
`main.go` statt der wirklichen Nummer.
Every run counts the patch number in `version.txt` up by one and builds it into
all the binaries via `-ldflags` — so `dx --version` tells which build is
running. A bare `go build` works too, but then reports the default from
`main.go` instead of the real number.
Braucht Go ≥ 1.26 und `github.com/fatih/color`. Das Perl-Original (`dx`, 1.0.2,
2016) liegt unverändert daneben und wird vom Bau nicht angefasst; es hat keine
Farben und keine Balken, tut aber sonst dasselbe.
Needs Go ≥ 1.26 and `github.com/fatih/color`. The Perl original (`dx`, 1.0.2,
2016) lies next to it unchanged and is not touched by the build; it has no
colours and no bars, but otherwise does the same thing.
## Selbst aktualisieren
## Updating itself
```sh
dx --check-update # nur nachsehen
dx --update # holen und ersetzen
dx --check-update # only look
dx --update # fetch and replace
```
`dx` holt sich das neueste Release von
[git.micw.org/mike/dx](https://git.micw.org/mike/dx) — die URL steht fest im
Programm, es gibt nichts zu konfigurieren. Gebraucht wird ein Release je
Version, dessen Tag die nackte Nummer ist (`2.1.6`), mit den Dateien aus `./bin`
als Assets; gesucht wird die eine, die zu `GOOS`/`GOARCH` dieser Maschine passt.
`dx` fetches the newest release from
[git.micw.org/mike/dx](https://git.micw.org/mike/dx) — the URL sits fixed in the
program, there is nothing to configure. What it needs is one release per
version, whose tag is the bare number (`2.1.6`), with the files from `./bin` as
its assets; the one looked for is the one matching `GOOS`/`GOARCH` of this
machine.
Ersetzt wird die laufende Datei selbst. Ist der aufgerufene `dx` ein Symlink —
etwa `~/bin/dx` auf `~/src/dx/bin/dx-darwin-arm64` —, wird das Ziel dahinter
erneuert, nicht der Link. Vor dem Tausch wird das frisch Geladene einmal mit
`--version` aufgerufen; meldet es nicht die erwartete Nummer, bleibt alles, wie
es war. Der Tausch selbst ist ein `rename` im selben Verzeichnis, also atomar:
entweder die alte Datei oder die neue, nie eine halbe.
What gets replaced is the running file itself. If the `dx` that was called is a
symlink — say `~/bin/dx` pointing at `~/src/dx/bin/dx-darwin-arm64` — the target
behind it is renewed, not the link. Before the swap, what was freshly fetched is
called once with `--version`; if it does not report the expected number,
everything stays as it was. The swap itself is a `rename` within the same
directory, hence atomic: either the old file or the new one, never half of one.
Liegt das Binary irgendwo, wo der eigene Benutzer nicht schreiben darf
(`/usr/local/bin`), sagt `dx --update` das und tut nichts — dann `sudo`.
If the binary lies somewhere the user may not write to (`/usr/local/bin`),
`dx --update` says so and does nothing — then `sudo`.
### Einmal am Tag von selbst
### Once a day, by itself
Ohne Zutun sieht `dx` einmal am Tag nach und sagt danach am Ende der Ausgabe auf
stderr Bescheid:
Without being asked, `dx` looks once a day and afterwards says so at the end of
the output, on stderr:
```
dx 2.1.9 is available, run 'dx --update'
```
Der Lauf im Vordergrund fasst dafür nie das Netz an. Er liest nur einen
Merkzettel — `~/Library/Caches/dx/update.json`, unter Linux
`~/.cache/dx/update.json` — und wenn der älter als einen Tag ist, startet er
nebenbei `dx --update-refresh`: denselben Läufer noch einmal, abgekoppelt, ohne
Ausgabe, nur zum Fragen. Auf dessen Antwort wartet niemand, sie steht beim
nächsten Aufruf im Merkzettel. `dx` bleibt damit genau so schnell wie vorher,
auch wenn der Server gerade schweigt.
The run in the foreground never touches the network for this. It only reads a
note — `~/Library/Caches/dx/update.json`, on Linux `~/.cache/dx/update.json` —
and when that one is older than a day, it starts `dx --update-refresh` on the
side: the same runner once more, detached, without output, only to ask. Nobody
waits for its answer; it will be in the note at the next call. `dx` thereby
stays exactly as fast as before, even when the server happens to be silent.
Der Zeitstempel wandert weiter, *bevor* gefragt wird. Zwei gleichzeitige Läufe
starten deshalb nur eine Abfrage, und ein Server, der nicht antwortet, wird
morgen wieder gefragt und nicht bei jedem Aufruf. Lässt sich der Merkzettel
nicht schreiben, unterbleibt die Frage ganz — sonst hinge an einem
schreibgeschützten Cache-Verzeichnis ein Prozess je Aufruf.
The timestamp moves on *before* the asking. Two simultaneous runs therefore
start one query, not two, and a server that does not answer is asked again
tomorrow rather than on every call. If the note cannot be written, the question
is dropped entirely — otherwise a write-protected cache directory would mean one
process per call.
Gefragt und gesagt wird nur, wenn stderr am Terminal hängt. In einer Pipe, in
einem Skript und unter cron ist Ruhe, und `DX_NO_UPDATE_CHECK=1` schaltet es
ganz ab.
Asking and speaking happen only when stderr hangs on a terminal. In a pipe, in a
script and under cron there is quiet, and `DX_NO_UPDATE_CHECK=1` turns it off
altogether.
Das kostet Platz: `net/http` samt TLS macht aus einem 1,8-MB-Binary eins von
6,2 MB. Für ein Werkzeug, das ohnehin auf der Platte liegt, ist das der Preis
dafür, sich selbst erneuern zu können — wer ihn nicht zahlen will, lässt
`selfupdate.go` weg.
This costs space: `net/http` together with TLS turns a 1.8 MB binary into one of
6.2 MB. For a tool that sits on the disk anyway, that is the price of being able
to renew itself — whoever will not pay it leaves `selfupdate.go` out.
## Wiederverwenden
## Reusing it
`selfupdate.go` hängt an nichts im Rest des Programms und kommt mit der
Standardbibliothek aus. Für ein anderes Programm die Datei kopieren, den Block
oben anpassen —
`selfupdate.go` hangs on nothing in the rest of the program and gets by with the
standard library. For another program, copy the file, adjust the block at the
top —
```go
var selfUpdate = selfUpdater{
@@ -122,8 +121,8 @@ var selfUpdate = selfUpdater{
}
```
Dazu drei Zeilen im Optionenteil — `--update`, `--check-update` und das
versteckte `updateRefreshFlag` — und eine am Ende des Programms:
— plus three lines in the options part: `--update`, `--check-update` and the
hidden `updateRefreshFlag`, and one at the end of the program:
```go
if hint := selfUpdate.daily(); hint != "" {
@@ -131,134 +130,131 @@ if hint := selfUpdate.daily(); hint != "" {
}
```
`daily` liefert nur den Text; wie er aussieht, entscheidet das Programm. Alles
Weitere in der Datei heißt `selfUpdate…` oder `update…` und kollidiert deshalb
nicht. `verify` leer lassen, wenn das Programm keinen billigen Versionsaufruf
hat, dann entfällt der Probelauf; `every: 0` schaltet die Nachschau von selbst
ab und lässt nur die beiden Optionen übrig. Erwartet wird eine Gitea-Instanz
(getestet mit 1.27); GitHub kennt dieselbe Route unter anderen Feldnamen und
wird nicht bedient.
`daily` delivers only the text; what it looks like is for the program to decide.
Everything else in the file is called `selfUpdate…` or `update…` and therefore
does not collide. Leave `verify` empty when the program has no cheap version
call, and the trial run is skipped; `every: 0` turns off the looking by itself
and leaves just the two options. What is expected is a Gitea instance (tested
with 1.27); GitHub knows the same route under different field names and is not
served.
## Optionen
## Options
| | | |
|---|---|---|
| `-n` | `--number` | nach Dateizahl sortieren statt nach Größe |
| `-d` | `--dirs` | nur Verzeichnisse zählen, Dateien der obersten Ebene weglassen |
| `-r` | `--reverse` | größtes zuerst |
| `-t N` | `--top=N` | nur die N größten zeigen, den Rest zu einer Zeile summieren |
| `-h` | `--help` | Hilfe |
| | `--bar`, `--no-bar` | Balken und Prozent erzwingen bzw. abschalten |
| | `--color=auto\|always\|never` | Farbe erzwingen bzw. abschalten |
| | `--version` | Version |
| | `--check-update` | nachsehen, ob ein neueres Release da ist |
| | `--update` | neuestes Release holen und installieren |
| `-n` | `--number` | sort by number of files instead of by size |
| `-d` | `--dirs` | count only directories, leave out top-level files |
| `-r` | `--reverse` | largest first |
| `-t N` | `--top=N` | show only the N largest, sum the rest into one line |
| `-h` | `--help` | help |
| | `--bar`, `--no-bar` | force bar and percentage, or switch them off |
| | `--color=auto\|always\|never` | force colour, or switch it off |
| | `--version` | version |
| | `--check-update` | look whether a newer release is there |
| | `--update` | fetch and install the newest release |
Kurzoptionen lassen sich bündeln (`-rt 3`, `-dn`), der Wert von `-t` darf kleben
(`-t5`) oder folgen (`-t 5`). Ein `--` trennt Optionen von einem Pfad, der mit
`-` beginnt. Ohne Pfad gilt das aktuelle Verzeichnis.
Short options can be bundled (`-rt 3`, `-dn`), the value of `-t` may stick
(`-t5`) or follow (`-t 5`). A `--` separates options from a path beginning with
`-`. Without a path, the current directory applies.
Sortiert wird aufsteigend, das Größte steht also direkt über dem Prompt und man
muss nicht scrollen. `-r` dreht das um, für Pipes und lange Listen.
Sorting is ascending, so the largest stands right above the prompt and there is
no need to scroll. `-r` turns that around, for pipes and long lists.
Bei `-t` steht die Sammelzeile immer am kleinen Ende: oben bei normaler
Sortierung, unten bei `-r`.
With `-t`, the summary line always sits at the small end: at the top under
normal sorting, at the bottom with `-r`.
| Exit | |
|---|---|
| 0 | alles gelesen |
| 1 | Pfad existiert nicht oder ist kein Verzeichnis |
| 2 | Option falsch benutzt |
| 0 | everything read |
| 1 | path does not exist or is not a directory |
| 2 | option used wrongly |
## Farbe und Balken
## Colour and bars
Beides gilt am Terminal und entfällt in einer Pipe — `dx | sort` bekommt reinen
Text. `--color=always` schaltet beides wieder ein, für `less -R`. Wer nur eins
davon will, sagt es mit `--bar` / `--no-bar` ausdrücklich. `NO_COLOR` wird
beachtet.
Both apply at the terminal and fall away in a pipe — `dx | sort` gets plain
text. `--color=always` switches both back on, for `less -R`. Whoever wants only
one of them says so explicitly with `--bar` / `--no-bar`. `NO_COLOR` is
respected.
Die Farben folgen den `file_type`-Rollen aus `themes/eza/theme.yml`, damit
dieselbe Datei in `eza` und `dx` gleich aussieht:
The colours follow the `file_type` roles from `themes/eza/theme.yml`, so that
the same file looks the same in `eza` and in `dx`:
| | | | | |
|---|---|---|---|---|
| Verzeichnis | blue, fett | | Archive | orange |
| ausführbar | green, fett | | Bilder | violet |
| Symlink | pink | | Video | pink |
| Quelltext, Konfiguration | blue | | Audio | green |
| README, Makefile, `go.mod` | yellow | | Schlüssel, Zertifikate | red |
| Dokumente | white | | Kompilate (`.o`, `.dylib`) | grey |
| sonstige Dateien | white | | `.tmp`, `.bak`, `.DS_Store` | darker |
| directory | blue, bold | | archives | orange |
| executable | green, bold | | images | violet |
| symlink | pink | | video | pink |
| source, configuration | blue | | audio | green |
| README, Makefile, `go.mod` | yellow | | keys, certificates | red |
| documents | white | | compiled (`.o`, `.dylib`) | grey |
| other files | white | | `.tmp`, `.bak`, `.DS_Store` | darker |
Die Zahlenspalte ist grey, eine Null dark. Die Größenspalte trägt die
Größenrampe des eza-Themes — bis K green, M yellow, G orange, T red — und der
Balken nimmt die Farbe der Spalte auf, deren Anteil er zeigt: die der Größe,
mit `-n` die der Dateizahl.
The number column is grey, a zero dark. The size column carries the size ramp of
the eza theme — up to K green, M yellow, G orange, T red — and the bar takes on
the colour of the column whose share it shows: that of the size, with `-n` that
of the file count.
Der Balken ist zehn Zellen breit und rechnet in Achteln (`▏▎▍▌▋▊▉█`). Was
weniger als ein halbes Prozent ausmacht, bekommt keinen Strich und steht als
`<1%` da statt als geschöntes `0%`. Dadurch bleiben genau die Zeilen sichtbar,
auf die es ankommt.
The bar is ten cells wide and reckons in eighths (`▏▎▍▌▋▊▉█`). Whatever amounts
to less than half a percent gets no stroke and stands there as `<1%` instead of
a flattering `0%`. That way exactly those lines stay visible that matter.
## Größen
## Sizes
Die Schwellen sind die des Originals: ab 1024 der jeweiligen Einheit wird
umgeschaltet. Unterhalb von 10 kommt eine Nachkommastelle dazu, weil `2G` sonst
alles zwischen 1,5 und 2,4 Gigabyte bedeuten könnte. Ganze Bytes bleiben ganz.
The thresholds are those of the original: from 1024 of the unit in question it
switches over. Below 10, one decimal is added, because `2G` could otherwise mean
anything between 1.5 and 2.4 gigabytes. Whole bytes stay whole.
8 4.9K 316M 1.1G
## Was genau gezählt wird
## What exactly gets counted
Gezählt werden **reguläre Dateien**, wie bei `find -type f`. Verzeichnisse,
Sockets, Fifos und Geräte zählen nicht mit, auch nicht die Verzeichnisse selbst.
What gets counted are **regular files**, as with `find -type f`. Directories,
sockets, fifos and devices do not count, nor do the directories themselves.
* Ein **Symlink auf ein Verzeichnis** wird nicht verfolgt und steht deshalb mit
`0` da — pink eingefärbt, damit die Null erklärt ist.
* Ein **Symlink auf eine Datei** auf oberster Ebene zählt mit der Größe seines
Ziels, ebenfalls pink.
* **Hardlinks** zählen so oft, wie sie vorkommen. In einem Baum mit
`rsync --link-dest` oder Time Machine sind die Zahlen dadurch zu hoch; für so
etwas ist `dx` nicht gedacht.
* Gemessen wird die **Dateigröße**, nicht der belegte Platz. Sparse-Dateien und
APFS-Klone erscheinen größer, als sie auf der Platte sind.
* Was sich nicht lesen lässt — gesperrte Verzeichnisse, tote Symlinks — wird
übersprungen und am Ende auf **stderr** gemeldet, damit die Tabelle heil
bleibt.
* `-d` lässt Dateien der obersten Ebene ganz weg, auch aus der Summe.
* A **symlink to a directory** is not followed and therefore stands there with
`0` — coloured pink, so that the zero is explained.
* A **symlink to a file** at the top level counts with the size of its target,
likewise pink.
* **Hardlinks** count as often as they occur. In a tree made with
`rsync --link-dest` or Time Machine the numbers come out too high; `dx` is not
meant for that sort of thing.
* What is measured is the **file size**, not the space occupied. Sparse files
and APFS clones appear larger than they are on the disk.
* What cannot be read — locked directories, dead symlinks — is skipped and
reported at the end on **stderr**, so that the table stays intact.
* `-d` leaves out top-level files entirely, from the sum as well.
Unterverzeichnisse werden nebenläufig durchlaufen, so viele auf einmal wie
Kerne da sind. Das Original startete pro Verzeichnis einen `find`-Prozess; auf
`~/go` sind das 0,19 s statt 0,34 s.
Subdirectories are walked concurrently, as many at a time as there are cores.
The original started one `find` process per directory; on `~/go` that makes
0.19 s instead of 0.34 s.
## Unterschiede zum Perl-Original
## Differences from the Perl original
1. **Gleichstände sortieren nach Namen.** Perl ließ sie in der Reihenfolge des
Hashes, `dx -n` gab bei jedem Aufruf eine andere Reihenfolge aus.
2. **Ein ungültiger Pfad ist ein Fehler.** Perl listete stillschweigend das
aktuelle Verzeichnis — plausible, aber falsche Zahlen.
3. **Die Namensspalte ist eine Stelle breiter**, wenn der längste Eintrag ein
Verzeichnis ist. Perl zählte den angehängten `/` bei der Spaltenbreite nicht
mit, der längste Name klebte am Zahlenblock.
4. **Fehler stehen auf stderr**, statt als `find: … Permission denied` mitten in
der Ausgabe.
5. **Größen haben eine Nachkommastelle**, siehe oben. Das ist die einzige
Abweichung im alten Ausgabeformat; ohne Farbe und Balken ist der Rest
byteweise identisch.
1. **Ties sort by name.** Perl left them in the order of the hash, and `dx -n`
gave a different order on every call.
2. **An invalid path is an error.** Perl silently listed the current directory —
plausible numbers, but the wrong ones.
3. **The name column is one place wider** when the longest entry is a directory.
Perl did not count the appended `/` towards the column width, and the longest
name stuck to the block of numbers.
4. **Errors go to stderr**, instead of appearing as `find: … Permission denied`
in the middle of the output.
5. **Sizes have one decimal**, see above. That is the only deviation in the old
output format; without colour and bars the rest is identical byte for byte.
Neu sind `-r`, `-t`, die Balkenspalte, die Farben und die langen Optionen.
New are `-r`, `-t`, the bar column, the colours and the long options.
## Aufbau
## Layout
| Datei | |
| File | |
|---|---|
| `main.go` | Zählen, Sortieren, Formatieren, Optionen |
| `filetypes.go` | Endung und Dateiname → Farbe |
| `selfupdate.go` | `--update`, eigenständig und zum Kopieren gedacht |
| `build.sh` | Bau für alle Plattformen, zählt `version.txt` hoch |
| `version.txt` | die zuletzt gebaute Version |
| `dx` | das Perl-Original von 2016 |
| `main.go` | counting, sorting, formatting, options |
| `filetypes.go` | extension and file name → colour |
| `selfupdate.go` | `--update`, self-contained and meant for copying |
| `build.sh` | build for all platforms, counts `version.txt` up |
| `version.txt` | the version built last |
| `dx` | the Perl original from 2016 |
Die Palette steht in `main.go` doppelt — einmal als Zahlenwerte, einmal in den
Kommentaren als Rolle. Quelle der Wahrheit bleibt `~/src/mwxcol/mwxcol.go`;
ändert sie sich dort, müssen die elf `rgb{…}` hier nachgezogen werden.
The palette sits in `main.go` twice — once as numeric values, once in the
comments as a role. The source of truth remains `~/src/mwxcol/mwxcol.go`; if it
changes there, the eleven `rgb{…}` here have to be brought along.