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