[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 # dx
Zählt, was in einem Verzeichnis liegt, und wie schwer es ist — eine Ebene tief, Counts what lies in a directory, and how heavy it is — one level deep, one
ein Blick, sofort fertig. glance, done at once.
``` ```
$ dx ~/src -r -t 8 $ dx ~/src -r -t 8
@@ -17,15 +17,15 @@ $ dx ~/src -r -t 8
… 16 more 1305 179M █▋ 16% … 16 more 1305 179M █▋ 16%
``` ```
Links der Name, dann die Zahl der enthaltenen Dateien, dann die Summe ihrer On the left the name, then the number of files inside, then the sum of their
Größen, dann der Anteil am Ganzen. Die Kopfzeile ist das Verzeichnis selbst. sizes, then the share of the whole. The header line is the directory itself.
Verzeichnisse werden vollständig durchlaufen, aber nicht aufgeklappt — für Directories are walked through completely, but not unfolded — for everything
alles Weitere ist `ncdu` da. 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). [mwxcol](../mwxcol/README.md).
## Bauen und installieren ## Building and installing
```sh ```sh
cd ~/src/dx cd ~/src/dx
@@ -33,83 +33,82 @@ cd ~/src/dx
cp bin/dx ~/bin/dx cp bin/dx ~/bin/dx
``` ```
`build.sh` baut nach `./bin`, je ein Binary für macOS und Linux auf arm64 und `build.sh` builds into `./bin`, one binary each for macOS and Linux on arm64
amd64, und legt `bin/dx` als Symlink auf das für diese Maschine. Für eine and amd64, and puts `bin/dx` there as a symlink to the one for this machine.
einzelne Plattform: For a single platform:
```sh ```sh
PLATFORMS="linux/amd64" ./build.sh PLATFORMS="linux/amd64" ./build.sh
``` ```
Jeder Lauf zählt die Patch-Nummer in `version.txt` um eins hoch und baut sie Every run counts the patch number in `version.txt` up by one and builds it into
per `-ldflags` in alle Binaries ein — `dx --version` sagt also, welcher Bau all the binaries via `-ldflags` — so `dx --version` tells which build is
läuft. Ein nacktes `go build` geht auch, meldet dann aber die Vorgabe aus running. A bare `go build` works too, but then reports the default from
`main.go` statt der wirklichen Nummer. `main.go` instead of the real number.
Braucht Go ≥ 1.26 und `github.com/fatih/color`. Das Perl-Original (`dx`, 1.0.2, Needs Go ≥ 1.26 and `github.com/fatih/color`. The Perl original (`dx`, 1.0.2,
2016) liegt unverändert daneben und wird vom Bau nicht angefasst; es hat keine 2016) lies next to it unchanged and is not touched by the build; it has no
Farben und keine Balken, tut aber sonst dasselbe. colours and no bars, but otherwise does the same thing.
## Selbst aktualisieren ## Updating itself
```sh ```sh
dx --check-update # nur nachsehen dx --check-update # only look
dx --update # holen und ersetzen dx --update # fetch and replace
``` ```
`dx` holt sich das neueste Release von `dx` fetches the newest release from
[git.micw.org/mike/dx](https://git.micw.org/mike/dx) — die URL steht fest im [git.micw.org/mike/dx](https://git.micw.org/mike/dx) — the URL sits fixed in the
Programm, es gibt nichts zu konfigurieren. Gebraucht wird ein Release je program, there is nothing to configure. What it needs is one release per
Version, dessen Tag die nackte Nummer ist (`2.1.6`), mit den Dateien aus `./bin` version, whose tag is the bare number (`2.1.6`), with the files from `./bin` as
als Assets; gesucht wird die eine, die zu `GOOS`/`GOARCH` dieser Maschine passt. 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 — What gets replaced is the running file itself. If the `dx` that was called is a
etwa `~/bin/dx` auf `~/src/dx/bin/dx-darwin-arm64` —, wird das Ziel dahinter symlink — say `~/bin/dx` pointing at `~/src/dx/bin/dx-darwin-arm64` — the target
erneuert, nicht der Link. Vor dem Tausch wird das frisch Geladene einmal mit behind it is renewed, not the link. Before the swap, what was freshly fetched is
`--version` aufgerufen; meldet es nicht die erwartete Nummer, bleibt alles, wie called once with `--version`; if it does not report the expected number,
es war. Der Tausch selbst ist ein `rename` im selben Verzeichnis, also atomar: everything stays as it was. The swap itself is a `rename` within the same
entweder die alte Datei oder die neue, nie eine halbe. 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 If the binary lies somewhere the user may not write to (`/usr/local/bin`),
(`/usr/local/bin`), sagt `dx --update` das und tut nichts — dann `sudo`. `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 Without being asked, `dx` looks once a day and afterwards says so at the end of
stderr Bescheid: the output, on stderr:
``` ```
dx 2.1.9 is available, run 'dx --update' dx 2.1.9 is available, run 'dx --update'
``` ```
Der Lauf im Vordergrund fasst dafür nie das Netz an. Er liest nur einen The run in the foreground never touches the network for this. It only reads a
Merkzettel — `~/Library/Caches/dx/update.json`, unter Linux note — `~/Library/Caches/dx/update.json`, on Linux `~/.cache/dx/update.json` —
`~/.cache/dx/update.json` — und wenn der älter als einen Tag ist, startet er and when that one is older than a day, it starts `dx --update-refresh` on the
nebenbei `dx --update-refresh`: denselben Läufer noch einmal, abgekoppelt, ohne side: the same runner once more, detached, without output, only to ask. Nobody
Ausgabe, nur zum Fragen. Auf dessen Antwort wartet niemand, sie steht beim waits for its answer; it will be in the note at the next call. `dx` thereby
nächsten Aufruf im Merkzettel. `dx` bleibt damit genau so schnell wie vorher, stays exactly as fast as before, even when the server happens to be silent.
auch wenn der Server gerade schweigt.
Der Zeitstempel wandert weiter, *bevor* gefragt wird. Zwei gleichzeitige Läufe The timestamp moves on *before* the asking. Two simultaneous runs therefore
starten deshalb nur eine Abfrage, und ein Server, der nicht antwortet, wird start one query, not two, and a server that does not answer is asked again
morgen wieder gefragt und nicht bei jedem Aufruf. Lässt sich der Merkzettel tomorrow rather than on every call. If the note cannot be written, the question
nicht schreiben, unterbleibt die Frage ganz — sonst hinge an einem is dropped entirely — otherwise a write-protected cache directory would mean one
schreibgeschützten Cache-Verzeichnis ein Prozess je Aufruf. process per call.
Gefragt und gesagt wird nur, wenn stderr am Terminal hängt. In einer Pipe, in Asking and speaking happen only when stderr hangs on a terminal. In a pipe, in a
einem Skript und unter cron ist Ruhe, und `DX_NO_UPDATE_CHECK=1` schaltet es script and under cron there is quiet, and `DX_NO_UPDATE_CHECK=1` turns it off
ganz ab. altogether.
Das kostet Platz: `net/http` samt TLS macht aus einem 1,8-MB-Binary eins von This costs space: `net/http` together with TLS turns a 1.8 MB binary into one of
6,2 MB. Für ein Werkzeug, das ohnehin auf der Platte liegt, ist das der Preis 6.2 MB. For a tool that sits on the disk anyway, that is the price of being able
dafür, sich selbst erneuern zu können — wer ihn nicht zahlen will, lässt to renew itself — whoever will not pay it leaves `selfupdate.go` out.
`selfupdate.go` weg.
## Wiederverwenden ## Reusing it
`selfupdate.go` hängt an nichts im Rest des Programms und kommt mit der `selfupdate.go` hangs on nothing in the rest of the program and gets by with the
Standardbibliothek aus. Für ein anderes Programm die Datei kopieren, den Block standard library. For another program, copy the file, adjust the block at the
oben anpassen — top —
```go ```go
var selfUpdate = selfUpdater{ var selfUpdate = selfUpdater{
@@ -122,8 +121,8 @@ var selfUpdate = selfUpdater{
} }
``` ```
Dazu drei Zeilen im Optionenteil — `--update`, `--check-update` und das — plus three lines in the options part: `--update`, `--check-update` and the
versteckte `updateRefreshFlag` — und eine am Ende des Programms: hidden `updateRefreshFlag`, and one at the end of the program:
```go ```go
if hint := selfUpdate.daily(); hint != "" { 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 `daily` delivers only the text; what it looks like is for the program to decide.
Weitere in der Datei heißt `selfUpdate…` oder `update…` und kollidiert deshalb Everything else in the file is called `selfUpdate…` or `update…` and therefore
nicht. `verify` leer lassen, wenn das Programm keinen billigen Versionsaufruf does not collide. Leave `verify` empty when the program has no cheap version
hat, dann entfällt der Probelauf; `every: 0` schaltet die Nachschau von selbst call, and the trial run is skipped; `every: 0` turns off the looking by itself
ab und lässt nur die beiden Optionen übrig. Erwartet wird eine Gitea-Instanz and leaves just the two options. What is expected is a Gitea instance (tested
(getestet mit 1.27); GitHub kennt dieselbe Route unter anderen Feldnamen und with 1.27); GitHub knows the same route under different field names and is not
wird nicht bedient. served.
## Optionen ## Options
| | | | | | | |
|---|---|---| |---|---|---|
| `-n` | `--number` | nach Dateizahl sortieren statt nach Größe | | `-n` | `--number` | sort by number of files instead of by size |
| `-d` | `--dirs` | nur Verzeichnisse zählen, Dateien der obersten Ebene weglassen | | `-d` | `--dirs` | count only directories, leave out top-level files |
| `-r` | `--reverse` | größtes zuerst | | `-r` | `--reverse` | largest first |
| `-t N` | `--top=N` | nur die N größten zeigen, den Rest zu einer Zeile summieren | | `-t N` | `--top=N` | show only the N largest, sum the rest into one line |
| `-h` | `--help` | Hilfe | | `-h` | `--help` | help |
| | `--bar`, `--no-bar` | Balken und Prozent erzwingen bzw. abschalten | | | `--bar`, `--no-bar` | force bar and percentage, or switch them off |
| | `--color=auto\|always\|never` | Farbe erzwingen bzw. abschalten | | | `--color=auto\|always\|never` | force colour, or switch it off |
| | `--version` | Version | | | `--version` | version |
| | `--check-update` | nachsehen, ob ein neueres Release da ist | | | `--check-update` | look whether a newer release is there |
| | `--update` | neuestes Release holen und installieren | | | `--update` | fetch and install the newest release |
Kurzoptionen lassen sich bündeln (`-rt 3`, `-dn`), der Wert von `-t` darf kleben Short options can be bundled (`-rt 3`, `-dn`), the value of `-t` may stick
(`-t5`) oder folgen (`-t 5`). Ein `--` trennt Optionen von einem Pfad, der mit (`-t5`) or follow (`-t 5`). A `--` separates options from a path beginning with
`-` beginnt. Ohne Pfad gilt das aktuelle Verzeichnis. `-`. Without a path, the current directory applies.
Sortiert wird aufsteigend, das Größte steht also direkt über dem Prompt und man Sorting is ascending, so the largest stands right above the prompt and there is
muss nicht scrollen. `-r` dreht das um, für Pipes und lange Listen. 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 With `-t`, the summary line always sits at the small end: at the top under
Sortierung, unten bei `-r`. normal sorting, at the bottom with `-r`.
| Exit | | | Exit | |
|---|---| |---|---|
| 0 | alles gelesen | | 0 | everything read |
| 1 | Pfad existiert nicht oder ist kein Verzeichnis | | 1 | path does not exist or is not a directory |
| 2 | Option falsch benutzt | | 2 | option used wrongly |
## Farbe und Balken ## Colour and bars
Beides gilt am Terminal und entfällt in einer Pipe — `dx | sort` bekommt reinen Both apply at the terminal and fall away in a pipe — `dx | sort` gets plain
Text. `--color=always` schaltet beides wieder ein, für `less -R`. Wer nur eins text. `--color=always` switches both back on, for `less -R`. Whoever wants only
davon will, sagt es mit `--bar` / `--no-bar` ausdrücklich. `NO_COLOR` wird one of them says so explicitly with `--bar` / `--no-bar`. `NO_COLOR` is
beachtet. respected.
Die Farben folgen den `file_type`-Rollen aus `themes/eza/theme.yml`, damit The colours follow the `file_type` roles from `themes/eza/theme.yml`, so that
dieselbe Datei in `eza` und `dx` gleich aussieht: the same file looks the same in `eza` and in `dx`:
| | | | | | | | | | | |
|---|---|---|---|---| |---|---|---|---|---|
| Verzeichnis | blue, fett | | Archive | orange | | directory | blue, bold | | archives | orange |
| ausführbar | green, fett | | Bilder | violet | | executable | green, bold | | images | violet |
| Symlink | pink | | Video | pink | | symlink | pink | | video | pink |
| Quelltext, Konfiguration | blue | | Audio | green | | source, configuration | blue | | audio | green |
| README, Makefile, `go.mod` | yellow | | Schlüssel, Zertifikate | red | | README, Makefile, `go.mod` | yellow | | keys, certificates | red |
| Dokumente | white | | Kompilate (`.o`, `.dylib`) | grey | | documents | white | | compiled (`.o`, `.dylib`) | grey |
| sonstige Dateien | white | | `.tmp`, `.bak`, `.DS_Store` | darker | | other files | white | | `.tmp`, `.bak`, `.DS_Store` | darker |
Die Zahlenspalte ist grey, eine Null dark. Die Größenspalte trägt die The number column is grey, a zero dark. The size column carries the size ramp of
Größenrampe des eza-Themes — bis K green, M yellow, G orange, T red — und der the eza theme — up to K green, M yellow, G orange, T red — and the bar takes on
Balken nimmt die Farbe der Spalte auf, deren Anteil er zeigt: die der Größe, the colour of the column whose share it shows: that of the size, with `-n` that
mit `-n` die der Dateizahl. of the file count.
Der Balken ist zehn Zellen breit und rechnet in Achteln (`▏▎▍▌▋▊▉█`). Was The bar is ten cells wide and reckons in eighths (`▏▎▍▌▋▊▉█`). Whatever amounts
weniger als ein halbes Prozent ausmacht, bekommt keinen Strich und steht als to less than half a percent gets no stroke and stands there as `<1%` instead of
`<1%` da statt als geschöntes `0%`. Dadurch bleiben genau die Zeilen sichtbar, a flattering `0%`. That way exactly those lines stay visible that matter.
auf die es ankommt.
## Größen ## Sizes
Die Schwellen sind die des Originals: ab 1024 der jeweiligen Einheit wird The thresholds are those of the original: from 1024 of the unit in question it
umgeschaltet. Unterhalb von 10 kommt eine Nachkommastelle dazu, weil `2G` sonst switches over. Below 10, one decimal is added, because `2G` could otherwise mean
alles zwischen 1,5 und 2,4 Gigabyte bedeuten könnte. Ganze Bytes bleiben ganz. anything between 1.5 and 2.4 gigabytes. Whole bytes stay whole.
8 4.9K 316M 1.1G 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, What gets counted are **regular files**, as with `find -type f`. Directories,
Sockets, Fifos und Geräte zählen nicht mit, auch nicht die Verzeichnisse selbst. sockets, fifos and devices do not count, nor do the directories themselves.
* Ein **Symlink auf ein Verzeichnis** wird nicht verfolgt und steht deshalb mit * A **symlink to a directory** is not followed and therefore stands there with
`0` da — pink eingefärbt, damit die Null erklärt ist. `0` — coloured pink, so that the zero is explained.
* Ein **Symlink auf eine Datei** auf oberster Ebene zählt mit der Größe seines * A **symlink to a file** at the top level counts with the size of its target,
Ziels, ebenfalls pink. likewise pink.
* **Hardlinks** zählen so oft, wie sie vorkommen. In einem Baum mit * **Hardlinks** count as often as they occur. In a tree made with
`rsync --link-dest` oder Time Machine sind die Zahlen dadurch zu hoch; für so `rsync --link-dest` or Time Machine the numbers come out too high; `dx` is not
etwas ist `dx` nicht gedacht. meant for that sort of thing.
* Gemessen wird die **Dateigröße**, nicht der belegte Platz. Sparse-Dateien und * What is measured is the **file size**, not the space occupied. Sparse files
APFS-Klone erscheinen größer, als sie auf der Platte sind. and APFS clones appear larger than they are on the disk.
* Was sich nicht lesen lässt — gesperrte Verzeichnisse, tote Symlinks — wird * What cannot be read — locked directories, dead symlinks — is skipped and
übersprungen und am Ende auf **stderr** gemeldet, damit die Tabelle heil reported at the end on **stderr**, so that the table stays intact.
bleibt. * `-d` leaves out top-level files entirely, from the sum as well.
* `-d` lässt Dateien der obersten Ebene ganz weg, auch aus der Summe.
Unterverzeichnisse werden nebenläufig durchlaufen, so viele auf einmal wie Subdirectories are walked concurrently, as many at a time as there are cores.
Kerne da sind. Das Original startete pro Verzeichnis einen `find`-Prozess; auf The original started one `find` process per directory; on `~/go` that makes
`~/go` sind das 0,19 s statt 0,34 s. 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 1. **Ties sort by name.** Perl left them in the order of the hash, and `dx -n`
Hashes, `dx -n` gab bei jedem Aufruf eine andere Reihenfolge aus. gave a different order on every call.
2. **Ein ungültiger Pfad ist ein Fehler.** Perl listete stillschweigend das 2. **An invalid path is an error.** Perl silently listed the current directory —
aktuelle Verzeichnis — plausible, aber falsche Zahlen. plausible numbers, but the wrong ones.
3. **Die Namensspalte ist eine Stelle breiter**, wenn der längste Eintrag ein 3. **The name column is one place wider** when the longest entry is a directory.
Verzeichnis ist. Perl zählte den angehängten `/` bei der Spaltenbreite nicht Perl did not count the appended `/` towards the column width, and the longest
mit, der längste Name klebte am Zahlenblock. name stuck to the block of numbers.
4. **Fehler stehen auf stderr**, statt als `find: … Permission denied` mitten in 4. **Errors go to stderr**, instead of appearing as `find: … Permission denied`
der Ausgabe. in the middle of the output.
5. **Größen haben eine Nachkommastelle**, siehe oben. Das ist die einzige 5. **Sizes have one decimal**, see above. That is the only deviation in the old
Abweichung im alten Ausgabeformat; ohne Farbe und Balken ist der Rest output format; without colour and bars the rest is identical byte for byte.
byteweise identisch.
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 | | `main.go` | counting, sorting, formatting, options |
| `filetypes.go` | Endung und Dateiname → Farbe | | `filetypes.go` | extension and file name → colour |
| `selfupdate.go` | `--update`, eigenständig und zum Kopieren gedacht | | `selfupdate.go` | `--update`, self-contained and meant for copying |
| `build.sh` | Bau für alle Plattformen, zählt `version.txt` hoch | | `build.sh` | build for all platforms, counts `version.txt` up |
| `version.txt` | die zuletzt gebaute Version | | `version.txt` | the version built last |
| `dx` | das Perl-Original von 2016 | | `dx` | the Perl original from 2016 |
Die Palette steht in `main.go` doppelt — einmal als Zahlenwerte, einmal in den The palette sits in `main.go` twice — once as numeric values, once in the
Kommentaren als Rolle. Quelle der Wahrheit bleibt `~/src/mwxcol/mwxcol.go`; comments as a role. The source of truth remains `~/src/mwxcol/mwxcol.go`; if it
ändert sie sich dort, müssen die elf `rgb{…}` hier nachgezogen werden. changes there, the eleven `rgb{…}` here have to be brought along.
+12 -12
View File
@@ -8,9 +8,9 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
) )
// Dateityp → Farbe, nach den file_type-Rollen des eza-Themes // File type → colour, after the file_type roles of the eza theme
// (~/src/mwxcol/themes/eza/theme.yml), damit dieselbe Datei in eza und dx // (~/src/mwxcol/themes/eza/theme.yml), so that the same file has the same
// dieselbe Farbe hat. // colour in eza and in dx.
var byExt = map[string]*color.Color{} var byExt = map[string]*color.Color{}
func register(c *color.Color, exts ...string) { func register(c *color.Color, exts ...string) {
@@ -20,7 +20,7 @@ func register(c *color.Color, exts ...string) {
} }
func init() { func init() {
// source — blau // source — blue
register(cBlue, register(cBlue,
"go", "c", "h", "cc", "cpp", "cxx", "hpp", "rs", "py", "pl", "pm", "t", "go", "c", "h", "cc", "cpp", "cxx", "hpp", "rs", "py", "pl", "pm", "t",
"sh", "bash", "zsh", "fish", "js", "mjs", "cjs", "ts", "jsx", "tsx", "sh", "bash", "zsh", "fish", "js", "mjs", "cjs", "ts", "jsx", "tsx",
@@ -33,7 +33,7 @@ func init() {
register(cOrange, register(cOrange,
"zip", "tar", "gz", "tgz", "bz2", "tbz", "xz", "txz", "zst", "7z", "zip", "tar", "gz", "tgz", "bz2", "tbz", "xz", "txz", "zst", "7z",
"rar", "lz", "lzma", "z", "jar", "war", "dmg", "iso", "pkg", "deb", "rpm") "rar", "lz", "lzma", "z", "jar", "war", "dmg", "iso", "pkg", "deb", "rpm")
// image — violett // image — violet
register(cViolet, register(cViolet,
"jpg", "jpeg", "png", "gif", "bmp", "tif", "tiff", "svg", "webp", "jpg", "jpeg", "png", "gif", "bmp", "tif", "tiff", "svg", "webp",
"heic", "heif", "ico", "psd", "ai", "eps", "raw", "cr2", "nef", "dng", "heic", "heif", "ico", "psd", "ai", "eps", "raw", "cr2", "nef", "dng",
@@ -41,26 +41,26 @@ func init() {
// video — pink // video — pink
register(cPink, register(cPink,
"mp4", "mov", "avi", "mkv", "webm", "m4v", "mpg", "mpeg", "wmv", "flv", "m2ts") "mp4", "mov", "avi", "mkv", "webm", "m4v", "mpg", "mpeg", "wmv", "flv", "m2ts")
// music / lossless — grün // music / lossless — green
register(cGreen, register(cGreen,
"mp3", "aac", "m4a", "ogg", "opus", "wma", "flac", "wav", "aiff", "aif", "alac") "mp3", "aac", "m4a", "ogg", "opus", "wma", "flac", "wav", "aiff", "aif", "alac")
// crypto — rot // crypto — red
register(cRed, register(cRed,
"asc", "gpg", "pgp", "sig", "key", "pem", "crt", "cer", "p12", "kdbx") "asc", "gpg", "pgp", "sig", "key", "pem", "crt", "cer", "p12", "kdbx")
// document — weiß // document — white
register(cWhite, register(cWhite,
"pdf", "doc", "docx", "odt", "xls", "xlsx", "ods", "ppt", "pptx", "odp", "pdf", "doc", "docx", "odt", "xls", "xlsx", "ods", "ppt", "pptx", "odp",
"epub", "rtf", "txt", "csv", "pages", "numbers") "epub", "rtf", "txt", "csv", "pages", "numbers")
// compiled — grau // compiled — grey
register(cGrey, register(cGrey,
"o", "a", "so", "dylib", "obj", "lib", "class", "pyc", "pyo", "wasm", "d") "o", "a", "so", "dylib", "obj", "lib", "class", "pyc", "pyo", "wasm", "d")
// temp — dunkel // temp — dark
register(cDarker, register(cDarker,
"tmp", "temp", "swp", "swo", "bak", "old", "orig", "rej", "lock", "tmp", "temp", "swp", "swo", "bak", "old", "orig", "rej", "lock",
"part", "crdownload", "log") "part", "crdownload", "log")
} }
// build — gelb (README, Makefile, …) // build — yellow (README, Makefile, …)
var byName = map[string]*color.Color{ var byName = map[string]*color.Color{
"makefile": cYellow, "gnumakefile": cYellow, "cmakelists.txt": cYellow, "makefile": cYellow, "gnumakefile": cYellow, "cmakelists.txt": cYellow,
"dockerfile": cYellow, "justfile": cYellow, "rakefile": cYellow, "dockerfile": cYellow, "justfile": cYellow, "rakefile": cYellow,
@@ -71,7 +71,7 @@ var byName = map[string]*color.Color{
} }
func fileColor(name string, mode os.FileMode) *color.Color { func fileColor(name string, mode os.FileMode) *color.Color {
if mode&0o111 != 0 { // ausführbar schlägt alles andere, wie in eza if mode&0o111 != 0 { // executable beats everything else, as in eza
return cExecB return cExecB
} }
low := strings.ToLower(name) low := strings.ToLower(name)
+36 -35
View File
@@ -1,8 +1,8 @@
// dx — count files and directories, mwx'2016 // dx — count files and directories, mwx'2016
// //
// Go-Portierung des Perl-Originals (dx 1.0.2), Ausgabe eingefärbt nach der // Go port of the Perl original (dx 1.0.2), output coloured after the mwxcol
// mwxcol-Palette (~/src/mwxcol). Ohne Terminal (Pipe, NO_COLOR, --color=never) // palette (~/src/mwxcol). Without a terminal (pipe, NO_COLOR, --color=never)
// bleibt die Ausgabe unverändert einfarbig. // the output stays plain, exactly as it was.
package main package main
import ( import (
@@ -22,9 +22,9 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
) )
// version ist eine var (keine const), damit build.sh die gebaute Nummer per // version is a var, not a const, so build.sh can put the built number in via
// -ldflags "-X main.version=…" einsetzen kann. Der Wert hier erscheint nur bei // -ldflags "-X main.version=…". The value here only ever shows up in a bare
// einem nackten `go build`; die wirklich gebaute Version steht in version.txt. // `go build`; the version actually built is the one in version.txt.
var version = "2.1.0" var version = "2.1.0"
// ------------------------------------------------------------------ Palette // ------------------------------------------------------------------ Palette
@@ -48,7 +48,7 @@ var (
func plain(c rgb) *color.Color { return color.RGB(c.r, c.g, c.b) } func plain(c rgb) *color.Color { return color.RGB(c.r, c.g, c.b) }
func bold(c rgb) *color.Color { return color.New(color.Bold).AddRGB(c.r, c.g, c.b) } func bold(c rgb) *color.Color { return color.New(color.Bold).AddRGB(c.r, c.g, c.b) }
// Rollen wie im eza-Theme, damit dx und eza dieselbe Datei gleich einfärben. // The roles of the eza theme, so that dx and eza colour the same file alike.
var ( var (
cWhite = plain(white) cWhite = plain(white)
cYellow = plain(yellow) cYellow = plain(yellow)
@@ -61,8 +61,8 @@ var (
cGrey = plain(grey) cGrey = plain(grey)
cDark = plain(dark) cDark = plain(dark)
cDarker = plain(darker) cDarker = plain(darker)
cDirB = bold(blue) // Verzeichnis cDirB = bold(blue) // directory
cExecB = bold(green) // ausführbar cExecB = bold(green) // executable
cWhiteB = bold(white) cWhiteB = bold(white)
cYellowB = bold(yellow) cYellowB = bold(yellow)
cOrangeB = bold(orange) cOrangeB = bold(orange)
@@ -71,14 +71,14 @@ var (
cDarkB = bold(dark) cDarkB = bold(dark)
) )
// ------------------------------------------------------------------- Zählen // ----------------------------------------------------------------- Counting
type entry struct { type entry struct {
name string // Anzeigename, Verzeichnisse mit "/" name string // display name, directories with a "/"
col *color.Color col *color.Color
n int64 n int64
size int64 size int64
agg bool // Sammelzeile aus -t, kein echter Eintrag agg bool // the summary line from -t, not a real entry
} }
type result struct { type result struct {
@@ -103,7 +103,7 @@ func scan(dir string, onlyDirs bool) (result, error) {
name := de.Name() name := de.Name()
path := filepath.Join(dir, name) path := filepath.Join(dir, name)
// Perls -d/-f folgen Symlinks; ein toter Link fällt durch beide Tests. // Perl's -d/-f follow symlinks; a dead link fails both tests.
st, err := os.Stat(path) st, err := os.Stat(path)
if err != nil { if err != nil {
atomic.AddInt64(&res.unreadbl, 1) atomic.AddInt64(&res.unreadbl, 1)
@@ -117,7 +117,7 @@ func scan(dir string, onlyDirs bool) (result, error) {
sem <- struct{}{} sem <- struct{}{}
defer func() { <-sem }() defer func() { <-sem }()
// WalkDir folgt Symlinks nicht — wie `find <pfad> -type f`. // WalkDir does not follow symlinks — like `find <path> -type f`.
n, size := walk(path, &res.unreadbl) n, size := walk(path, &res.unreadbl)
col := cDirB col := cDirB
@@ -154,7 +154,7 @@ func walk(root string, unreadable *int64) (n, size int64) {
atomic.AddInt64(unreadable, 1) atomic.AddInt64(unreadable, 1)
return nil return nil
} }
if !d.Type().IsRegular() { // wie find -type f if !d.Type().IsRegular() { // like find -type f
return nil return nil
} }
info, err := d.Info() info, err := d.Info()
@@ -169,10 +169,10 @@ func walk(root string, unreadable *int64) (n, size int64) {
return return
} }
// ------------------------------------------------------------------ Ausgabe // ------------------------------------------------------------------- Output
// Schwellen wie im Perl-Original; unterhalb von 10 der jeweiligen Einheit // The thresholds of the Perl original; below 10 of the unit in question,
// jedoch mit einer Nachkommastelle — "1.9G" statt "2G". // though, with one decimal — "1.9G" instead of "2G".
func formSize(b int64) string { func formSize(b int64) string {
const k = 1024 const k = 1024
f := float64(b) f := float64(b)
@@ -186,7 +186,7 @@ func formSize(b int64) string {
case b > k: case b > k:
return scaled(f/k, "K") return scaled(f/k, "K")
default: default:
return fmt.Sprintf("%.0f", f) // ganze Bytes, nie gebrochen return fmt.Sprintf("%.0f", f) // whole bytes, never fractions
} }
} }
@@ -197,7 +197,8 @@ func scaled(v float64, unit string) string {
return fmt.Sprintf("%.0f%s", v, unit) return fmt.Sprintf("%.0f%s", v, unit)
} }
// Größenrampe wie im eza-Theme: byte/kilo grün, mega gelb, giga orange, huge rot. // The size ramp of the eza theme: byte/kilo green, mega yellow, giga orange,
// huge red.
func sizeColor(b int64, strong bool) *color.Color { func sizeColor(b int64, strong bool) *color.Color {
switch { switch {
case b == 0: case b == 0:
@@ -242,7 +243,7 @@ func pad(s string, w int) string {
return "" return ""
} }
// Anteilsbalken in Achtelschritten, feste Breite barCells. // Share bar in eighths of a cell, barCells wide throughout.
const barCells = 10 const barCells = 10
var eighths = []rune{' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉'} var eighths = []rune{' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉'}
@@ -251,7 +252,7 @@ func bar(frac float64) string {
if frac > 1 { if frac > 1 {
frac = 1 frac = 1
} }
if !(frac > 0) { // fängt auch NaN if !(frac > 0) { // catches NaN too
return strings.Repeat(" ", barCells) return strings.Repeat(" ", barCells)
} }
units := int(frac*barCells*8 + 0.5) units := int(frac*barCells*8 + 0.5)
@@ -288,10 +289,10 @@ func printDir(w *bufio.Writer, dir string, res result, o opts) {
if ka, kb := metric(a), metric(b); ka != kb { if ka, kb := metric(a), metric(b); ka != kb {
return ka < kb return ka < kb
} }
return a.name < b.name // Perl ließ Gleichstände der Hash-Ordnung return a.name < b.name // Perl left ties to the order of the hash
}) })
// -t N: nur die N größten zeigen, der Rest wird zu einer Zeile. // -t N: show only the N largest, the rest becomes a single line.
rows := res.entries rows := res.entries
var rest *entry var rest *entry
if o.top > 0 && len(rows) > o.top { if o.top > 0 && len(rows) > o.top {
@@ -310,7 +311,7 @@ func printDir(w *bufio.Writer, dir string, res result, o opts) {
rows[i], rows[j] = rows[j], rows[i] rows[i], rows[j] = rows[j], rows[i]
} }
} }
if rest != nil { // Restzeile bleibt am kleinen Ende if rest != nil { // the summary line stays at the small end
if o.reverse { if o.reverse {
rows = append(rows, *rest) rows = append(rows, *rest)
} else { } else {
@@ -347,12 +348,12 @@ func printDir(w *bufio.Writer, dir string, res result, o opts) {
if total > 0 { if total > 0 {
frac = float64(metric(e)) / float64(total) frac = float64(metric(e)) / float64(total)
} }
// Balken trägt die Farbe der Spalte, deren Anteil er zeigt. // The bar takes the colour of the column whose share it shows.
col := sizeColor(e.size, false) col := sizeColor(e.size, false)
if o.byCount { if o.byCount {
col = countColor(e.n) col = countColor(e.n)
} }
if e.agg { // Sammelzeile bleibt zurückhaltend if e.agg { // the summary line keeps quiet
col = cDark col = cDark
} }
fmt.Fprintf(w, " %s %s", fmt.Fprintf(w, " %s %s",
@@ -368,7 +369,7 @@ type opts struct {
byCount bool // -n byCount bool // -n
onlyDirs bool // -d onlyDirs bool // -d
reverse bool // -r reverse bool // -r
top int // -t N, 0 = alle top int // -t N, 0 = all
bar bool bar bool
} }
@@ -398,7 +399,7 @@ func die(format string, a ...any) {
func main() { func main() {
var o opts var o opts
var barSet bool // sonst folgt der Balken der Farbentscheidung var barSet bool // otherwise the bar follows the colour decision
var dir string var dir string
when := "auto" when := "auto"
@@ -429,7 +430,7 @@ func main() {
os.Exit(1) os.Exit(1)
} }
return return
case a == updateRefreshFlag: // der Hintergrundlauf, nicht in der Hilfe case a == updateRefreshFlag: // the background run, not in the help
selfUpdate.refresh() selfUpdate.refresh()
return return
case a == "--number": case a == "--number":
@@ -450,7 +451,7 @@ func main() {
when = strings.TrimPrefix(a, "--color=") when = strings.TrimPrefix(a, "--color=")
case len(a) > 1 && a[0] == '-': case len(a) > 1 && a[0] == '-':
flags := a[1:] flags := a[1:]
for j := 0; j < len(flags); j++ { // Bündelung wie Getopt::Std: -dn for j := 0; j < len(flags); j++ { // bundling as in Getopt::Std: -dn
switch flags[j] { switch flags[j] {
case 'n': case 'n':
o.byCount = true o.byCount = true
@@ -461,7 +462,7 @@ func main() {
case 'h': case 'h':
usage(os.Stdout) usage(os.Stdout)
return return
case 't': // Wert klebt am Flag (-t5) oder folgt (-t 5) case 't': // the value sticks to the flag (-t5) or follows (-t 5)
val := flags[j+1:] val := flags[j+1:]
if val == "" { if val == "" {
if i+1 >= len(args) { if i+1 >= len(args) {
@@ -488,7 +489,7 @@ func main() {
color.NoColor = false color.NoColor = false
case "never": case "never":
color.NoColor = true color.NoColor = true
case "auto": // fatih/color entscheidet per isatty und NO_COLOR case "auto": // fatih/color decides, by isatty and NO_COLOR
default: default:
die("--color: expected auto|always|never, got %q", when) die("--color: expected auto|always|never, got %q", when)
} }
@@ -513,8 +514,8 @@ func main() {
if res.unreadbl > 0 { if res.unreadbl > 0 {
fmt.Fprintln(os.Stderr, cDark.Sprintf("dx: %d unreadable entries skipped", res.unreadbl)) fmt.Fprintln(os.Stderr, cDark.Sprintf("dx: %d unreadable entries skipped", res.unreadbl))
} }
// Kostet nichts: der Hinweis stammt aus dem Merkzettel, gefragt wird // Costs nothing: the hint comes from the note in the cache, and the asking
// höchstens einmal am Tag und dann im Hintergrund. // happens once a day at most, in the background.
if hint := selfUpdate.daily(); hint != "" { if hint := selfUpdate.daily(); hint != "" {
fmt.Fprintln(os.Stderr, cDark.Sprint(hint)) fmt.Fprintln(os.Stderr, cDark.Sprint(hint))
} }
+88 -89
View File
@@ -1,17 +1,17 @@
// selfupdate.go — Selbstaktualisierung aus den Releases einer Gitea-Instanz. // selfupdate.go — updating oneself from the releases of a Gitea instance.
// //
// Die Datei ist als Kopiervorlage gedacht: in ein anderes Programm übernehmen, // The file is meant to be copied: take it into another program, adjust the
// den Konfigurationsblock unten anpassen, `--update` und `--check-update` in // configuration block below, hang `--update` and `--check-update` into the
// die Optionen hängen — fertig. Sie braucht nur die Standardbibliothek und // options — done. It needs nothing but the standard library, and apart from
// bringt außer dem Block keine Namen mit, die nicht mit "selfUpdate" oder // that block it brings no names that do not begin with "selfUpdate" or
// "update" beginnen. // "update".
// //
// Vorausgesetzt wird das Ablageschema von build.sh: je Version ein Release, // It assumes the layout build.sh produces: one release per version, whose tag
// dessen Tag die nackte Nummer ist (2.1.6, ein führendes "v" ist erlaubt), und // is the bare number (2.1.6, a leading "v" is allowed), holding one asset
// darin je ein Asset "<name>-<goos>-<goarch>" — also genau die Dateien aus // "<name>-<goos>-<goarch>" each — that is, exactly the files from ./bin. Under
// ./bin. Gitea liefert unter /api/v1/repos/<owner>/<repo>/releases/latest das // /api/v1/repos/<owner>/<repo>/releases/latest Gitea hands out the newest
// neueste Release ohne Entwurf und ohne Vorabversion; GitHub spricht dieselbe // release that is neither a draft nor a prerelease; GitHub speaks the same
// Route mit anderen Feldnamen und ist deshalb nicht mitgemeint. // route with different field names and is therefore not covered.
package main package main
import ( import (
@@ -31,50 +31,50 @@ import (
"time" "time"
) )
// ------------------------------------------------------------ Konfiguration // ------------------------------------------------------------ Configuration
var selfUpdate = selfUpdater{ var selfUpdate = selfUpdater{
repo: "https://git.micw.org/mike/dx", repo: "https://git.micw.org/mike/dx",
asset: "dx", asset: "dx",
current: version, // aus main.go, per -ldflags gesetzt current: version, // from main.go, set by -ldflags
verify: []string{"--version"}, verify: []string{"--version"},
every: 24 * time.Hour, every: 24 * time.Hour,
quietEnv: "DX_NO_UPDATE_CHECK", quietEnv: "DX_NO_UPDATE_CHECK",
} }
type selfUpdater struct { type selfUpdater struct {
repo string // Repo-URL wie im Browser: https://host/owner/repo repo string // repo URL as in the browser: https://host/owner/repo
asset string // Basisname der Assets, "-<goos>-<goarch>" kommt dazu asset string // base name of the assets, "-<goos>-<goarch>" is added
current string // laufende Version current string // the running version
verify []string // Probelauf des Downloads; leer lässt ihn ausfallen verify []string // trial run of the download; empty skips it
every time.Duration // Abstand der Nachschau von selbst; 0 schaltet sie ab every time.Duration // how often to look on its own; 0 turns that off
quietEnv string // diese Umgebungsvariable gesetzt: auch dann Ruhe quietEnv string // this environment variable set: keep quiet as well
} }
// updateRefreshFlag ist die Option, mit der das Programm sich selbst im // updateRefreshFlag is the option the program calls itself with, in the
// Hintergrund aufruft. Sie steht bewusst nicht in der Hilfe. // background. It is deliberately absent from the help.
const updateRefreshFlag = "--update-refresh" const updateRefreshFlag = "--update-refresh"
// ------------------------------------------------------- Nachschau von selbst // ------------------------------------------------------------ Looking by itself
// daily ist der Anschluss für den gewöhnlichen Programmlauf. Sie kostet nichts: // daily is the hook for the ordinary run of the program. It costs nothing: in
// im Vordergrund wird nie das Netz angefasst. Zurück kommt die Zeile, die auf // the foreground the network is never touched. What comes back is the line
// eine neue Version hinweist — oder "", wenn es nichts zu sagen gibt; wie sie // pointing at a new version — or "", when there is nothing to say; what it
// aussieht, entscheidet der Aufrufer. Ist der Merkzettel älter als `every`, // looks like is up to the caller. Should the note be older than `every`, daily
// stößt sie nebenbei einen Hintergrundlauf an, dessen Antwort der nächste // starts a background run on the side, whose answer the next call will find
// Aufruf vorfindet. // waiting.
func (u selfUpdater) daily() string { func (u selfUpdater) daily() string {
if u.every <= 0 || os.Getenv(u.quietEnv) != "" || !updateOnTerminal() { if u.every <= 0 || os.Getenv(u.quietEnv) != "" || !updateOnTerminal() {
return "" return ""
} }
st := u.loadState() // keine Datei: Nullwert, also sofort fällig st := u.loadState() // no file: the zero value, hence due at once
if time.Since(st.Checked) >= u.every { if time.Since(st.Checked) >= u.every {
// Der Zeitstempel wandert weiter, bevor gefragt wird, nicht danach: // The timestamp moves on before the asking, not after: otherwise two
// sonst starten zwei gleichzeitige Läufe zwei Abfragen, und ein Server, // simultaneous runs start two queries, and a server that is not in the
// der gerade nicht mag, bekäme bei jedem Aufruf eine neue. Bleibt der // mood would get a new one on every call. If the note does not stay
// Merkzettel nicht liegen, wird auch nicht gefragt — sonst hinge an // put, nothing is asked either — else an unwritable cache directory
// einem unbeschreibbaren Cache-Verzeichnis ein Prozess je Aufruf. // would mean one process per call.
st.Checked = time.Now() st.Checked = time.Now()
if u.saveState(st) == nil { if u.saveState(st) == nil {
u.spawnRefresh() u.spawnRefresh()
@@ -87,46 +87,45 @@ func (u selfUpdater) daily() string {
return fmt.Sprintf("%s %s is available, run '%s --update'", u.asset, st.Latest, u.asset) return fmt.Sprintf("%s %s is available, run '%s --update'", u.asset, st.Latest, u.asset)
} }
// refresh ist der Hintergrundlauf: fragen, notieren, still bleiben. Das // refresh is the background run: ask, write it down, stay quiet. The writing
// Notieren erledigt latest, misslingt die Abfrage, bleibt der alte Stand. // down is done by latest; if the query fails, the old state remains.
func (u selfUpdater) refresh() { func (u selfUpdater) refresh() {
_, _ = u.latest() _, _ = u.latest()
} }
// spawnRefresh ruft dieses Programm noch einmal auf, nur zum Fragen, und wartet // spawnRefresh calls this program once more, only to ask, and does not wait.
// nicht. Ohne Wait wird das Kind beim Ende dieses Prozesses von init // Without a Wait the child is adopted by init when this process ends — it thus
// übernommen — es lebt also länger als der Aufruf, und dessen Ausgabe bleibt // outlives the call, and the call's output stays untouched by it.
// davon unberührt.
func (u selfUpdater) spawnRefresh() { func (u selfUpdater) spawnRefresh() {
exe, err := os.Executable() exe, err := os.Executable()
if err != nil { if err != nil {
return return
} }
cmd := exec.Command(exe, updateRefreshFlag) cmd := exec.Command(exe, updateRefreshFlag)
cmd.Stdin, cmd.Stdout, cmd.Stderr = nil, nil, nil // alles nach /dev/null cmd.Stdin, cmd.Stdout, cmd.Stderr = nil, nil, nil // everything to /dev/null
if cmd.Start() == nil { if cmd.Start() == nil {
cmd.Process.Release() cmd.Process.Release()
} }
} }
// Der Hinweis gilt dem Menschen davor. Läuft das Programm in einer Pipe, in // The hint is meant for the person sitting there. Running in a pipe, in a
// einem Skript oder unter cron, wird weder gefragt noch etwas gesagt. // script or under cron, the program neither asks nor says anything.
func updateOnTerminal() bool { func updateOnTerminal() bool {
st, err := os.Stderr.Stat() st, err := os.Stderr.Stat()
return err == nil && st.Mode()&os.ModeCharDevice != 0 return err == nil && st.Mode()&os.ModeCharDevice != 0
} }
// ------------------------------------------------------------- Merkzettel // --------------------------------------------------------------------- Note
// updateState ist das, was zwischen zwei Aufrufen übrig bleibt: wann zuletzt // updateState is what is left between two calls: when the last question was
// gefragt wurde und was dabei herauskam. // asked and what came of it.
type updateState struct { type updateState struct {
Checked time.Time `json:"checked"` Checked time.Time `json:"checked"`
Latest string `json:"latest"` Latest string `json:"latest"`
} }
// Der Merkzettel liegt im Cache-Verzeichnis, nicht in der Konfiguration: geht // The note lives in the cache directory, not in the configuration: if it gets
// er verloren, wird eben einmal zu früh gefragt. // lost, the only cost is asking once too early.
func (u selfUpdater) statePath() (string, error) { func (u selfUpdater) statePath() (string, error) {
dir, err := os.UserCacheDir() dir, err := os.UserCacheDir()
if err != nil { if err != nil {
@@ -145,7 +144,7 @@ func (u selfUpdater) loadState() updateState {
if err != nil { if err != nil {
return st return st
} }
json.Unmarshal(b, &st) // eine kaputte Datei zählt wie keine json.Unmarshal(b, &st) // a broken file counts as none
return st return st
} }
@@ -161,8 +160,8 @@ func (u selfUpdater) saveState(st updateState) error {
if err != nil { if err != nil {
return err return err
} }
// Über eine Nebendatei, damit ein gleichzeitiger Lauf nie ein halbes JSON // By way of a file alongside, so that a simultaneous run never comes upon
// vorfindet. // half a JSON.
tmp := path + ".new" tmp := path + ".new"
if err := os.WriteFile(tmp, b, 0o644); err != nil { if err := os.WriteFile(tmp, b, 0o644); err != nil {
return err return err
@@ -174,9 +173,9 @@ func (u selfUpdater) saveState(st updateState) error {
return nil return nil
} }
// ------------------------------------------------------------------ Ablauf // ------------------------------------------------------------------ The work
// check sieht nur nach und fasst nichts an. // check only looks and touches nothing.
func (u selfUpdater) check(w io.Writer) error { func (u selfUpdater) check(w io.Writer) error {
rel, err := u.latest() rel, err := u.latest()
if err != nil { if err != nil {
@@ -191,7 +190,7 @@ func (u selfUpdater) check(w io.Writer) error {
return nil return nil
} }
// install holt das neueste Release und ersetzt die laufende Datei damit. // install fetches the newest release and replaces the running file with it.
func (u selfUpdater) install(w io.Writer) error { func (u selfUpdater) install(w io.Writer) error {
rel, err := u.latest() rel, err := u.latest()
if err != nil { if err != nil {
@@ -222,8 +221,8 @@ func (u selfUpdater) install(w io.Writer) error {
if err != nil { if err != nil {
return fmt.Errorf("cannot locate the running binary: %w", err) return fmt.Errorf("cannot locate the running binary: %w", err)
} }
// Ein installiertes dx ist oft ein Symlink nach ./bin. Ersetzt werden soll // An installed dx is often a symlink into ./bin. What should be replaced is
// die Datei dahinter, nicht der Link. // the file behind it, not the link.
if real, err := filepath.EvalSymlinks(exe); err == nil { if real, err := filepath.EvalSymlinks(exe); err == nil {
exe = real exe = real
} }
@@ -237,7 +236,7 @@ func (u selfUpdater) install(w io.Writer) error {
if err != nil { if err != nil {
return err return err
} }
defer os.Remove(tmp) // greift nur, wenn das Umbenennen unten ausfällt defer os.Remove(tmp) // only bites when the renaming below falls through
if err := u.probe(tmp, rel.TagName); err != nil { if err := u.probe(tmp, rel.TagName); err != nil {
return err return err
@@ -251,14 +250,14 @@ func (u selfUpdater) install(w io.Writer) error {
} }
func (u selfUpdater) download(a *updateAsset, exe string, mode os.FileMode) (string, error) { func (u selfUpdater) download(a *updateAsset, exe string, mode os.FileMode) (string, error) {
// Die neue Datei entsteht neben der alten: dasselbe Dateisystem, also ist // The new file comes into being next to the old one: same filesystem, so
// das Umbenennen am Ende ein atomarer Schritt und kein halber Kopiervorgang. // the renaming at the end is one atomic step and not half a copy. It also
// Sie entsteht auch vor dem ersten Byte — ein fehlendes Schreibrecht soll // comes into being before the first byte — a missing write permission ought
// auffallen, bevor ein paar Megabyte durch die Leitung sind. // to show up before a few megabytes have gone down the wire.
dir := filepath.Dir(exe) dir := filepath.Dir(exe)
f, err := os.CreateTemp(dir, "."+filepath.Base(exe)+".new") f, err := os.CreateTemp(dir, "."+filepath.Base(exe)+".new")
if err != nil { if err != nil {
var pe *os.PathError // der Pfad steht schon in der Meldung var pe *os.PathError // the path is in the message already
if errors.As(err, &pe) { if errors.As(err, &pe) {
err = pe.Err err = pe.Err
} }
@@ -291,9 +290,9 @@ func (u selfUpdater) download(a *updateAsset, exe string, mode os.FileMode) (str
return tmp, nil return tmp, nil
} }
// probe ruft den frisch geladenen Läufer einmal auf. Das fängt eine // probe calls the freshly fetched runner once. That catches a file that is
// abgeschnittene, für die falsche Plattform gebaute oder gar nicht erst // truncated, built for the wrong platform, or not executable in the first
// ausführbare Datei ab, bevor sie die laufende ersetzt. // place, before it replaces the running one.
func (u selfUpdater) probe(path, tag string) error { func (u selfUpdater) probe(path, tag string) error {
if len(u.verify) == 0 { if len(u.verify) == 0 {
return nil return nil
@@ -312,21 +311,22 @@ func (u selfUpdater) probe(path, tag string) error {
return nil return nil
} }
// updateReplace tauscht die laufende Datei gegen die neue. // updateReplace swaps the running file for the new one.
func updateReplace(tmp, exe string) error { func updateReplace(tmp, exe string) error {
if err := os.Rename(tmp, exe); err == nil { if err := os.Rename(tmp, exe); err == nil {
return nil return nil
} }
// Unix überschreibt die Datei eines laufenden Programms klaglos, Windows // Unix overwrites the file of a running program without complaint, Windows
// nicht: dort muss die alte erst aus dem Weg. Löschen lässt sie sich // does not: there the old one has to be got out of the way first. Deleting
// frühestens, wenn dieser Prozess endet — das Aufräumen darf also scheitern. // it becomes possible when this process ends at the earliest — so the
// tidying up is allowed to fail.
old := exe + ".old" old := exe + ".old"
os.Remove(old) os.Remove(old)
if err := os.Rename(exe, old); err != nil { if err := os.Rename(exe, old); err != nil {
return fmt.Errorf("cannot replace %s: %w", exe, err) return fmt.Errorf("cannot replace %s: %w", exe, err)
} }
if err := os.Rename(tmp, exe); err != nil { if err := os.Rename(tmp, exe); err != nil {
os.Rename(old, exe) // zurück auf den alten Stand os.Rename(old, exe) // back to how it was
return fmt.Errorf("cannot replace %s: %w", exe, err) return fmt.Errorf("cannot replace %s: %w", exe, err)
} }
os.Remove(old) os.Remove(old)
@@ -352,8 +352,8 @@ func (u selfUpdater) latest() (updateRelease, error) {
if err != nil { if err != nil {
return updateRelease{}, err return updateRelease{}, err
} }
// Die Frage ist klein; hängt sie, hängt sie nicht lange. Das großzügige // The question is a small one; if it hangs, it does not hang for long. The
// Zeitlimit von updateClient gilt dem Download. // generous time limit of updateClient is meant for the download.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
@@ -371,13 +371,13 @@ func (u selfUpdater) latest() (updateRelease, error) {
return updateRelease{}, fmt.Errorf("%s has no releases", u.repo) return updateRelease{}, fmt.Errorf("%s has no releases", u.repo)
} }
// Jede geglückte Frage füllt den Merkzettel — egal ob sie von --update, // Every question that succeeds fills the note — no matter whether it came
// --check-update oder dem Hintergrundlauf kam. // from --update, from --check-update or from the background run.
u.saveState(updateState{Checked: time.Now(), Latest: rel.TagName}) u.saveState(updateState{Checked: time.Now(), Latest: rel.TagName})
return rel, nil return rel, nil
} }
// apiBase macht aus https://host/owner/repo die API-Wurzel des Repos. // apiBase turns https://host/owner/repo into the API root of the repo.
func (u selfUpdater) apiBase() (string, error) { func (u selfUpdater) apiBase() (string, error) {
bad := fmt.Errorf("repo %q: expected https://host/owner/repo", u.repo) bad := fmt.Errorf("repo %q: expected https://host/owner/repo", u.repo)
@@ -392,9 +392,8 @@ func (u selfUpdater) apiBase() (string, error) {
return fmt.Sprintf("%s://%s/api/v1/repos/%s/%s", ref.Scheme, ref.Host, parts[0], parts[1]), nil return fmt.Sprintf("%s://%s/api/v1/repos/%s/%s", ref.Scheme, ref.Host, parts[0], parts[1]), nil
} }
// Ein Zeitlimit für alles zusammen: die Suche kostet ein paar hundert // One time limit for all of it: the look costs a few hundred milliseconds, the
// Millisekunden, der Download ein paar Megabyte — beides darf hängen bleiben, // download a few megabytes — both may hang, but not forever.
// aber nicht ewig.
var updateClient = &http.Client{Timeout: 5 * time.Minute} var updateClient = &http.Client{Timeout: 5 * time.Minute}
func updateGet(ctx context.Context, target string) (*http.Response, error) { func updateGet(ctx context.Context, target string) (*http.Response, error) {
@@ -415,13 +414,13 @@ func updateGet(ctx context.Context, target string) (*http.Response, error) {
return resp, nil return resp, nil
} }
// ------------------------------------------------------------------ Nummern // ------------------------------------------------------------------ Numbers
// updateCompare vergleicht zwei Versionen komponentenweise numerisch, damit // updateCompare compares two versions component by component, numerically, so
// 2.1.10 hinter 2.1.9 landet und nicht davor. Ein führendes "v" zählt nicht, // that 2.1.10 lands behind 2.1.9 and not in front of it. A leading "v" does not
// fehlende Stellen gelten als 0 (2.1 == 2.1.0), und ein Suffix am Zahlenrest // count, missing places count as 0 (2.1 == 2.1.0), and a suffix on the number
// macht die Version älter, nicht neuer (2.1.6-rc1 < 2.1.6). Ergebnis wie bei // makes the version older, not newer (2.1.6-rc1 < 2.1.6). The result is the one
// strings.Compare: -1, 0, 1. // of strings.Compare: -1, 0, 1.
func updateCompare(a, b string) int { func updateCompare(a, b string) int {
as := strings.Split(strings.TrimPrefix(a, "v"), ".") as := strings.Split(strings.TrimPrefix(a, "v"), ".")
bs := strings.Split(strings.TrimPrefix(b, "v"), ".") bs := strings.Split(strings.TrimPrefix(b, "v"), ".")
@@ -452,7 +451,7 @@ func updateComparePart(a, b string) int {
return 1 return 1
case ra == rb: case ra == rb:
return 0 return 0
case ra == "": // 2.1.6 ist fertig, 2.1.6-rc1 noch nicht case ra == "": // 2.1.6 is finished, 2.1.6-rc1 is not yet
return 1 return 1
case rb == "": case rb == "":
return -1 return -1
@@ -460,7 +459,7 @@ func updateComparePart(a, b string) int {
return strings.Compare(ra, rb) return strings.Compare(ra, rb)
} }
// updateSplitNum trennt "10-rc1" in 10 und "-rc1". // updateSplitNum separates "10-rc1" into 10 and "-rc1".
func updateSplitNum(s string) (int, string) { func updateSplitNum(s string) (int, string) {
i := 0 i := 0
for i < len(s) && s[i] >= '0' && s[i] <= '9' { for i < len(s) && s[i] >= '0' && s[i] <= '9' {
@@ -470,8 +469,8 @@ func updateSplitNum(s string) (int, string) {
return n, s[i:] return n, s[i:]
} }
// updateSize ist bewusst eine eigene kleine Formatierung und nicht formSize aus // updateSize is deliberately a small formatting of its own and not formSize
// main.go — die Datei soll für sich alleine stehen. // from main.go — the file is meant to stand on its own.
func updateSize(b int64) string { func updateSize(b int64) string {
const k = 1024 const k = 1024
switch { switch {