From 2bd9a4a1693db25dfe1e3f65d5e6f80981e90788 Mon Sep 17 00:00:00 2001 From: Michael Wesemann Date: Mon, 10 Aug 2026 21:51:53 +0200 Subject: [PATCH] [mike@maginot] --- README.md | 316 +++++++++++++++++++++++++------------------------- filetypes.go | 24 ++-- main.go | 71 ++++++------ selfupdate.go | 177 ++++++++++++++-------------- 4 files changed, 292 insertions(+), 296 deletions(-) diff --git a/README.md b/README.md index 6d6f52d..8073234 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/filetypes.go b/filetypes.go index 62a49b5..1b1b4de 100644 --- a/filetypes.go +++ b/filetypes.go @@ -8,9 +8,9 @@ import ( "github.com/fatih/color" ) -// Dateityp → Farbe, nach den file_type-Rollen des eza-Themes -// (~/src/mwxcol/themes/eza/theme.yml), damit dieselbe Datei in eza und dx -// dieselbe Farbe hat. +// File type → colour, after the file_type roles of the eza theme +// (~/src/mwxcol/themes/eza/theme.yml), so that the same file has the same +// colour in eza and in dx. var byExt = map[string]*color.Color{} func register(c *color.Color, exts ...string) { @@ -20,7 +20,7 @@ func register(c *color.Color, exts ...string) { } func init() { - // source — blau + // source — blue register(cBlue, "go", "c", "h", "cc", "cpp", "cxx", "hpp", "rs", "py", "pl", "pm", "t", "sh", "bash", "zsh", "fish", "js", "mjs", "cjs", "ts", "jsx", "tsx", @@ -33,7 +33,7 @@ func init() { register(cOrange, "zip", "tar", "gz", "tgz", "bz2", "tbz", "xz", "txz", "zst", "7z", "rar", "lz", "lzma", "z", "jar", "war", "dmg", "iso", "pkg", "deb", "rpm") - // image — violett + // image — violet register(cViolet, "jpg", "jpeg", "png", "gif", "bmp", "tif", "tiff", "svg", "webp", "heic", "heif", "ico", "psd", "ai", "eps", "raw", "cr2", "nef", "dng", @@ -41,26 +41,26 @@ func init() { // video — pink register(cPink, "mp4", "mov", "avi", "mkv", "webm", "m4v", "mpg", "mpeg", "wmv", "flv", "m2ts") - // music / lossless — grün + // music / lossless — green register(cGreen, "mp3", "aac", "m4a", "ogg", "opus", "wma", "flac", "wav", "aiff", "aif", "alac") - // crypto — rot + // crypto — red register(cRed, "asc", "gpg", "pgp", "sig", "key", "pem", "crt", "cer", "p12", "kdbx") - // document — weiß + // document — white register(cWhite, "pdf", "doc", "docx", "odt", "xls", "xlsx", "ods", "ppt", "pptx", "odp", "epub", "rtf", "txt", "csv", "pages", "numbers") - // compiled — grau + // compiled — grey register(cGrey, "o", "a", "so", "dylib", "obj", "lib", "class", "pyc", "pyo", "wasm", "d") - // temp — dunkel + // temp — dark register(cDarker, "tmp", "temp", "swp", "swo", "bak", "old", "orig", "rej", "lock", "part", "crdownload", "log") } -// build — gelb (README, Makefile, …) +// build — yellow (README, Makefile, …) var byName = map[string]*color.Color{ "makefile": cYellow, "gnumakefile": cYellow, "cmakelists.txt": 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 { - 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 } low := strings.ToLower(name) diff --git a/main.go b/main.go index add301c..e8d7d59 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,8 @@ // dx — count files and directories, mwx'2016 // -// Go-Portierung des Perl-Originals (dx 1.0.2), Ausgabe eingefärbt nach der -// mwxcol-Palette (~/src/mwxcol). Ohne Terminal (Pipe, NO_COLOR, --color=never) -// bleibt die Ausgabe unverändert einfarbig. +// Go port of the Perl original (dx 1.0.2), output coloured after the mwxcol +// palette (~/src/mwxcol). Without a terminal (pipe, NO_COLOR, --color=never) +// the output stays plain, exactly as it was. package main import ( @@ -22,9 +22,9 @@ import ( "github.com/fatih/color" ) -// version ist eine var (keine const), damit build.sh die gebaute Nummer per -// -ldflags "-X main.version=…" einsetzen kann. Der Wert hier erscheint nur bei -// einem nackten `go build`; die wirklich gebaute Version steht in version.txt. +// version is a var, not a const, so build.sh can put the built number in via +// -ldflags "-X main.version=…". The value here only ever shows up in a bare +// `go build`; the version actually built is the one in version.txt. var version = "2.1.0" // ------------------------------------------------------------------ Palette @@ -48,7 +48,7 @@ var ( 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) } -// 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 ( cWhite = plain(white) cYellow = plain(yellow) @@ -61,8 +61,8 @@ var ( cGrey = plain(grey) cDark = plain(dark) cDarker = plain(darker) - cDirB = bold(blue) // Verzeichnis - cExecB = bold(green) // ausführbar + cDirB = bold(blue) // directory + cExecB = bold(green) // executable cWhiteB = bold(white) cYellowB = bold(yellow) cOrangeB = bold(orange) @@ -71,14 +71,14 @@ var ( cDarkB = bold(dark) ) -// ------------------------------------------------------------------- Zählen +// ----------------------------------------------------------------- Counting type entry struct { - name string // Anzeigename, Verzeichnisse mit "/" + name string // display name, directories with a "/" col *color.Color n 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 { @@ -103,7 +103,7 @@ func scan(dir string, onlyDirs bool) (result, error) { name := de.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) if err != nil { atomic.AddInt64(&res.unreadbl, 1) @@ -117,7 +117,7 @@ func scan(dir string, onlyDirs bool) (result, error) { sem <- struct{}{} defer func() { <-sem }() - // WalkDir folgt Symlinks nicht — wie `find -type f`. + // WalkDir does not follow symlinks — like `find -type f`. n, size := walk(path, &res.unreadbl) col := cDirB @@ -154,7 +154,7 @@ func walk(root string, unreadable *int64) (n, size int64) { atomic.AddInt64(unreadable, 1) return nil } - if !d.Type().IsRegular() { // wie find -type f + if !d.Type().IsRegular() { // like find -type f return nil } info, err := d.Info() @@ -169,10 +169,10 @@ func walk(root string, unreadable *int64) (n, size int64) { return } -// ------------------------------------------------------------------ Ausgabe +// ------------------------------------------------------------------- Output -// Schwellen wie im Perl-Original; unterhalb von 10 der jeweiligen Einheit -// jedoch mit einer Nachkommastelle — "1.9G" statt "2G". +// The thresholds of the Perl original; below 10 of the unit in question, +// though, with one decimal — "1.9G" instead of "2G". func formSize(b int64) string { const k = 1024 f := float64(b) @@ -186,7 +186,7 @@ func formSize(b int64) string { case b > k: return scaled(f/k, "K") 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) } -// 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 { switch { case b == 0: @@ -242,7 +243,7 @@ func pad(s string, w int) string { return "" } -// Anteilsbalken in Achtelschritten, feste Breite barCells. +// Share bar in eighths of a cell, barCells wide throughout. const barCells = 10 var eighths = []rune{' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉'} @@ -251,7 +252,7 @@ func bar(frac float64) string { if frac > 1 { frac = 1 } - if !(frac > 0) { // fängt auch NaN + if !(frac > 0) { // catches NaN too return strings.Repeat(" ", barCells) } 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 { 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 var rest *entry 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] } } - if rest != nil { // Restzeile bleibt am kleinen Ende + if rest != nil { // the summary line stays at the small end if o.reverse { rows = append(rows, *rest) } else { @@ -347,12 +348,12 @@ func printDir(w *bufio.Writer, dir string, res result, o opts) { if total > 0 { 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) if o.byCount { col = countColor(e.n) } - if e.agg { // Sammelzeile bleibt zurückhaltend + if e.agg { // the summary line keeps quiet col = cDark } fmt.Fprintf(w, " %s %s", @@ -368,7 +369,7 @@ type opts struct { byCount bool // -n onlyDirs bool // -d reverse bool // -r - top int // -t N, 0 = alle + top int // -t N, 0 = all bar bool } @@ -398,7 +399,7 @@ func die(format string, a ...any) { func main() { 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 when := "auto" @@ -429,7 +430,7 @@ func main() { os.Exit(1) } return - case a == updateRefreshFlag: // der Hintergrundlauf, nicht in der Hilfe + case a == updateRefreshFlag: // the background run, not in the help selfUpdate.refresh() return case a == "--number": @@ -450,7 +451,7 @@ func main() { when = strings.TrimPrefix(a, "--color=") case len(a) > 1 && a[0] == '-': 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] { case 'n': o.byCount = true @@ -461,7 +462,7 @@ func main() { case 'h': usage(os.Stdout) 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:] if val == "" { if i+1 >= len(args) { @@ -488,7 +489,7 @@ func main() { color.NoColor = false case "never": color.NoColor = true - case "auto": // fatih/color entscheidet per isatty und NO_COLOR + case "auto": // fatih/color decides, by isatty and NO_COLOR default: die("--color: expected auto|always|never, got %q", when) } @@ -513,8 +514,8 @@ func main() { if res.unreadbl > 0 { fmt.Fprintln(os.Stderr, cDark.Sprintf("dx: %d unreadable entries skipped", res.unreadbl)) } - // Kostet nichts: der Hinweis stammt aus dem Merkzettel, gefragt wird - // höchstens einmal am Tag und dann im Hintergrund. + // Costs nothing: the hint comes from the note in the cache, and the asking + // happens once a day at most, in the background. if hint := selfUpdate.daily(); hint != "" { fmt.Fprintln(os.Stderr, cDark.Sprint(hint)) } diff --git a/selfupdate.go b/selfupdate.go index 8c52856..b544044 100644 --- a/selfupdate.go +++ b/selfupdate.go @@ -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, -// den Konfigurationsblock unten anpassen, `--update` und `--check-update` in -// die Optionen hängen — fertig. Sie braucht nur die Standardbibliothek und -// bringt außer dem Block keine Namen mit, die nicht mit "selfUpdate" oder -// "update" beginnen. +// The file is meant to be copied: take it into another program, adjust the +// configuration block below, hang `--update` and `--check-update` into the +// options — done. It needs nothing but the standard library, and apart from +// that block it brings no names that do not begin with "selfUpdate" or +// "update". // -// Vorausgesetzt wird das Ablageschema von build.sh: je Version ein Release, -// dessen Tag die nackte Nummer ist (2.1.6, ein führendes "v" ist erlaubt), und -// darin je ein Asset "--" — also genau die Dateien aus -// ./bin. Gitea liefert unter /api/v1/repos///releases/latest das -// neueste Release ohne Entwurf und ohne Vorabversion; GitHub spricht dieselbe -// Route mit anderen Feldnamen und ist deshalb nicht mitgemeint. +// It assumes the layout build.sh produces: one release per version, whose tag +// is the bare number (2.1.6, a leading "v" is allowed), holding one asset +// "--" each — that is, exactly the files from ./bin. Under +// /api/v1/repos///releases/latest Gitea hands out the newest +// release that is neither a draft nor a prerelease; GitHub speaks the same +// route with different field names and is therefore not covered. package main import ( @@ -31,50 +31,50 @@ import ( "time" ) -// ------------------------------------------------------------ Konfiguration +// ------------------------------------------------------------ Configuration var selfUpdate = selfUpdater{ repo: "https://git.micw.org/mike/dx", asset: "dx", - current: version, // aus main.go, per -ldflags gesetzt + current: version, // from main.go, set by -ldflags verify: []string{"--version"}, every: 24 * time.Hour, quietEnv: "DX_NO_UPDATE_CHECK", } type selfUpdater struct { - repo string // Repo-URL wie im Browser: https://host/owner/repo - asset string // Basisname der Assets, "--" kommt dazu - current string // laufende Version - verify []string // Probelauf des Downloads; leer lässt ihn ausfallen - every time.Duration // Abstand der Nachschau von selbst; 0 schaltet sie ab - quietEnv string // diese Umgebungsvariable gesetzt: auch dann Ruhe + repo string // repo URL as in the browser: https://host/owner/repo + asset string // base name of the assets, "--" is added + current string // the running version + verify []string // trial run of the download; empty skips it + every time.Duration // how often to look on its own; 0 turns that off + quietEnv string // this environment variable set: keep quiet as well } -// updateRefreshFlag ist die Option, mit der das Programm sich selbst im -// Hintergrund aufruft. Sie steht bewusst nicht in der Hilfe. +// updateRefreshFlag is the option the program calls itself with, in the +// background. It is deliberately absent from the help. const updateRefreshFlag = "--update-refresh" -// ------------------------------------------------------- Nachschau von selbst +// ------------------------------------------------------------ Looking by itself -// daily ist der Anschluss für den gewöhnlichen Programmlauf. Sie kostet nichts: -// im Vordergrund wird nie das Netz angefasst. Zurück kommt die Zeile, die auf -// eine neue Version hinweist — oder "", wenn es nichts zu sagen gibt; wie sie -// aussieht, entscheidet der Aufrufer. Ist der Merkzettel älter als `every`, -// stößt sie nebenbei einen Hintergrundlauf an, dessen Antwort der nächste -// Aufruf vorfindet. +// daily is the hook for the ordinary run of the program. It costs nothing: in +// the foreground the network is never touched. What comes back is the line +// pointing at a new version — or "", when there is nothing to say; what it +// looks like is up to the caller. Should the note be older than `every`, daily +// starts a background run on the side, whose answer the next call will find +// waiting. func (u selfUpdater) daily() string { if u.every <= 0 || os.Getenv(u.quietEnv) != "" || !updateOnTerminal() { 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 { - // Der Zeitstempel wandert weiter, bevor gefragt wird, nicht danach: - // sonst starten zwei gleichzeitige Läufe zwei Abfragen, und ein Server, - // der gerade nicht mag, bekäme bei jedem Aufruf eine neue. Bleibt der - // Merkzettel nicht liegen, wird auch nicht gefragt — sonst hinge an - // einem unbeschreibbaren Cache-Verzeichnis ein Prozess je Aufruf. + // The timestamp moves on before the asking, not after: otherwise two + // simultaneous runs start two queries, and a server that is not in the + // mood would get a new one on every call. If the note does not stay + // put, nothing is asked either — else an unwritable cache directory + // would mean one process per call. st.Checked = time.Now() if u.saveState(st) == nil { 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) } -// refresh ist der Hintergrundlauf: fragen, notieren, still bleiben. Das -// Notieren erledigt latest, misslingt die Abfrage, bleibt der alte Stand. +// refresh is the background run: ask, write it down, stay quiet. The writing +// down is done by latest; if the query fails, the old state remains. func (u selfUpdater) refresh() { _, _ = u.latest() } -// spawnRefresh ruft dieses Programm noch einmal auf, nur zum Fragen, und wartet -// nicht. Ohne Wait wird das Kind beim Ende dieses Prozesses von init -// übernommen — es lebt also länger als der Aufruf, und dessen Ausgabe bleibt -// davon unberührt. +// spawnRefresh calls this program once more, only to ask, and does not wait. +// Without a Wait the child is adopted by init when this process ends — it thus +// outlives the call, and the call's output stays untouched by it. func (u selfUpdater) spawnRefresh() { exe, err := os.Executable() if err != nil { return } 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 { cmd.Process.Release() } } -// Der Hinweis gilt dem Menschen davor. Läuft das Programm in einer Pipe, in -// einem Skript oder unter cron, wird weder gefragt noch etwas gesagt. +// The hint is meant for the person sitting there. Running in a pipe, in a +// script or under cron, the program neither asks nor says anything. func updateOnTerminal() bool { st, err := os.Stderr.Stat() return err == nil && st.Mode()&os.ModeCharDevice != 0 } -// ------------------------------------------------------------- Merkzettel +// --------------------------------------------------------------------- Note -// updateState ist das, was zwischen zwei Aufrufen übrig bleibt: wann zuletzt -// gefragt wurde und was dabei herauskam. +// updateState is what is left between two calls: when the last question was +// asked and what came of it. type updateState struct { Checked time.Time `json:"checked"` Latest string `json:"latest"` } -// Der Merkzettel liegt im Cache-Verzeichnis, nicht in der Konfiguration: geht -// er verloren, wird eben einmal zu früh gefragt. +// The note lives in the cache directory, not in the configuration: if it gets +// lost, the only cost is asking once too early. func (u selfUpdater) statePath() (string, error) { dir, err := os.UserCacheDir() if err != nil { @@ -145,7 +144,7 @@ func (u selfUpdater) loadState() updateState { if err != nil { 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 } @@ -161,8 +160,8 @@ func (u selfUpdater) saveState(st updateState) error { if err != nil { return err } - // Über eine Nebendatei, damit ein gleichzeitiger Lauf nie ein halbes JSON - // vorfindet. + // By way of a file alongside, so that a simultaneous run never comes upon + // half a JSON. tmp := path + ".new" if err := os.WriteFile(tmp, b, 0o644); err != nil { return err @@ -174,9 +173,9 @@ func (u selfUpdater) saveState(st updateState) error { 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 { rel, err := u.latest() if err != nil { @@ -191,7 +190,7 @@ func (u selfUpdater) check(w io.Writer) error { 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 { rel, err := u.latest() if err != nil { @@ -222,8 +221,8 @@ func (u selfUpdater) install(w io.Writer) error { if err != nil { return fmt.Errorf("cannot locate the running binary: %w", err) } - // Ein installiertes dx ist oft ein Symlink nach ./bin. Ersetzt werden soll - // die Datei dahinter, nicht der Link. + // An installed dx is often a symlink into ./bin. What should be replaced is + // the file behind it, not the link. if real, err := filepath.EvalSymlinks(exe); err == nil { exe = real } @@ -237,7 +236,7 @@ func (u selfUpdater) install(w io.Writer) error { if err != nil { 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 { 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) { - // Die neue Datei entsteht neben der alten: dasselbe Dateisystem, also ist - // das Umbenennen am Ende ein atomarer Schritt und kein halber Kopiervorgang. - // Sie entsteht auch vor dem ersten Byte — ein fehlendes Schreibrecht soll - // auffallen, bevor ein paar Megabyte durch die Leitung sind. + // The new file comes into being next to the old one: same filesystem, so + // the renaming at the end is one atomic step and not half a copy. It also + // comes into being before the first byte — a missing write permission ought + // to show up before a few megabytes have gone down the wire. dir := filepath.Dir(exe) f, err := os.CreateTemp(dir, "."+filepath.Base(exe)+".new") 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) { err = pe.Err } @@ -291,9 +290,9 @@ func (u selfUpdater) download(a *updateAsset, exe string, mode os.FileMode) (str return tmp, nil } -// probe ruft den frisch geladenen Läufer einmal auf. Das fängt eine -// abgeschnittene, für die falsche Plattform gebaute oder gar nicht erst -// ausführbare Datei ab, bevor sie die laufende ersetzt. +// probe calls the freshly fetched runner once. That catches a file that is +// truncated, built for the wrong platform, or not executable in the first +// place, before it replaces the running one. func (u selfUpdater) probe(path, tag string) error { if len(u.verify) == 0 { return nil @@ -312,21 +311,22 @@ func (u selfUpdater) probe(path, tag string) error { 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 { if err := os.Rename(tmp, exe); err == nil { return nil } - // Unix überschreibt die Datei eines laufenden Programms klaglos, Windows - // nicht: dort muss die alte erst aus dem Weg. Löschen lässt sie sich - // frühestens, wenn dieser Prozess endet — das Aufräumen darf also scheitern. + // Unix overwrites the file of a running program without complaint, Windows + // does not: there the old one has to be got out of the way first. Deleting + // it becomes possible when this process ends at the earliest — so the + // tidying up is allowed to fail. old := exe + ".old" os.Remove(old) if err := os.Rename(exe, old); err != nil { return fmt.Errorf("cannot replace %s: %w", exe, err) } 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) } os.Remove(old) @@ -352,8 +352,8 @@ func (u selfUpdater) latest() (updateRelease, error) { if err != nil { return updateRelease{}, err } - // Die Frage ist klein; hängt sie, hängt sie nicht lange. Das großzügige - // Zeitlimit von updateClient gilt dem Download. + // The question is a small one; if it hangs, it does not hang for long. The + // generous time limit of updateClient is meant for the download. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -371,13 +371,13 @@ func (u selfUpdater) latest() (updateRelease, error) { return updateRelease{}, fmt.Errorf("%s has no releases", u.repo) } - // Jede geglückte Frage füllt den Merkzettel — egal ob sie von --update, - // --check-update oder dem Hintergrundlauf kam. + // Every question that succeeds fills the note — no matter whether it came + // from --update, from --check-update or from the background run. u.saveState(updateState{Checked: time.Now(), Latest: rel.TagName}) 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) { 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 } -// Ein Zeitlimit für alles zusammen: die Suche kostet ein paar hundert -// Millisekunden, der Download ein paar Megabyte — beides darf hängen bleiben, -// aber nicht ewig. +// One time limit for all of it: the look costs a few hundred milliseconds, the +// download a few megabytes — both may hang, but not forever. var updateClient = &http.Client{Timeout: 5 * time.Minute} 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 } -// ------------------------------------------------------------------ Nummern +// ------------------------------------------------------------------ Numbers -// updateCompare vergleicht zwei Versionen komponentenweise numerisch, damit -// 2.1.10 hinter 2.1.9 landet und nicht davor. Ein führendes "v" zählt nicht, -// fehlende Stellen gelten als 0 (2.1 == 2.1.0), und ein Suffix am Zahlenrest -// macht die Version älter, nicht neuer (2.1.6-rc1 < 2.1.6). Ergebnis wie bei -// strings.Compare: -1, 0, 1. +// updateCompare compares two versions component by component, numerically, so +// that 2.1.10 lands behind 2.1.9 and not in front of it. A leading "v" does not +// count, missing places count as 0 (2.1 == 2.1.0), and a suffix on the number +// makes the version older, not newer (2.1.6-rc1 < 2.1.6). The result is the one +// of strings.Compare: -1, 0, 1. func updateCompare(a, b string) int { as := strings.Split(strings.TrimPrefix(a, "v"), ".") bs := strings.Split(strings.TrimPrefix(b, "v"), ".") @@ -452,7 +451,7 @@ func updateComparePart(a, b string) int { return 1 case ra == rb: 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 case rb == "": return -1 @@ -460,7 +459,7 @@ func updateComparePart(a, b string) int { 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) { i := 0 for i < len(s) && s[i] >= '0' && s[i] <= '9' { @@ -470,8 +469,8 @@ func updateSplitNum(s string) (int, string) { return n, s[i:] } -// updateSize ist bewusst eine eigene kleine Formatierung und nicht formSize aus -// main.go — die Datei soll für sich alleine stehen. +// updateSize is deliberately a small formatting of its own and not formSize +// from main.go — the file is meant to stand on its own. func updateSize(b int64) string { const k = 1024 switch {