initial commit [141.14.140.180,mike]

This commit is contained in:
2026-08-10 16:35:51 +02:00
commit 6249cd387c
8 changed files with 862 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
.DS_Store
.AppleDouble
.LSOverride
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
bin/
dx
.mgshrc
+172
View File
@@ -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.
Executable
+60
View File
@@ -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"
+93
View File
@@ -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
}
+11
View File
@@ -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
)
+9
View File
@@ -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=
+504
View File
@@ -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
}
+1
View File
@@ -0,0 +1 @@
2.1.4