initial commit [141.14.140.180,mike]
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
|||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
._*
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
bin/
|
||||||
|
dx
|
||||||
|
.mgshrc
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# dx
|
||||||
|
|
||||||
|
Zählt, was in einem Verzeichnis liegt, und wie schwer es ist — eine Ebene tief,
|
||||||
|
ein Blick, sofort fertig.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ dx ~/src -r -t 8
|
||||||
|
/Users/mike/src 2489 1.1G
|
||||||
|
Wetter/ 297 316M ██▉ 29%
|
||||||
|
gblog/ 99 135M █▎ 12%
|
||||||
|
gbld/ 82 131M █▏ 12%
|
||||||
|
goca/ 80 108M █ 10%
|
||||||
|
futterturm/ 62 68M ▋ 6%
|
||||||
|
BugSim/ 323 68M ▋ 6%
|
||||||
|
gvm/ 37 57M ▌ 5%
|
||||||
|
mJournal/ 204 42M ▍ 4%
|
||||||
|
… 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.
|
||||||
|
|
||||||
|
Im Terminal ist die Ausgabe farbig, nach der Palette aus
|
||||||
|
[mwxcol](../mwxcol/README.md).
|
||||||
|
|
||||||
|
## Bauen und installieren
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd ~/src/dx
|
||||||
|
./build.sh
|
||||||
|
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:
|
||||||
|
|
||||||
|
```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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Optionen
|
||||||
|
|
||||||
|
| | | |
|
||||||
|
|---|---|---|
|
||||||
|
| `-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 |
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Bei `-t` steht die Sammelzeile immer am kleinen Ende: oben bei normaler
|
||||||
|
Sortierung, unten bei `-r`.
|
||||||
|
|
||||||
|
| Exit | |
|
||||||
|
|---|---|
|
||||||
|
| 0 | alles gelesen |
|
||||||
|
| 1 | Pfad existiert nicht oder ist kein Verzeichnis |
|
||||||
|
| 2 | Option falsch benutzt |
|
||||||
|
|
||||||
|
## Farbe und Balken
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Die Farben folgen den `file_type`-Rollen aus `themes/eza/theme.yml`, damit
|
||||||
|
dieselbe Datei in `eza` und `dx` gleich aussieht:
|
||||||
|
|
||||||
|
| | | | | |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Größen
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
8 4.9K 316M 1.1G
|
||||||
|
|
||||||
|
## Was genau gezählt wird
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Unterschiede zum 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.
|
||||||
|
|
||||||
|
Neu sind `-r`, `-t`, die Balkenspalte, die Farben und die langen Optionen.
|
||||||
|
|
||||||
|
## Aufbau
|
||||||
|
|
||||||
|
| Datei | |
|
||||||
|
|---|---|
|
||||||
|
| `main.go` | Zählen, Sortieren, Formatieren, Optionen |
|
||||||
|
| `filetypes.go` | Endung und Dateiname → Farbe |
|
||||||
|
| `build.sh` | Bau für alle Plattformen, zählt `version.txt` hoch |
|
||||||
|
| `version.txt` | die zuletzt gebaute Version |
|
||||||
|
| `dx` | das Perl-Original von 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.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Build dx for the usual platforms into ./bin, auto-incrementing the patch
|
||||||
|
# version by 0.0.1 on every build.
|
||||||
|
#
|
||||||
|
# version.txt holds the currently built version. Each run increments the patch
|
||||||
|
# component, then builds every platform with that one version injected via
|
||||||
|
# -ldflags, and writes it back. So version.txt always reflects the version of
|
||||||
|
# the binaries just built, and all of them carry the same one.
|
||||||
|
#
|
||||||
|
# Override the platform list to build just one:
|
||||||
|
# PLATFORMS="linux/amd64" ./build.sh
|
||||||
|
#
|
||||||
|
# Windows is not in the list: dx compiles there, but the output leans on a
|
||||||
|
# terminal that speaks 24-bit colour and draws U+2588 block characters, so
|
||||||
|
# shipping it would promise more than has been tested. PLATFORMS can add it.
|
||||||
|
#
|
||||||
|
# -s -w drops the symbol table and DWARF info, -trimpath keeps build paths out
|
||||||
|
# of the binary; together they roughly halve it. Neither affects a panic trace.
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
PLATFORMS=${PLATFORMS:-"darwin/arm64 darwin/amd64 linux/amd64 linux/arm64"}
|
||||||
|
|
||||||
|
V=$(cat version.txt 2>/dev/null || echo 2.1.0)
|
||||||
|
|
||||||
|
# split MAJOR.MINOR.PATCH and increment PATCH (no carry: 2.1.9 -> 2.1.10)
|
||||||
|
MAJOR=${V%%.*}
|
||||||
|
REST=${V#*.}
|
||||||
|
MINOR=${REST%%.*}
|
||||||
|
PATCH=${REST#*.}
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
NV="$MAJOR.$MINOR.$PATCH"
|
||||||
|
|
||||||
|
# NOTE: ./dx in the repo root is the Perl original from 2016, not a build
|
||||||
|
# artefact. Nothing here writes to or removes it — the build only touches ./bin.
|
||||||
|
|
||||||
|
mkdir -p bin
|
||||||
|
HOST="$(go env GOOS)/$(go env GOARCH)"
|
||||||
|
|
||||||
|
for p in $PLATFORMS; do
|
||||||
|
os=${p%/*}
|
||||||
|
arch=${p#*/}
|
||||||
|
out="bin/dx-$os-$arch"
|
||||||
|
|
||||||
|
# CGO_ENABLED=0 throughout: it makes the cross builds work without a
|
||||||
|
# toolchain per target and the binaries static. dx only needs the
|
||||||
|
# filesystem, so nothing is lost by dropping cgo.
|
||||||
|
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" \
|
||||||
|
go build -trimpath -ldflags "-s -w -X main.version=$NV" -o "$out" .
|
||||||
|
|
||||||
|
if [ "$p" = "$HOST" ]; then
|
||||||
|
ln -sf "dx-$os-$arch" bin/dx # the one for this machine
|
||||||
|
echo " $out -> bin/dx"
|
||||||
|
else
|
||||||
|
echo " $out"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "$NV" > version.txt
|
||||||
|
echo "built dx v$NV"
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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.
|
||||||
|
var byExt = map[string]*color.Color{}
|
||||||
|
|
||||||
|
func register(c *color.Color, exts ...string) {
|
||||||
|
for _, e := range exts {
|
||||||
|
byExt[e] = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// source — blau
|
||||||
|
register(cBlue,
|
||||||
|
"go", "c", "h", "cc", "cpp", "cxx", "hpp", "rs", "py", "pl", "pm", "t",
|
||||||
|
"sh", "bash", "zsh", "fish", "js", "mjs", "cjs", "ts", "jsx", "tsx",
|
||||||
|
"rb", "java", "kt", "swift", "m", "mm", "php", "lua", "sql", "r", "jl",
|
||||||
|
"hs", "el", "vim", "scala", "cs", "dart", "zig", "asm", "s", "f90",
|
||||||
|
"html", "htm", "css", "scss", "sass", "less", "vue", "svelte",
|
||||||
|
"json", "yml", "yaml", "toml", "ini", "conf", "cfg", "xml", "plist",
|
||||||
|
"md", "markdown", "tex", "awk", "sed")
|
||||||
|
// compressed — orange
|
||||||
|
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
|
||||||
|
register(cViolet,
|
||||||
|
"jpg", "jpeg", "png", "gif", "bmp", "tif", "tiff", "svg", "webp",
|
||||||
|
"heic", "heif", "ico", "psd", "ai", "eps", "raw", "cr2", "nef", "dng",
|
||||||
|
"stl", "obj3d")
|
||||||
|
// video — pink
|
||||||
|
register(cPink,
|
||||||
|
"mp4", "mov", "avi", "mkv", "webm", "m4v", "mpg", "mpeg", "wmv", "flv", "m2ts")
|
||||||
|
// music / lossless — grün
|
||||||
|
register(cGreen,
|
||||||
|
"mp3", "aac", "m4a", "ogg", "opus", "wma", "flac", "wav", "aiff", "aif", "alac")
|
||||||
|
// crypto — rot
|
||||||
|
register(cRed,
|
||||||
|
"asc", "gpg", "pgp", "sig", "key", "pem", "crt", "cer", "p12", "kdbx")
|
||||||
|
// document — weiß
|
||||||
|
register(cWhite,
|
||||||
|
"pdf", "doc", "docx", "odt", "xls", "xlsx", "ods", "ppt", "pptx", "odp",
|
||||||
|
"epub", "rtf", "txt", "csv", "pages", "numbers")
|
||||||
|
// compiled — grau
|
||||||
|
register(cGrey,
|
||||||
|
"o", "a", "so", "dylib", "obj", "lib", "class", "pyc", "pyo", "wasm", "d")
|
||||||
|
// temp — dunkel
|
||||||
|
register(cDarker,
|
||||||
|
"tmp", "temp", "swp", "swo", "bak", "old", "orig", "rej", "lock",
|
||||||
|
"part", "crdownload", "log")
|
||||||
|
}
|
||||||
|
|
||||||
|
// build — gelb (README, Makefile, …)
|
||||||
|
var byName = map[string]*color.Color{
|
||||||
|
"makefile": cYellow, "gnumakefile": cYellow, "cmakelists.txt": cYellow,
|
||||||
|
"dockerfile": cYellow, "justfile": cYellow, "rakefile": cYellow,
|
||||||
|
"go.mod": cYellow, "go.sum": cYellow, "package.json": cYellow,
|
||||||
|
"package-lock.json": cYellow, "cargo.toml": cYellow, "cargo.lock": cYellow,
|
||||||
|
"license": cYellow, "copying": cYellow,
|
||||||
|
".ds_store": cDarker, ".gitignore": cDark, ".gitattributes": cDark,
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileColor(name string, mode os.FileMode) *color.Color {
|
||||||
|
if mode&0o111 != 0 { // ausführbar schlägt alles andere, wie in eza
|
||||||
|
return cExecB
|
||||||
|
}
|
||||||
|
low := strings.ToLower(name)
|
||||||
|
if c, ok := byName[low]; ok {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(low, "readme") || strings.HasPrefix(low, "makefile") {
|
||||||
|
return cYellow
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(name, "~") {
|
||||||
|
return cDarker
|
||||||
|
}
|
||||||
|
if ext := strings.TrimPrefix(filepath.Ext(low), "."); ext != "" {
|
||||||
|
if c, ok := byExt[ext]; ok {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cWhite
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
module dx
|
||||||
|
|
||||||
|
go 1.26.1
|
||||||
|
|
||||||
|
require github.com/fatih/color v1.19.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||||
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
@@ -0,0 +1,504 @@
|
|||||||
|
// 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.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"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.
|
||||||
|
var version = "2.1.0"
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ Palette
|
||||||
|
|
||||||
|
type rgb struct{ r, g, b int }
|
||||||
|
|
||||||
|
var (
|
||||||
|
white = rgb{0xf0, 0xf0, 0xea}
|
||||||
|
yellow = rgb{0xf9, 0xe2, 0xaf}
|
||||||
|
orange = rgb{0xfa, 0xb3, 0x87}
|
||||||
|
red = rgb{0xf3, 0x8b, 0xa8}
|
||||||
|
green = rgb{0x94, 0xe2, 0xd5}
|
||||||
|
blue = rgb{0xb4, 0xbe, 0xfe}
|
||||||
|
pink = rgb{0xf5, 0xb2, 0xf7}
|
||||||
|
violet = rgb{0xcb, 0xa6, 0xf7}
|
||||||
|
grey = rgb{0xaa, 0xaa, 0xbb}
|
||||||
|
dark = rgb{0x77, 0x77, 0x88}
|
||||||
|
darker = rgb{0x44, 0x44, 0x55}
|
||||||
|
)
|
||||||
|
|
||||||
|
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.
|
||||||
|
var (
|
||||||
|
cWhite = plain(white)
|
||||||
|
cYellow = plain(yellow)
|
||||||
|
cOrange = plain(orange)
|
||||||
|
cRed = plain(red)
|
||||||
|
cGreen = plain(green)
|
||||||
|
cBlue = plain(blue)
|
||||||
|
cPink = plain(pink)
|
||||||
|
cViolet = plain(violet)
|
||||||
|
cGrey = plain(grey)
|
||||||
|
cDark = plain(dark)
|
||||||
|
cDarker = plain(darker)
|
||||||
|
cDirB = bold(blue) // Verzeichnis
|
||||||
|
cExecB = bold(green) // ausführbar
|
||||||
|
cWhiteB = bold(white)
|
||||||
|
cYellowB = bold(yellow)
|
||||||
|
cOrangeB = bold(orange)
|
||||||
|
cRedB = bold(red)
|
||||||
|
cGreenB = bold(green)
|
||||||
|
cDarkB = bold(dark)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------- Zählen
|
||||||
|
|
||||||
|
type entry struct {
|
||||||
|
name string // Anzeigename, Verzeichnisse mit "/"
|
||||||
|
col *color.Color
|
||||||
|
n int64
|
||||||
|
size int64
|
||||||
|
agg bool // Sammelzeile aus -t, kein echter Eintrag
|
||||||
|
}
|
||||||
|
|
||||||
|
type result struct {
|
||||||
|
entries []entry
|
||||||
|
totalN int64
|
||||||
|
totalS int64
|
||||||
|
unreadbl int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func scan(dir string, onlyDirs bool) (result, error) {
|
||||||
|
des, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res result
|
||||||
|
var mu sync.Mutex
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
sem := make(chan struct{}, runtime.NumCPU())
|
||||||
|
|
||||||
|
for _, de := range des {
|
||||||
|
name := de.Name()
|
||||||
|
path := filepath.Join(dir, name)
|
||||||
|
|
||||||
|
// Perls -d/-f folgen Symlinks; ein toter Link fällt durch beide Tests.
|
||||||
|
st, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
atomic.AddInt64(&res.unreadbl, 1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
isLink := de.Type()&os.ModeSymlink != 0
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case st.IsDir():
|
||||||
|
wg.Go(func() {
|
||||||
|
sem <- struct{}{}
|
||||||
|
defer func() { <-sem }()
|
||||||
|
|
||||||
|
// WalkDir folgt Symlinks nicht — wie `find <pfad> -type f`.
|
||||||
|
n, size := walk(path, &res.unreadbl)
|
||||||
|
|
||||||
|
col := cDirB
|
||||||
|
if isLink {
|
||||||
|
col = cPink
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
res.entries = append(res.entries, entry{name: name + "/", col: col, n: n, size: size})
|
||||||
|
res.totalN += n
|
||||||
|
res.totalS += size
|
||||||
|
mu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
case st.Mode().IsRegular() && !onlyDirs:
|
||||||
|
col := fileColor(name, st.Mode())
|
||||||
|
if isLink {
|
||||||
|
col = cPink
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
res.entries = append(res.entries, entry{name: name, col: col, n: 1, size: st.Size()})
|
||||||
|
res.totalN++
|
||||||
|
res.totalS += st.Size()
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func walk(root string, unreadable *int64) (n, size int64) {
|
||||||
|
filepath.WalkDir(root, func(_ string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
atomic.AddInt64(unreadable, 1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !d.Type().IsRegular() { // wie find -type f
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
info, err := d.Info()
|
||||||
|
if err != nil {
|
||||||
|
atomic.AddInt64(unreadable, 1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
size += info.Size()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ Ausgabe
|
||||||
|
|
||||||
|
// Schwellen wie im Perl-Original; unterhalb von 10 der jeweiligen Einheit
|
||||||
|
// jedoch mit einer Nachkommastelle — "1.9G" statt "2G".
|
||||||
|
func formSize(b int64) string {
|
||||||
|
const k = 1024
|
||||||
|
f := float64(b)
|
||||||
|
switch {
|
||||||
|
case b > k*k*k*k:
|
||||||
|
return scaled(f/k/k/k/k, "T")
|
||||||
|
case b > k*k*k:
|
||||||
|
return scaled(f/k/k/k, "G")
|
||||||
|
case b > k*k:
|
||||||
|
return scaled(f/k/k, "M")
|
||||||
|
case b > k:
|
||||||
|
return scaled(f/k, "K")
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%.0f", f) // ganze Bytes, nie gebrochen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scaled(v float64, unit string) string {
|
||||||
|
if v < 9.95 {
|
||||||
|
return fmt.Sprintf("%.1f%s", v, unit)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.0f%s", v, unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Größenrampe wie im eza-Theme: byte/kilo grün, mega gelb, giga orange, huge rot.
|
||||||
|
func sizeColor(b int64, strong bool) *color.Color {
|
||||||
|
switch {
|
||||||
|
case b == 0:
|
||||||
|
if strong {
|
||||||
|
return cDarkB
|
||||||
|
}
|
||||||
|
return cDark
|
||||||
|
case b > 1024*1024*1024*1024:
|
||||||
|
if strong {
|
||||||
|
return cRedB
|
||||||
|
}
|
||||||
|
return cRed
|
||||||
|
case b > 1024*1024*1024:
|
||||||
|
if strong {
|
||||||
|
return cOrangeB
|
||||||
|
}
|
||||||
|
return cOrange
|
||||||
|
case b > 1024*1024:
|
||||||
|
if strong {
|
||||||
|
return cYellowB
|
||||||
|
}
|
||||||
|
return cYellow
|
||||||
|
default:
|
||||||
|
if strong {
|
||||||
|
return cGreenB
|
||||||
|
}
|
||||||
|
return cGreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func countColor(n int64) *color.Color {
|
||||||
|
if n == 0 {
|
||||||
|
return cDark
|
||||||
|
}
|
||||||
|
return cGrey
|
||||||
|
}
|
||||||
|
|
||||||
|
func pad(s string, w int) string {
|
||||||
|
if n := w - utf8.RuneCountInString(s); n > 0 {
|
||||||
|
return strings.Repeat(" ", n)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anteilsbalken in Achtelschritten, feste Breite barCells.
|
||||||
|
const barCells = 10
|
||||||
|
|
||||||
|
var eighths = []rune{' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉'}
|
||||||
|
|
||||||
|
func bar(frac float64) string {
|
||||||
|
if frac > 1 {
|
||||||
|
frac = 1
|
||||||
|
}
|
||||||
|
if !(frac > 0) { // fängt auch NaN
|
||||||
|
return strings.Repeat(" ", barCells)
|
||||||
|
}
|
||||||
|
units := int(frac*barCells*8 + 0.5)
|
||||||
|
full, rem := units/8, units%8
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(strings.Repeat("█", full))
|
||||||
|
used := full
|
||||||
|
if rem > 0 {
|
||||||
|
b.WriteRune(eighths[rem])
|
||||||
|
used++
|
||||||
|
}
|
||||||
|
b.WriteString(strings.Repeat(" ", barCells-used))
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func percent(frac float64) string {
|
||||||
|
p := frac * 100
|
||||||
|
if p > 0 && p < 0.5 {
|
||||||
|
return " <1%"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%3.0f%%", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printDir(w *bufio.Writer, dir string, res result, o opts) {
|
||||||
|
metric := func(e entry) int64 {
|
||||||
|
if o.byCount {
|
||||||
|
return e.n
|
||||||
|
}
|
||||||
|
return e.size
|
||||||
|
}
|
||||||
|
sort.SliceStable(res.entries, func(i, j int) bool {
|
||||||
|
a, b := res.entries[i], res.entries[j]
|
||||||
|
if ka, kb := metric(a), metric(b); ka != kb {
|
||||||
|
return ka < kb
|
||||||
|
}
|
||||||
|
return a.name < b.name // Perl ließ Gleichstände der Hash-Ordnung
|
||||||
|
})
|
||||||
|
|
||||||
|
// -t N: nur die N größten zeigen, der Rest wird zu einer Zeile.
|
||||||
|
rows := res.entries
|
||||||
|
var rest *entry
|
||||||
|
if o.top > 0 && len(rows) > o.top {
|
||||||
|
cut := len(rows) - o.top
|
||||||
|
var agg entry
|
||||||
|
for _, e := range rows[:cut] {
|
||||||
|
agg.n += e.n
|
||||||
|
agg.size += e.size
|
||||||
|
}
|
||||||
|
agg.name = fmt.Sprintf("… %d more", cut)
|
||||||
|
agg.col, agg.agg = cDark, true
|
||||||
|
rest, rows = &agg, rows[cut:]
|
||||||
|
}
|
||||||
|
if o.reverse {
|
||||||
|
for i, j := 0, len(rows)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
rows[i], rows[j] = rows[j], rows[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rest != nil { // Restzeile bleibt am kleinen Ende
|
||||||
|
if o.reverse {
|
||||||
|
rows = append(rows, *rest)
|
||||||
|
} else {
|
||||||
|
rows = append([]entry{*rest}, rows...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
total := res.totalS
|
||||||
|
if o.byCount {
|
||||||
|
total = res.totalN
|
||||||
|
}
|
||||||
|
|
||||||
|
ml := utf8.RuneCountInString(dir)
|
||||||
|
for _, e := range rows {
|
||||||
|
if n := utf8.RuneCountInString(e.name); n > ml {
|
||||||
|
ml = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
width := ml + 1
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s%s %s %s\n",
|
||||||
|
cDirB.Sprint(dir), pad(dir, width),
|
||||||
|
cWhiteB.Sprintf("%8d", res.totalN),
|
||||||
|
sizeColor(res.totalS, true).Sprintf("%8s", formSize(res.totalS)))
|
||||||
|
|
||||||
|
for _, e := range rows {
|
||||||
|
fmt.Fprintf(w, " %s%s %s %s",
|
||||||
|
e.col.Sprint(e.name), pad(e.name, width),
|
||||||
|
countColor(e.n).Sprintf("%8d", e.n),
|
||||||
|
sizeColor(e.size, false).Sprintf("%8s", formSize(e.size)))
|
||||||
|
|
||||||
|
if o.bar {
|
||||||
|
var frac float64
|
||||||
|
if total > 0 {
|
||||||
|
frac = float64(metric(e)) / float64(total)
|
||||||
|
}
|
||||||
|
// Balken trägt die Farbe der Spalte, deren Anteil er zeigt.
|
||||||
|
col := sizeColor(e.size, false)
|
||||||
|
if o.byCount {
|
||||||
|
col = countColor(e.n)
|
||||||
|
}
|
||||||
|
if e.agg { // Sammelzeile bleibt zurückhaltend
|
||||||
|
col = cDark
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, " %s %s",
|
||||||
|
col.Sprint(bar(frac)), cGrey.Sprint(percent(frac)))
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------- CLI
|
||||||
|
|
||||||
|
type opts struct {
|
||||||
|
byCount bool // -n
|
||||||
|
onlyDirs bool // -d
|
||||||
|
reverse bool // -r
|
||||||
|
top int // -t N, 0 = alle
|
||||||
|
bar bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage(w *os.File) {
|
||||||
|
fmt.Fprintf(w, `dx %s, count files and directories, mwx'2016
|
||||||
|
usage: dx [-n] [-d] [-r] [-t N] [path]
|
||||||
|
-n sort by number of files (default by size)
|
||||||
|
-d count only directories
|
||||||
|
-r reverse order (largest first)
|
||||||
|
-t N show only the N largest, sum up the rest
|
||||||
|
-h this help
|
||||||
|
--bar / --no-bar share bar and percentage (default: when colorized)
|
||||||
|
--color=auto|always|never colorize output (default auto)
|
||||||
|
--version
|
||||||
|
`, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func die(format string, a ...any) {
|
||||||
|
fmt.Fprintf(os.Stderr, "dx: "+format+"\n", a...)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var o opts
|
||||||
|
var barSet bool // sonst folgt der Balken der Farbentscheidung
|
||||||
|
var dir string
|
||||||
|
when := "auto"
|
||||||
|
|
||||||
|
args := os.Args[1:]
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
a := args[i]
|
||||||
|
switch {
|
||||||
|
case a == "--":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
dir = args[i+1]
|
||||||
|
}
|
||||||
|
i = len(args)
|
||||||
|
case a == "--help":
|
||||||
|
usage(os.Stdout)
|
||||||
|
return
|
||||||
|
case a == "--version":
|
||||||
|
fmt.Printf("dx %s\n", version)
|
||||||
|
return
|
||||||
|
case a == "--number":
|
||||||
|
o.byCount = true
|
||||||
|
case a == "--dirs":
|
||||||
|
o.onlyDirs = true
|
||||||
|
case a == "--reverse":
|
||||||
|
o.reverse = true
|
||||||
|
case a == "--bar":
|
||||||
|
o.bar, barSet = true, true
|
||||||
|
case a == "--no-bar":
|
||||||
|
o.bar, barSet = false, true
|
||||||
|
case a == "--no-color":
|
||||||
|
when = "never"
|
||||||
|
case strings.HasPrefix(a, "--top="):
|
||||||
|
o.top = mustNum(strings.TrimPrefix(a, "--top="))
|
||||||
|
case strings.HasPrefix(a, "--color="):
|
||||||
|
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
|
||||||
|
switch flags[j] {
|
||||||
|
case 'n':
|
||||||
|
o.byCount = true
|
||||||
|
case 'd':
|
||||||
|
o.onlyDirs = true
|
||||||
|
case 'r':
|
||||||
|
o.reverse = true
|
||||||
|
case 'h':
|
||||||
|
usage(os.Stdout)
|
||||||
|
return
|
||||||
|
case 't': // Wert klebt am Flag (-t5) oder folgt (-t 5)
|
||||||
|
val := flags[j+1:]
|
||||||
|
if val == "" {
|
||||||
|
if i+1 >= len(args) {
|
||||||
|
die("option requires an argument -- t")
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
val = args[i]
|
||||||
|
}
|
||||||
|
o.top = mustNum(val)
|
||||||
|
j = len(flags)
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "dx: unknown option -- %c\n", flags[j])
|
||||||
|
usage(os.Stderr)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
dir = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch when {
|
||||||
|
case "always":
|
||||||
|
color.NoColor = false
|
||||||
|
case "never":
|
||||||
|
color.NoColor = true
|
||||||
|
case "auto": // fatih/color entscheidet per isatty und NO_COLOR
|
||||||
|
default:
|
||||||
|
die("--color: expected auto|always|never, got %q", when)
|
||||||
|
}
|
||||||
|
if !barSet {
|
||||||
|
o.bar = !color.NoColor
|
||||||
|
}
|
||||||
|
|
||||||
|
if dir == "" {
|
||||||
|
dir = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := scan(dir, o.onlyDirs)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "dx: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := bufio.NewWriter(os.Stdout)
|
||||||
|
printDir(w, dir, res, o)
|
||||||
|
w.Flush()
|
||||||
|
|
||||||
|
if res.unreadbl > 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, cDark.Sprintf("dx: %d unreadable entries skipped", res.unreadbl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustNum(s string) int {
|
||||||
|
n, err := strconv.Atoi(s)
|
||||||
|
if err != nil || n < 1 {
|
||||||
|
die("-t: expected a positive number, got %q", s)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
2.1.4
|
||||||
Reference in New Issue
Block a user