Repository: muesli/duf Branch: master Commit: 4636deb4a7b7 Files: 35 Total size: 101.4 KB Directory structure: gitextract_nb2n7rrk/ ├── .github/ │ ├── FUNDING.yml │ ├── dependabot.yml │ └── workflows/ │ ├── build.yml │ ├── goreleaser.yml │ ├── lint-soft.yml │ ├── lint.yml │ └── manpage.yml ├── .gitignore ├── .golangci-soft.yml ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── duf.1 ├── filesystems.go ├── filesystems_darwin.go ├── filesystems_freebsd.go ├── filesystems_linux.go ├── filesystems_openbsd.go ├── filesystems_windows.go ├── go.mod ├── go.sum ├── groups.go ├── main.go ├── man.go ├── mounts.go ├── mounts_darwin.go ├── mounts_freebsd.go ├── mounts_linux.go ├── mounts_linux_test.go ├── mounts_openbsd.go ├── mounts_windows.go ├── style.go ├── table.go └── themes.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: muesli ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" labels: - "dependencies" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" labels: - "dependencies" ================================================ FILE: .github/workflows/build.yml ================================================ name: build on: [push, pull_request] jobs: build: strategy: matrix: go-version: [~1.23, ^1] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} env: GO111MODULE: "on" steps: - name: Install Go uses: actions/setup-go@v6.0.0 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v5 - name: Download Go modules run: go mod download - name: Build run: go build -v ./... - name: Test run: go test ./... ================================================ FILE: .github/workflows/goreleaser.yml ================================================ name: goreleaser on: pull_request: push: jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v6.0.0 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: version: latest args: release --snapshot --skip publish --skip sign --clean ================================================ FILE: .github/workflows/lint-soft.yml ================================================ name: lint-soft on: push: branches: - master pull_request: permissions: contents: read # Optional: allow read access to pull request. Use with `only-new-issues` option. pull-requests: read jobs: golangci: name: lint-soft runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-go@v6.0.0 with: go-version: stable - name: golangci-lint uses: golangci/golangci-lint-action@v8 with: # Optional: golangci-lint command line arguments. args: --config .golangci-soft.yml --issues-exit-code=0 # Optional: show only new issues if it's a pull request. The default value is `false`. only-new-issues: true ================================================ FILE: .github/workflows/lint.yml ================================================ name: lint on: push: branches: - master pull_request: permissions: contents: read # Optional: allow read access to pull request. Use with `only-new-issues` option. pull-requests: read jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-go@v6.0.0 with: go-version: stable - name: golangci-lint uses: golangci/golangci-lint-action@v8 with: # Optional: golangci-lint command line arguments. #args: # Optional: show only new issues if it's a pull request. The default value is `false`. only-new-issues: true ================================================ FILE: .github/workflows/manpage.yml ================================================ name: manpage on: push: branches: - master jobs: manpage: runs-on: ubuntu-latest steps: - name: Install Go uses: actions/setup-go@v6.0.0 with: go-version: 1.23 - name: Checkout code uses: actions/checkout@v5 - name: Download Go modules run: go mod download - name: Build run: go build -v -tags mango - name: Generate man-page run: ./duf > duf.1 - name: Commit uses: stefanzweifel/git-auto-commit-action@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: commit_message: "docs: update man page" branch: master commit_user_name: mango 🤖 commit_user_email: actions@github.com commit_author: mango 🤖 ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ duf dist/ ================================================ FILE: .golangci-soft.yml ================================================ version: "2" run: tests: false linters: enable: - exhaustive - goconst - godot - godox - gomoddirectives - goprintffuncname - misspell - mnd - nakedret - nestif - noctx - nolintlint - prealloc - wrapcheck disable: - errcheck - govet - ineffassign - staticcheck - unused exclusions: generated: lax presets: - common-false-positives issues: max-issues-per-linter: 0 max-same-issues: 0 formatters: exclusions: generated: lax ================================================ FILE: .golangci.yml ================================================ version: "2" run: tests: false linters: enable: - bodyclose - gosec - nilerr - predeclared - revive - rowserrcheck - sqlclosecheck - tparallel - unconvert - unparam - whitespace exclusions: generated: lax presets: - common-false-positives issues: max-issues-per-linter: 0 max-same-issues: 0 formatters: enable: - goimports exclusions: generated: lax ================================================ FILE: .goreleaser.yml ================================================ version: 2 env: - CGO_ENABLED=0 before: hooks: - go mod tidy builds: - binary: duf flags: - -trimpath ldflags: -s -w -X main.Version={{ .Version }} -X main.CommitSHA={{ .Commit }} goos: - linux - freebsd - openbsd - darwin - windows goarch: - amd64 - arm64 - 386 - arm - ppc64le goarm: - 6 - 7 ignore: - goos: windows goarm: "6" - goos: windows goarm: "7" archives: - format_overrides: - goos: windows formats: ['zip'] name_template: >- {{- .ProjectName }}_ {{- .Version }}_ {{- .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end -}} files: - duf.1 nfpms: - ids: - duf vendor: muesli homepage: "https://fribbledom.com/" maintainer: "Christian Muehlhaeuser " description: "Disk Usage/Free Utility" license: MIT formats: - apk - deb - rpm bindir: /usr/bin homebrew_casks: - repository: owner: muesli name: homebrew-tap commit_author: name: "Christian Muehlhaeuser" email: "muesli@gmail.com" homepage: "https://fribbledom.com/" description: "Disk Usage/Free Utility" manpages: - duf.1 skip_upload: true signs: - artifacts: checksum checksum: name_template: "checksums.txt" snapshot: version_template: "{{ .Tag }}-next" changelog: sort: asc filters: exclude: - "^docs:" - "^test:" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Christian Muehlhaeuser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- Portions of duf's code are copied and modified from https://github.com/shirou/gopsutil. gopsutil is distributed under BSD license reproduced below. Copyright (c) 2014, WAKAYAMA Shirou All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the gopsutil authors nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # duf [![Latest Release](https://img.shields.io/github/release/muesli/duf.svg?style=for-the-badge)](https://github.com/muesli/duf/releases) [![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge)](https://pkg.go.dev/github.com/muesli/duf) [![Software License](https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge)](/LICENSE) [![Build Status](https://img.shields.io/github/actions/workflow/status/muesli/duf/build.yml?style=for-the-badge&branch=master)](https://github.com/muesli/duf/actions) [![Go ReportCard](https://goreportcard.com/badge/github.com/muesli/duf?style=for-the-badge)](https://goreportcard.com/report/muesli/duf) Disk Usage/Free Utility (Linux, BSD, macOS & Windows) ![duf](/duf.png) ## Features - [x] User-friendly, colorful output - [x] Adjusts to your terminal's theme & width - [x] Sort the results according to your needs - [x] Groups & filters devices - [x] Can conveniently output JSON ## Installation ### Packages #### Linux - Arch Linux: `pacman -S duf` - Ubuntu (22.04 and later) / Debian (12 and later): `apt install duf` - Fedora Linux: `dnf install duf` - Nix: `nix-env -iA nixpkgs.duf` - Void Linux: `xbps-install -S duf` - Gentoo Linux: `emerge sys-fs/duf` - Solus: `eopkg it duf` - [Packages](https://github.com/muesli/duf/releases) in Alpine, Debian & RPM formats #### BSD - FreeBSD: `pkg install duf` - OpenBSD: `pkg_add duf` #### macOS - with [Homebrew](https://brew.sh/): `brew install duf` - with [MacPorts](https://www.macports.org): `sudo port selfupdate && sudo port install duf` #### Windows - with [Chocolatey](https://chocolatey.org/): `choco install duf` - with [scoop](https://scoop.sh/): `scoop install duf` #### Android - Android (via termux): `pkg install duf` ### Binaries - [Binaries](https://github.com/muesli/duf/releases) for Linux, FreeBSD, OpenBSD, macOS, Windows ### From source Make sure you have a working Go environment (Go 1.23 or higher is required). See the [install instructions](https://golang.org/doc/install.html). Compiling duf is easy, simply run: git clone https://github.com/muesli/duf.git cd duf go build ## Usage You can simply start duf without any command-line arguments: duf If you supply arguments, duf will only list specific devices & mount points: duf /home /some/file If you want to list everything (including pseudo, duplicate, inaccessible file systems): duf --all ### Filtering You can show and hide specific tables: duf --only local,network,fuse,special,loops,binds duf --hide local,network,fuse,special,loops,binds You can also show and hide specific filesystems: duf --only-fs tmpfs,vfat duf --hide-fs tmpfs,vfat ...or specific mount points: duf --only-mp /,/home,/dev duf --hide-mp /,/home,/dev Wildcards inside quotes work: duf --only-mp '/sys/*,/dev/*' ### Display options Sort the output: duf --sort size Valid keys are: `mountpoint`, `size`, `used`, `avail`, `usage`, `inodes`, `inodes_used`, `inodes_avail`, `inodes_usage`, `type`, `filesystem`. Show or hide specific columns: duf --output mountpoint,size,usage Valid keys are: `mountpoint`, `size`, `used`, `avail`, `usage`, `inodes`, `inodes_used`, `inodes_avail`, `inodes_usage`, `type`, `filesystem`. List inode information instead of block usage: duf --inodes If duf doesn't detect your terminal's colors correctly, you can set a theme: duf --theme light ### Color-coding & Thresholds duf highlights the availability & usage columns in red, green, or yellow, depending on how much space is still available. You can set your own thresholds: duf --avail-threshold="10G,1G" duf --usage-threshold="0.5,0.9" ### Bonus If you prefer your output as JSON: duf --json ## Troubleshooting Users of `oh-my-zsh` should be aware that it already defines an alias called `duf`, which you will have to remove in order to use `duf`: unalias duf ## Feedback Got some feedback or suggestions? Please open an issue or drop me a note! * [Twitter](https://twitter.com/mueslix) * [The Fediverse](https://mastodon.social/@fribbledom) ================================================ FILE: duf.1 ================================================ .TH DUF 1 "2025-09-30" "duf" "Disk Usage/Free Utility" .SH NAME duf - Disk Usage/Free Utility .SH SYNOPSIS \fBduf\fP [\fIoptions\&.\&.\&.\fP] [\fIargument\&.\&.\&.\fP] .SH DESCRIPTION Simple Disk Usage/Free Utility\&. .PP Features: .PP .RS .IP \(bu 3 User-friendly, colorful output\&. .IP \(bu 3 Adjusts to your terminal's theme & width\&. .IP \(bu 3 Sort the results according to your needs\&. .IP \(bu 3 Groups & filters devices\&. .IP \(bu 3 Can conveniently output JSON\&. .SH OPTIONS .TP \fB--all\fP include pseudo, duplicate, inaccessible file systems .TP \fB--avail-threshold\fP specifies the coloring threshold (yellow, red) of the avail column, must be integer with optional SI prefixes .TP \fB--hide\fP hide specific devices, separated with commas: local, network, fuse, special, loops, binds .TP \fB--hide-fs\fP hide specific filesystems, separated with commas .TP \fB--hide-mp\fP hide specific mount points, separated with commas (supports wildcards) .TP \fB-h, --human-readable\fP ignored, just for df compatibility .TP \fB--inodes\fP list inode information instead of block usage .TP \fB--json\fP output all devices in JSON format .TP \fB--only\fP show only specific devices, separated with commas: local, network, fuse, special, loops, binds .TP \fB--only-fs\fP only specific filesystems, separated with commas .TP \fB--only-mp\fP only specific mount points, separated with commas (supports wildcards) .TP \fB--output\fP output fields: mountpoint, size, used, avail, usage, inodes, inodes_used, inodes_avail, inodes_usage, type, filesystem .TP \fB--sort\fP sort output by: mountpoint, size, used, avail, usage, inodes, inodes_used, inodes_avail, inodes_usage, type, filesystem .TP \fB--style\fP style: unicode, ascii .TP \fB--theme\fP color themes: dark, light, ansi .TP \fB--usage-threshold\fP specifies the coloring threshold (yellow, red) of the usage bars as a floating point number from 0 to 1 .TP \fB--version\fP display version .TP \fB--warnings\fP output all warnings to STDERR .TP \fB--width\fP max output width .SH USAGE You can simply start duf without any command-line arguments: .PP .PP $ duf .PP .PP If you supply arguments, duf will only list specific devices & mount points: .PP .PP $ duf /home /some/file .PP .PP If you want to list everything (including pseudo, duplicate, inaccessible file systems): .PP .PP $ duf --all .PP .PP You can show and hide specific tables: .PP .PP $ duf --only local,network,fuse,special,loops,binds .PP $ duf --hide local,network,fuse,special,loops,binds .PP .PP You can also show and hide specific filesystems: .PP .PP $ duf --only-fs tmpfs,vfat .PP $ duf --hide-fs tmpfs,vfat .PP .PP \&.\&.\&.or specific mount points: .PP .PP $ duf --only-mp /,/home,/dev .PP $ duf --hide-mp /,/home,/dev .PP .PP Wildcards inside quotes work: .PP .PP $ duf --only-mp '/sys/*,/dev/*' .PP .PP Sort the output: .PP .PP $ duf --sort size .PP .PP Valid keys are: mountpoint, size, used, avail, usage, inodes, inodes_used, inodes_avail, inodes_usage, type, filesystem\&. .PP .PP Show or hide specific columns: .PP .PP $ duf --output mountpoint,size,usage .PP .PP Valid keys are: mountpoint, size, used, avail, usage, inodes, inodes_used, inodes_avail, inodes_usage, type, filesystem\&. .PP .PP List inode information instead of block usage: .PP .PP $ duf --inodes .PP .PP If duf doesn't detect your terminal's colors correctly, you can set a theme: .PP .PP $ duf --theme light .PP .PP duf highlights the availability & usage columns in red, green, or yellow, depending on how much space is still available\&. You can set your own thresholds: .PP .PP $ duf --avail-threshold="10G,1G" .PP $ duf --usage-threshold="0\&.5,0\&.9" .PP .PP If you prefer your output as JSON: .PP .PP $ duf --json .PP .SH NOTES Portions of duf's code are copied and modified from https://github\&.com/shirou/gopsutil\&. .PP gopsutil was written by WAKAYAMA Shirou and is distributed under BSD-3-Clause\&. .SH AUTHORS duf was written by Christian Muehlhaeuser .SH COPYRIGHT Copyright (C) 2020-2022 Christian Muehlhaeuser .PP Released under MIT license\&. ================================================ FILE: filesystems.go ================================================ package main import ( "os" "path/filepath" "strings" ) func findMounts(mounts []Mount, path string) ([]Mount, error) { var err error path, err = filepath.Abs(path) if err != nil { return nil, err } path, err = filepath.EvalSymlinks(path) if err != nil { return nil, err } _, err = os.Stat(path) if err != nil { return nil, err } var m []Mount for _, v := range mounts { if path == v.Device { return []Mount{v}, nil } if strings.HasPrefix(path, v.Mountpoint) { var nm []Mount // keep all entries that are as close or closer to the target for _, mv := range m { if len(mv.Mountpoint) >= len(v.Mountpoint) { nm = append(nm, mv) } } m = nm // add entry only if we didn't already find something closer if len(nm) == 0 || len(v.Mountpoint) >= len(nm[0].Mountpoint) { m = append(m, v) } } } return m, nil } func deviceType(m Mount) string { if isNetworkFs(m) { return networkDevice } if isSpecialFs(m) { return specialDevice } if isFuseFs(m) { return fuseDevice } return localDevice } // remote: [ "nfs", "smbfs", "cifs", "ncpfs", "afs", "coda", "ftpfs", "mfs", "sshfs", "fuse.sshfs", "nfs4" ] // special: [ "tmpfs", "devpts", "devtmpfs", "proc", "sysfs", "usbfs", "devfs", "fdescfs", "linprocfs" ] ================================================ FILE: filesystems_darwin.go ================================================ //go:build darwin // +build darwin package main func isFuseFs(m Mount) bool { //FIXME: implement return false } func isNetworkFs(m Mount) bool { //FIXME: implement return false } func isSpecialFs(m Mount) bool { return m.Fstype == "devfs" } func isHiddenFs(m Mount) bool { return false } ================================================ FILE: filesystems_freebsd.go ================================================ //go:build freebsd // +build freebsd package main func isFuseFs(m Mount) bool { //FIXME: implement return false } func isNetworkFs(m Mount) bool { fs := []string{"nfs", "smbfs"} for _, v := range fs { if m.Fstype == v { return true } } return false } func isSpecialFs(m Mount) bool { fs := []string{"devfs", "tmpfs", "linprocfs", "linsysfs", "fdescfs", "procfs"} for _, v := range fs { if m.Fstype == v { return true } } return false } func isHiddenFs(m Mount) bool { return false } ================================================ FILE: filesystems_linux.go ================================================ //go:build linux // +build linux package main import "strings" //nolint:revive const ( // man statfs ADFS_SUPER_MAGIC = 0xadf5 AFFS_SUPER_MAGIC = 0xADFF AUTOFS_SUPER_MAGIC = 0x0187 BDEVFS_MAGIC = 0x62646576 BEFS_SUPER_MAGIC = 0x42465331 BFS_MAGIC = 0x1BADFACE BINFMTFS_MAGIC = 0x42494e4d BPF_FS_MAGIC = 0xcafe4a11 BTRFS_SUPER_MAGIC = 0x9123683E CGROUP_SUPER_MAGIC = 0x27e0eb CGROUP2_SUPER_MAGIC = 0x63677270 CIFS_MAGIC_NUMBER = 0xFF534D42 CODA_SUPER_MAGIC = 0x73757245 COH_SUPER_MAGIC = 0x012FF7B7 CONFIGFS_MAGIC = 0x62656570 CRAMFS_MAGIC = 0x28cd3d45 DEBUGFS_MAGIC = 0x64626720 DEVFS_SUPER_MAGIC = 0x1373 DEVPTS_SUPER_MAGIC = 0x1cd1 EFIVARFS_MAGIC = 0xde5e81e4 EFS_SUPER_MAGIC = 0x00414A53 EXT_SUPER_MAGIC = 0x137D EXT2_OLD_SUPER_MAGIC = 0xEF51 EXT2_SUPER_MAGIC = 0xEF53 EXT3_SUPER_MAGIC = 0xEF53 EXT4_SUPER_MAGIC = 0xEF53 FUSE_SUPER_MAGIC = 0x65735546 FUTEXFS_SUPER_MAGIC = 0xBAD1DEA HFS_SUPER_MAGIC = 0x4244 HFSPLUS_SUPER_MAGIC = 0x482b HOSTFS_SUPER_MAGIC = 0x00c0ffee HPFS_SUPER_MAGIC = 0xF995E849 HUGETLBFS_MAGIC = 0x958458f6 ISOFS_SUPER_MAGIC = 0x9660 JFFS2_SUPER_MAGIC = 0x72b6 JFS_SUPER_MAGIC = 0x3153464a MINIX_SUPER_MAGIC = 0x137F /* orig. minix */ MINIX_SUPER_MAGIC2 = 0x138F /* 30 char minix */ MINIX2_SUPER_MAGIC = 0x2468 /* minix V2 */ MINIX2_SUPER_MAGIC2 = 0x2478 /* minix V2, 30 char names */ MINIX3_SUPER_MAGIC = 0x4d5a /* minix V3 fs, 60 char names */ MQUEUE_MAGIC = 0x19800202 MSDOS_SUPER_MAGIC = 0x4d44 NCP_SUPER_MAGIC = 0x564c NFS_SUPER_MAGIC = 0x6969 NILFS_SUPER_MAGIC = 0x3434 NTFS_SB_MAGIC = 0x5346544e OCFS2_SUPER_MAGIC = 0x7461636f OPENPROM_SUPER_MAGIC = 0x9fa1 PIPEFS_MAGIC = 0x50495045 PROC_SUPER_MAGIC = 0x9fa0 PSTOREFS_MAGIC = 0x6165676C QNX4_SUPER_MAGIC = 0x002f QNX6_SUPER_MAGIC = 0x68191122 RAMFS_MAGIC = 0x858458f6 REISERFS_SUPER_MAGIC = 0x52654973 ROMFS_MAGIC = 0x7275 SELINUX_MAGIC = 0xf97cff8c SMACK_MAGIC = 0x43415d53 SMB_SUPER_MAGIC = 0x517B SMB2_MAGIC_NUMBER = 0xfe534d42 SOCKFS_MAGIC = 0x534F434B SQUASHFS_MAGIC = 0x73717368 SYSFS_MAGIC = 0x62656572 SYSV2_SUPER_MAGIC = 0x012FF7B6 SYSV4_SUPER_MAGIC = 0x012FF7B5 TMPFS_MAGIC = 0x01021994 TRACEFS_MAGIC = 0x74726163 UDF_SUPER_MAGIC = 0x15013346 UFS_MAGIC = 0x00011954 USBDEVICE_SUPER_MAGIC = 0x9fa2 V9FS_MAGIC = 0x01021997 VXFS_SUPER_MAGIC = 0xa501FCF5 XENFS_SUPER_MAGIC = 0xabba1974 XENIX_SUPER_MAGIC = 0x012FF7B4 XFS_SUPER_MAGIC = 0x58465342 _XIAFS_SUPER_MAGIC = 0x012FD16D AFS_SUPER_MAGIC = 0x5346414F AUFS_SUPER_MAGIC = 0x61756673 ANON_INODE_FS_SUPER_MAGIC = 0x09041934 CEPH_SUPER_MAGIC = 0x00C36400 ECRYPTFS_SUPER_MAGIC = 0xF15F FAT_SUPER_MAGIC = 0x4006 FHGFS_SUPER_MAGIC = 0x19830326 FUSEBLK_SUPER_MAGIC = 0x65735546 FUSECTL_SUPER_MAGIC = 0x65735543 GFS_SUPER_MAGIC = 0x1161970 GPFS_SUPER_MAGIC = 0x47504653 MTD_INODE_FS_SUPER_MAGIC = 0x11307854 INOTIFYFS_SUPER_MAGIC = 0x2BAD1DEA ISOFS_R_WIN_SUPER_MAGIC = 0x4004 ISOFS_WIN_SUPER_MAGIC = 0x4000 JFFS_SUPER_MAGIC = 0x07C0 KAFS_SUPER_MAGIC = 0x6B414653 LUSTRE_SUPER_MAGIC = 0x0BD00BD0 NFSD_SUPER_MAGIC = 0x6E667364 PANFS_SUPER_MAGIC = 0xAAD7AAEA RPC_PIPEFS_SUPER_MAGIC = 0x67596969 SECURITYFS_SUPER_MAGIC = 0x73636673 UFS_BYTESWAPPED_SUPER_MAGIC = 0x54190100 VMHGFS_SUPER_MAGIC = 0xBACBACBC VZFS_SUPER_MAGIC = 0x565A4653 ZFS_SUPER_MAGIC = 0x2FC12FC1 ) // coreutils/src/stat.c var fsTypeMap = map[int64]string{ ADFS_SUPER_MAGIC: "adfs", /* 0xADF5 local */ AFFS_SUPER_MAGIC: "affs", /* 0xADFF local */ AFS_SUPER_MAGIC: "afs", /* 0x5346414F remote */ ANON_INODE_FS_SUPER_MAGIC: "anon-inode FS", /* 0x09041934 local */ AUFS_SUPER_MAGIC: "aufs", /* 0x61756673 remote */ AUTOFS_SUPER_MAGIC: "autofs", /* 0x0187 local */ BEFS_SUPER_MAGIC: "befs", /* 0x42465331 local */ BDEVFS_MAGIC: "bdevfs", /* 0x62646576 local */ BFS_MAGIC: "bfs", /* 0x1BADFACE local */ BINFMTFS_MAGIC: "binfmt_misc", /* 0x42494E4D local */ BTRFS_SUPER_MAGIC: "btrfs", /* 0x9123683E local */ CEPH_SUPER_MAGIC: "ceph", /* 0x00C36400 remote */ CGROUP_SUPER_MAGIC: "cgroupfs", /* 0x0027E0EB local */ CIFS_MAGIC_NUMBER: "cifs", /* 0xFF534D42 remote */ CODA_SUPER_MAGIC: "coda", /* 0x73757245 remote */ COH_SUPER_MAGIC: "coh", /* 0x012FF7B7 local */ CRAMFS_MAGIC: "cramfs", /* 0x28CD3D45 local */ DEBUGFS_MAGIC: "debugfs", /* 0x64626720 local */ DEVFS_SUPER_MAGIC: "devfs", /* 0x1373 local */ DEVPTS_SUPER_MAGIC: "devpts", /* 0x1CD1 local */ ECRYPTFS_SUPER_MAGIC: "ecryptfs", /* 0xF15F local */ EFS_SUPER_MAGIC: "efs", /* 0x00414A53 local */ EXT_SUPER_MAGIC: "ext", /* 0x137D local */ EXT2_SUPER_MAGIC: "ext2/ext3", /* 0xEF53 local */ EXT2_OLD_SUPER_MAGIC: "ext2", /* 0xEF51 local */ FAT_SUPER_MAGIC: "fat", /* 0x4006 local */ FHGFS_SUPER_MAGIC: "fhgfs", /* 0x19830326 remote */ FUSEBLK_SUPER_MAGIC: "fuseblk", /* 0x65735546 remote */ FUSECTL_SUPER_MAGIC: "fusectl", /* 0x65735543 remote */ FUTEXFS_SUPER_MAGIC: "futexfs", /* 0x0BAD1DEA local */ GFS_SUPER_MAGIC: "gfs/gfs2", /* 0x1161970 remote */ GPFS_SUPER_MAGIC: "gpfs", /* 0x47504653 remote */ HFS_SUPER_MAGIC: "hfs", /* 0x4244 local */ HFSPLUS_SUPER_MAGIC: "hfsplus", /* 0x482b local */ HPFS_SUPER_MAGIC: "hpfs", /* 0xF995E849 local */ HUGETLBFS_MAGIC: "hugetlbfs", /* 0x958458F6 local */ MTD_INODE_FS_SUPER_MAGIC: "inodefs", /* 0x11307854 local */ INOTIFYFS_SUPER_MAGIC: "inotifyfs", /* 0x2BAD1DEA local */ ISOFS_SUPER_MAGIC: "isofs", /* 0x9660 local */ ISOFS_R_WIN_SUPER_MAGIC: "isofs", /* 0x4004 local */ ISOFS_WIN_SUPER_MAGIC: "isofs", /* 0x4000 local */ JFFS_SUPER_MAGIC: "jffs", /* 0x07C0 local */ JFFS2_SUPER_MAGIC: "jffs2", /* 0x72B6 local */ JFS_SUPER_MAGIC: "jfs", /* 0x3153464A local */ KAFS_SUPER_MAGIC: "k-afs", /* 0x6B414653 remote */ LUSTRE_SUPER_MAGIC: "lustre", /* 0x0BD00BD0 remote */ MINIX_SUPER_MAGIC: "minix", /* 0x137F local */ MINIX_SUPER_MAGIC2: "minix (30 char.)", /* 0x138F local */ MINIX2_SUPER_MAGIC: "minix v2", /* 0x2468 local */ MINIX2_SUPER_MAGIC2: "minix v2 (30 char.)", /* 0x2478 local */ MINIX3_SUPER_MAGIC: "minix3", /* 0x4D5A local */ MQUEUE_MAGIC: "mqueue", /* 0x19800202 local */ MSDOS_SUPER_MAGIC: "msdos", /* 0x4D44 local */ NCP_SUPER_MAGIC: "novell", /* 0x564C remote */ NFS_SUPER_MAGIC: "nfs", /* 0x6969 remote */ NFSD_SUPER_MAGIC: "nfsd", /* 0x6E667364 remote */ NILFS_SUPER_MAGIC: "nilfs", /* 0x3434 local */ NTFS_SB_MAGIC: "ntfs", /* 0x5346544E local */ OPENPROM_SUPER_MAGIC: "openprom", /* 0x9FA1 local */ OCFS2_SUPER_MAGIC: "ocfs2", /* 0x7461636f remote */ PANFS_SUPER_MAGIC: "panfs", /* 0xAAD7AAEA remote */ PIPEFS_MAGIC: "pipefs", /* 0x50495045 remote */ PROC_SUPER_MAGIC: "proc", /* 0x9FA0 local */ PSTOREFS_MAGIC: "pstorefs", /* 0x6165676C local */ QNX4_SUPER_MAGIC: "qnx4", /* 0x002F local */ QNX6_SUPER_MAGIC: "qnx6", /* 0x68191122 local */ RAMFS_MAGIC: "ramfs", /* 0x858458F6 local */ REISERFS_SUPER_MAGIC: "reiserfs", /* 0x52654973 local */ ROMFS_MAGIC: "romfs", /* 0x7275 local */ RPC_PIPEFS_SUPER_MAGIC: "rpc_pipefs", /* 0x67596969 local */ SECURITYFS_SUPER_MAGIC: "securityfs", /* 0x73636673 local */ SELINUX_MAGIC: "selinux", /* 0xF97CFF8C local */ SMB_SUPER_MAGIC: "smb", /* 0x517B remote */ SMB2_MAGIC_NUMBER: "smb2", /* 0xfe534d42 remote */ SOCKFS_MAGIC: "sockfs", /* 0x534F434B local */ SQUASHFS_MAGIC: "squashfs", /* 0x73717368 local */ SYSFS_MAGIC: "sysfs", /* 0x62656572 local */ SYSV2_SUPER_MAGIC: "sysv2", /* 0x012FF7B6 local */ SYSV4_SUPER_MAGIC: "sysv4", /* 0x012FF7B5 local */ TMPFS_MAGIC: "tmpfs", /* 0x01021994 local */ UDF_SUPER_MAGIC: "udf", /* 0x15013346 local */ UFS_MAGIC: "ufs", /* 0x00011954 local */ UFS_BYTESWAPPED_SUPER_MAGIC: "ufs", /* 0x54190100 local */ USBDEVICE_SUPER_MAGIC: "usbdevfs", /* 0x9FA2 local */ V9FS_MAGIC: "v9fs", /* 0x01021997 local */ VMHGFS_SUPER_MAGIC: "vmhgfs", /* 0xBACBACBC remote */ VXFS_SUPER_MAGIC: "vxfs", /* 0xA501FCF5 local */ VZFS_SUPER_MAGIC: "vzfs", /* 0x565A4653 local */ XENFS_SUPER_MAGIC: "xenfs", /* 0xABBA1974 local */ XENIX_SUPER_MAGIC: "xenix", /* 0x012FF7B4 local */ XFS_SUPER_MAGIC: "xfs", /* 0x58465342 local */ _XIAFS_SUPER_MAGIC: "xia", /* 0x012FD16D local */ ZFS_SUPER_MAGIC: "zfs", /* 0x2FC12FC1 local */ } /* var localMap = map[int64]bool{ AFS_SUPER_MAGIC: true, BTRFS_SUPER_MAGIC: true, EXT_SUPER_MAGIC: true, EXT2_OLD_SUPER_MAGIC: true, EXT2_SUPER_MAGIC: true, FAT_SUPER_MAGIC: true, HPFS_SUPER_MAGIC: true, MSDOS_SUPER_MAGIC: true, NTFS_SB_MAGIC: true, REISERFS_SUPER_MAGIC: true, UDF_SUPER_MAGIC: true, XFS_SUPER_MAGIC: true, ZFS_SUPER_MAGIC: true, } */ var networkMap = map[int64]bool{ CIFS_MAGIC_NUMBER: true, NFS_SUPER_MAGIC: true, SMB_SUPER_MAGIC: true, SMB2_MAGIC_NUMBER: true, } var specialMap = map[int64]bool{ AUTOFS_SUPER_MAGIC: true, BINFMTFS_MAGIC: true, BPF_FS_MAGIC: true, CGROUP_SUPER_MAGIC: true, CGROUP2_SUPER_MAGIC: true, CONFIGFS_MAGIC: true, DEBUGFS_MAGIC: true, DEVPTS_SUPER_MAGIC: true, EFIVARFS_MAGIC: true, FUSECTL_SUPER_MAGIC: true, HUGETLBFS_MAGIC: true, MQUEUE_MAGIC: true, PROC_SUPER_MAGIC: true, PSTOREFS_MAGIC: true, SECURITYFS_SUPER_MAGIC: true, SYSFS_MAGIC: true, TMPFS_MAGIC: true, TRACEFS_MAGIC: true, } /* func isLocalFs(m Mount) bool { return localMap[int64(m.Stat().Type)] //nolint:unconvert } */ func isFuseFs(m Mount) bool { return m.Stat().Type == FUSEBLK_SUPER_MAGIC || m.Stat().Type == FUSE_SUPER_MAGIC } func isNetworkFs(m Mount) bool { return networkMap[int64(m.Stat().Type)] //nolint:unconvert } func isSpecialFs(m Mount) bool { if m.Device == "nsfs" { return true } return specialMap[int64(m.Stat().Type)] //nolint:unconvert } func isHiddenFs(m Mount) bool { switch m.Device { case "shm": return true case "overlay": return true } switch m.Fstype { case "autofs": return true case "squashfs": if strings.HasPrefix(m.Mountpoint, "/snap") { return true } } return false } ================================================ FILE: filesystems_openbsd.go ================================================ //go:build openbsd // +build openbsd package main func isFuseFs(m Mount) bool { //FIXME: implement return false } func isNetworkFs(m Mount) bool { //FIXME: implement return false } func isSpecialFs(m Mount) bool { return m.Fstype == "devfs" } func isHiddenFs(m Mount) bool { return false } ================================================ FILE: filesystems_windows.go ================================================ //go:build windows // +build windows package main import ( "golang.org/x/sys/windows/registry" ) const ( WindowsSandboxMountPointRegistryPath = `Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2\CPC\LocalMOF` ) var windowsSandboxMountPoints = loadRegisteredWindowsSandboxMountPoints() func loadRegisteredWindowsSandboxMountPoints() (ret map[string]struct{}) { ret = make(map[string]struct{}) key, err := registry.OpenKey(registry.CURRENT_USER, WindowsSandboxMountPointRegistryPath, registry.READ) if err != nil { return } keyInfo, err := key.Stat() if err != nil { return } mountPoints, err := key.ReadValueNames(int(keyInfo.ValueCount)) if err != nil { return } for _, val := range mountPoints { ret[val] = struct{}{} } return ret } func isFuseFs(m Mount) bool { //FIXME: implement return false } func isNetworkFs(m Mount) bool { _, ok := m.Metadata.(*NetResource) return ok } func isSpecialFs(m Mount) bool { _, ok := windowsSandboxMountPoints[m.Mountpoint] return ok } func isHiddenFs(m Mount) bool { return false } ================================================ FILE: go.mod ================================================ module github.com/muesli/duf go 1.23.0 require ( github.com/IGLOU-EU/go-wildcard v1.0.3 github.com/jedib0t/go-pretty/v6 v6.6.8 github.com/mattn/go-runewidth v0.0.19 github.com/muesli/mango v0.2.0 github.com/muesli/mango-pflag v0.2.0 github.com/muesli/roff v0.1.0 github.com/muesli/termenv v0.16.0 github.com/spf13/pflag v1.0.10 golang.org/x/sys v0.35.0 golang.org/x/term v0.34.0 ) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/clipperhouse/uax29/v2 v2.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/text v0.22.0 // indirect ) ================================================ FILE: go.sum ================================================ github.com/IGLOU-EU/go-wildcard v1.0.3 h1:r8T46+8/9V1STciXJomTWRpPEv4nGJATDbJkdU0Nou0= github.com/IGLOU-EU/go-wildcard v1.0.3/go.mod h1:/qeV4QLmydCbwH0UMQJmXDryrFKJknWi/jjO8IiuQfY= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc= github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ= github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= github.com/muesli/mango-pflag v0.2.0 h1:QViokgKDZQCzKhYe1zH8D+UlPJzBSGoP9yx0hBG0t5k= github.com/muesli/mango-pflag v0.2.0/go.mod h1:X9LT1p/pbGA1wjvEbtwnixujKErkP0jVmrxwrw3fL0Y= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: groups.go ================================================ package main import ( "strings" ) const ( localDevice = "local" networkDevice = "network" fuseDevice = "fuse" specialDevice = "special" loopsDevice = "loops" bindsMount = "binds" ) // FilterOptions contains all filters. type FilterOptions struct { HiddenDevices map[string]struct{} OnlyDevices map[string]struct{} HiddenFilesystems map[string]struct{} OnlyFilesystems map[string]struct{} HiddenMountPoints map[string]struct{} OnlyMountPoints map[string]struct{} } // renderTables renders all tables. func renderTables(m []Mount, filters FilterOptions, opts TableOptions) { deviceMounts := make(map[string][]Mount) hasOnlyDevices := len(filters.OnlyDevices) != 0 _, hideLocal := filters.HiddenDevices[localDevice] _, hideNetwork := filters.HiddenDevices[networkDevice] _, hideFuse := filters.HiddenDevices[fuseDevice] _, hideSpecial := filters.HiddenDevices[specialDevice] _, hideLoops := filters.HiddenDevices[loopsDevice] _, hideBinds := filters.HiddenDevices[bindsMount] _, onlyLocal := filters.OnlyDevices[localDevice] _, onlyNetwork := filters.OnlyDevices[networkDevice] _, onlyFuse := filters.OnlyDevices[fuseDevice] _, onlySpecial := filters.OnlyDevices[specialDevice] _, onlyLoops := filters.OnlyDevices[loopsDevice] _, onlyBinds := filters.OnlyDevices[bindsMount] // sort/filter devices for _, v := range m { if len(filters.OnlyFilesystems) != 0 { // skip not onlyFs if _, ok := filters.OnlyFilesystems[strings.ToLower(v.Fstype)]; !ok { continue } } else { // skip hideFs if _, ok := filters.HiddenFilesystems[strings.ToLower(v.Fstype)]; ok { continue } } // skip hidden devices if isHiddenFs(v) && !*all { continue } // skip bind-mounts if strings.Contains(v.Opts, "bind") { if (hasOnlyDevices && !onlyBinds) || (hideBinds && !*all) { continue } } // skip loop devices if strings.HasPrefix(v.Device, "/dev/loop") { if (hasOnlyDevices && !onlyLoops) || (hideLoops && !*all) { continue } } // skip special devices if v.Blocks == 0 && !*all { continue } // skip zero size devices if v.BlockSize == 0 && !*all { continue } // skip not only mount point if len(filters.OnlyMountPoints) != 0 { if !findInKey(v.Mountpoint, filters.OnlyMountPoints) { continue } } // skip hidden mount point if len(filters.HiddenMountPoints) != 0 { if findInKey(v.Mountpoint, filters.HiddenMountPoints) { continue } } t := deviceType(v) deviceMounts[t] = append(deviceMounts[t], v) } // print tables for _, devType := range groups { mounts := deviceMounts[devType] shouldPrint := *all if !shouldPrint { switch devType { case localDevice: shouldPrint = (hasOnlyDevices && onlyLocal) || (!hasOnlyDevices && !hideLocal) case networkDevice: shouldPrint = (hasOnlyDevices && onlyNetwork) || (!hasOnlyDevices && !hideNetwork) case fuseDevice: shouldPrint = (hasOnlyDevices && onlyFuse) || (!hasOnlyDevices && !hideFuse) case specialDevice: shouldPrint = (hasOnlyDevices && onlySpecial) || (!hasOnlyDevices && !hideSpecial) } } if shouldPrint { printTable(devType, mounts, opts) } } } ================================================ FILE: main.go ================================================ package main import ( "encoding/json" "fmt" "os" "runtime/debug" "strconv" "strings" "time" wildcard "github.com/IGLOU-EU/go-wildcard" "github.com/jedib0t/go-pretty/v6/table" "github.com/muesli/termenv" flag "github.com/spf13/pflag" "golang.org/x/term" ) var ( // Version contains the application version number. It's set via ldflags // when building. Version = "" // CommitSHA contains the SHA of the commit that this application was built // against. It's set via ldflags when building. CommitSHA = "" env = termenv.EnvColorProfile() theme Theme groups = []string{localDevice, networkDevice, fuseDevice, specialDevice, loopsDevice, bindsMount} allowedValues = strings.Join(groups, ", ") all = flag.Bool("all", false, "include pseudo, duplicate, inaccessible file systems") hideDevices = flag.String("hide", "", "hide specific devices, separated with commas:\n"+allowedValues) hideFs = flag.String("hide-fs", "", "hide specific filesystems, separated with commas") hideMp = flag.String("hide-mp", "", "hide specific mount points, separated with commas (supports wildcards)") onlyDevices = flag.String("only", "", "show only specific devices, separated with commas:\n"+allowedValues) onlyFs = flag.String("only-fs", "", "only specific filesystems, separated with commas") onlyMp = flag.String("only-mp", "", "only specific mount points, separated with commas (supports wildcards)") output = flag.String("output", "", "output fields: "+strings.Join(columnIDs(), ", ")) sortBy = flag.String("sort", "mountpoint", "sort output by: "+strings.Join(columnIDs(), ", ")) width = flag.Uint("width", 0, "max output width") themeOpt = flag.String("theme", defaultThemeName(), "color themes: dark, light, ansi") styleOpt = flag.String("style", defaultStyleName(), "style: unicode, ascii") availThreshold = flag.String("avail-threshold", "10G,1G", "specifies the coloring threshold (yellow, red) of the avail column, must be integer with optional SI prefixes") usageThreshold = flag.String("usage-threshold", "0.5,0.9", "specifies the coloring threshold (yellow, red) of the usage bars as a floating point number from 0 to 1") _ = flag.BoolP("human-readable", "h", false, "ignored, just for df compatibility") inodes = flag.Bool("inodes", false, "list inode information instead of block usage") jsonOutput = flag.Bool("json", false, "output all devices in JSON format") warns = flag.Bool("warnings", false, "output all warnings to STDERR") version = flag.Bool("version", false, "display version") ) // renderJSON encodes the JSON output and prints it. func renderJSON(m []Mount) error { output, err := json.MarshalIndent(m, "", " ") if err != nil { return fmt.Errorf("error formatting the json output: %s", err) } fmt.Println(string(output)) return nil } // parseColumns parses the supplied output flag into a slice of column indices. func parseColumns(cols string) ([]int, error) { var i []int s := strings.Split(cols, ",") for _, v := range s { v = strings.TrimSpace(v) if len(v) == 0 { continue } col, err := stringToColumn(v) if err != nil { return nil, err } i = append(i, col) } return i, nil } // parseStyle converts user-provided style option into a table.Style. func parseStyle(styleOpt string) (table.Style, error) { switch styleOpt { case "unicode": return table.StyleRounded, nil case "ascii": return table.StyleDefault, nil default: return table.Style{}, fmt.Errorf("unknown style option: %s", styleOpt) } } // parseCommaSeparatedValues parses comma separated string into a map. func parseCommaSeparatedValues(values string) map[string]struct{} { m := make(map[string]struct{}) for _, v := range strings.Split(values, ",") { v = strings.TrimSpace(v) if len(v) == 0 { continue } v = strings.ToLower(v) m[v] = struct{}{} } return m } // validateGroups validates the parsed group maps. func validateGroups(m map[string]struct{}) error { for k := range m { found := false for _, g := range groups { if g == k { found = true break } } if !found { return fmt.Errorf("unknown device group: %s", k) } } return nil } // findInKey parse a slice of pattern to match the given key. func findInKey(str string, km map[string]struct{}) bool { for p := range km { if wildcard.Match(p, str) { return true } } return false } func printVersion() { info, ok := debug.ReadBuildInfo() var buildTime time.Time var modified bool if ok { if len(Version) == 0 { vs := strings.Split(info.Main.Version, "-") if len(vs) >= 1 { Version = vs[0] } } for _, setting := range info.Settings { switch setting.Key { case "vcs.revision": if len(CommitSHA) == 0 { CommitSHA = setting.Value if len(CommitSHA) > 12 { CommitSHA = CommitSHA[:12] } } case "vcs.time": buildTime, _ = time.Parse(time.RFC3339, setting.Value) case "vcs.modified": modified, _ = strconv.ParseBool(setting.Value) } } } if Version == "" || Version == "(devel)" { Version = "(built from source)" } fmt.Printf("duf %s", Version) if len(CommitSHA) > 0 { if modified { CommitSHA += "+modified" } fmt.Printf(" (%s)", CommitSHA) } if !buildTime.IsZero() { fmt.Printf(" (built on %s)", buildTime.Format("2006-01-02")) } fmt.Println() } func main() { // hide -h from help, it's just for df compatibility _ = flag.CommandLine.MarkHidden("human-readable") flag.Parse() if *version { printVersion() os.Exit(0) } // read mount table m, warnings, err := mounts() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } // print JSON if *jsonOutput { if err = renderJSON(m); err != nil { fmt.Fprintln(os.Stderr, err) } return } // validate theme theme, err = loadTheme(*themeOpt) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if env == termenv.ANSI { // enforce ANSI theme for limited color support theme, err = loadTheme("ansi") if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } // validate style style, err := parseStyle(*styleOpt) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } // validate output columns columns, err := parseColumns(*output) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if len(columns) == 0 { // no columns supplied, use defaults if *inodes { columns = []int{1, 6, 7, 8, 9, 10, 11} } else { columns = []int{1, 2, 3, 4, 5, 10, 11} } } // validate sort column sortCol, err := stringToSortIndex(*sortBy) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } // validate filters filters := FilterOptions{ HiddenDevices: parseCommaSeparatedValues(*hideDevices), OnlyDevices: parseCommaSeparatedValues(*onlyDevices), HiddenFilesystems: parseCommaSeparatedValues(*hideFs), OnlyFilesystems: parseCommaSeparatedValues(*onlyFs), HiddenMountPoints: parseCommaSeparatedValues(*hideMp), OnlyMountPoints: parseCommaSeparatedValues(*onlyMp), } err = validateGroups(filters.HiddenDevices) if err != nil { fmt.Println(err) os.Exit(1) } err = validateGroups(filters.OnlyDevices) if err != nil { fmt.Println(err) os.Exit(1) } // validate arguments if len(flag.Args()) > 0 { var mounts []Mount vis := map[string]struct{}{} for _, v := range flag.Args() { var fm []Mount fm, err = findMounts(m, v) if err != nil { fmt.Println(err) os.Exit(1) } // de-duplicate for _, v := range fm { if _, ok := vis[v.Mountpoint]; !ok { mounts = append(mounts, v) vis[v.Mountpoint] = struct{}{} } } } m = mounts } // validate availability thresholds availbilityThresholds := strings.Split(*availThreshold, ",") if len(availbilityThresholds) != 2 { fmt.Fprintln(os.Stderr, fmt.Errorf("error parsing avail-threshold: invalid option '%s'", *availThreshold)) os.Exit(1) } for _, threshold := range availbilityThresholds { _, err = stringToSize(threshold) if err != nil { fmt.Fprintln(os.Stderr, "error parsing avail-threshold:", err) os.Exit(1) } } // validate usage thresholds usageThresholds := strings.Split(*usageThreshold, ",") if len(usageThresholds) != 2 { fmt.Fprintln(os.Stderr, fmt.Errorf("error parsing usage-threshold: invalid option '%s'", *usageThreshold)) os.Exit(1) } for _, threshold := range usageThresholds { _, err = strconv.ParseFloat(threshold, 64) if err != nil { fmt.Fprintln(os.Stderr, "error parsing usage-threshold:", err) os.Exit(1) } } // print out warnings if *warns { for _, warning := range warnings { fmt.Fprintln(os.Stderr, warning) } } // detect terminal width isTerminal := term.IsTerminal(int(os.Stdout.Fd())) if isTerminal && *width == 0 { w, _, err := term.GetSize(int(os.Stdout.Fd())) if err == nil { *width = uint(w) } } if *width == 0 { *width = 80 } // print tables renderTables(m, filters, TableOptions{ Columns: columns, SortBy: sortCol, Style: style, StyleName: *styleOpt, }) } ================================================ FILE: man.go ================================================ //go:build mango // +build mango package main import ( "fmt" "os" "github.com/muesli/mango" mpflag "github.com/muesli/mango-pflag" "github.com/muesli/roff" flag "github.com/spf13/pflag" ) func init() { usage := `You can simply start duf without any command-line arguments: $ duf If you supply arguments, duf will only list specific devices & mount points: $ duf /home /some/file If you want to list everything (including pseudo, duplicate, inaccessible file systems): $ duf --all You can show and hide specific tables: $ duf --only local,network,fuse,special,loops,binds $ duf --hide local,network,fuse,special,loops,binds You can also show and hide specific filesystems: $ duf --only-fs tmpfs,vfat $ duf --hide-fs tmpfs,vfat ...or specific mount points: $ duf --only-mp /,/home,/dev $ duf --hide-mp /,/home,/dev Wildcards inside quotes work: $ duf --only-mp '/sys/*,/dev/*' Sort the output: $ duf --sort size Valid keys are: mountpoint, size, used, avail, usage, inodes, inodes_used, inodes_avail, inodes_usage, type, filesystem. Show or hide specific columns: $ duf --output mountpoint,size,usage Valid keys are: mountpoint, size, used, avail, usage, inodes, inodes_used, inodes_avail, inodes_usage, type, filesystem. List inode information instead of block usage: $ duf --inodes If duf doesn't detect your terminal's colors correctly, you can set a theme: $ duf --theme light duf highlights the availability & usage columns in red, green, or yellow, depending on how much space is still available. You can set your own thresholds: $ duf --avail-threshold="10G,1G" $ duf --usage-threshold="0.5,0.9" If you prefer your output as JSON: $ duf --json ` manPage := mango.NewManPage(1, "duf", "Disk Usage/Free Utility"). WithLongDescription("Simple Disk Usage/Free Utility.\n"+ "Features:\n"+ "* User-friendly, colorful output.\n"+ "* Adjusts to your terminal's theme & width.\n"+ "* Sort the results according to your needs.\n"+ "* Groups & filters devices.\n"+ "* Can conveniently output JSON."). WithSection("Usage", usage). WithSection("Notes", "Portions of duf's code are copied and modified from https://github.com/shirou/gopsutil.\n"+ "gopsutil was written by WAKAYAMA Shirou and is distributed under BSD-3-Clause."). WithSection("Authors", "duf was written by Christian Muehlhaeuser "). WithSection("Copyright", "Copyright (C) 2020-2022 Christian Muehlhaeuser \n"+ "Released under MIT license.") flag.VisitAll(mpflag.PFlagVisitor(manPage)) fmt.Println(manPage.Build(roff.NewDocument())) os.Exit(0) } ================================================ FILE: mounts.go ================================================ package main import ( "bufio" "os" "strconv" ) // Mount contains all metadata for a single filesystem mount. type Mount struct { Device string `json:"device"` DeviceType string `json:"device_type"` Mountpoint string `json:"mount_point"` Fstype string `json:"fs_type"` Type string `json:"type"` Opts string `json:"opts"` Total uint64 `json:"total"` Free uint64 `json:"free"` Used uint64 `json:"used"` Inodes uint64 `json:"inodes"` InodesFree uint64 `json:"inodes_free"` InodesUsed uint64 `json:"inodes_used"` Blocks uint64 `json:"blocks"` BlockSize uint64 `json:"block_size"` Metadata interface{} `json:"-"` } func readLines(filename string) ([]string, error) { file, err := os.Open(filename) if err != nil { return nil, err } defer file.Close() //nolint:errcheck // ignore error scanner := bufio.NewScanner(file) var s []string for scanner.Scan() { s = append(s, scanner.Text()) } return s, scanner.Err() } func unescapeFstab(path string) string { escaped, err := strconv.Unquote(`"` + path + `"`) if err != nil { return path } return escaped } //nolint:unused // used on BSD func byteToString(orig []byte) string { n := -1 l := -1 for i, b := range orig { // skip left side null if l == -1 && b == 0 { continue } if l == -1 { l = i } if b == 0 { break } n = i + 1 } if n == -1 { return string(orig) } return string(orig[l:n]) } //nolint:unused // used on OpenBSD func intToString(orig []int8) string { ret := make([]byte, len(orig)) size := -1 for i, o := range orig { if o == 0 { size = i break } ret[i] = byte(o) } if size == -1 { size = len(orig) } return string(ret[0:size]) } ================================================ FILE: mounts_darwin.go ================================================ //go:build darwin // +build darwin package main import ( "golang.org/x/sys/unix" ) func (m *Mount) Stat() unix.Statfs_t { return m.Metadata.(unix.Statfs_t) } func mounts() ([]Mount, []string, error) { var ret []Mount var warnings []string count, err := unix.Getfsstat(nil, unix.MNT_WAIT) if err != nil { return nil, nil, err } fs := make([]unix.Statfs_t, count) if _, err = unix.Getfsstat(fs, unix.MNT_WAIT); err != nil { return nil, nil, err } for _, stat := range fs { opts := "rw" if stat.Flags&unix.MNT_RDONLY != 0 { opts = "ro" } if stat.Flags&unix.MNT_SYNCHRONOUS != 0 { opts += ",sync" } if stat.Flags&unix.MNT_NOEXEC != 0 { opts += ",noexec" } if stat.Flags&unix.MNT_NOSUID != 0 { opts += ",nosuid" } if stat.Flags&unix.MNT_UNION != 0 { opts += ",union" } if stat.Flags&unix.MNT_ASYNC != 0 { opts += ",async" } if stat.Flags&unix.MNT_DONTBROWSE != 0 { opts += ",nobrowse" } if stat.Flags&unix.MNT_AUTOMOUNTED != 0 { opts += ",automounted" } if stat.Flags&unix.MNT_JOURNALED != 0 { opts += ",journaled" } if stat.Flags&unix.MNT_MULTILABEL != 0 { opts += ",multilabel" } if stat.Flags&unix.MNT_NOATIME != 0 { opts += ",noatime" } if stat.Flags&unix.MNT_NODEV != 0 { opts += ",nodev" } device := byteToString(stat.Mntfromname[:]) mountPoint := byteToString(stat.Mntonname[:]) fsType := byteToString(stat.Fstypename[:]) if len(device) == 0 { continue } d := Mount{ Device: device, Mountpoint: mountPoint, Fstype: fsType, Type: fsType, Opts: opts, Metadata: stat, Total: stat.Blocks * uint64(stat.Bsize), Free: stat.Bavail * uint64(stat.Bsize), Used: (stat.Blocks - stat.Bfree) * uint64(stat.Bsize), Inodes: stat.Files, InodesFree: stat.Ffree, InodesUsed: stat.Files - stat.Ffree, Blocks: stat.Blocks, BlockSize: uint64(stat.Bsize), } d.DeviceType = deviceType(d) ret = append(ret, d) } return ret, warnings, nil } ================================================ FILE: mounts_freebsd.go ================================================ //go:build freebsd // +build freebsd package main import ( "golang.org/x/sys/unix" ) func (m *Mount) Stat() unix.Statfs_t { return m.Metadata.(unix.Statfs_t) } func mounts() ([]Mount, []string, error) { var ret []Mount var warnings []string count, err := unix.Getfsstat(nil, unix.MNT_WAIT) if err != nil { return nil, nil, err } fs := make([]unix.Statfs_t, count) if _, err = unix.Getfsstat(fs, unix.MNT_WAIT); err != nil { return nil, nil, err } for _, stat := range fs { opts := "rw" if stat.Flags&unix.MNT_RDONLY != 0 { opts = "ro" } if stat.Flags&unix.MNT_SYNCHRONOUS != 0 { opts += ",sync" } if stat.Flags&unix.MNT_NOEXEC != 0 { opts += ",noexec" } if stat.Flags&unix.MNT_NOSUID != 0 { opts += ",nosuid" } if stat.Flags&unix.MNT_UNION != 0 { opts += ",union" } if stat.Flags&unix.MNT_ASYNC != 0 { opts += ",async" } if stat.Flags&unix.MNT_SUIDDIR != 0 { opts += ",suiddir" } if stat.Flags&unix.MNT_SOFTDEP != 0 { opts += ",softdep" } if stat.Flags&unix.MNT_NOSYMFOLLOW != 0 { opts += ",nosymfollow" } if stat.Flags&unix.MNT_GJOURNAL != 0 { opts += ",gjournal" } if stat.Flags&unix.MNT_MULTILABEL != 0 { opts += ",multilabel" } if stat.Flags&unix.MNT_ACLS != 0 { opts += ",acls" } if stat.Flags&unix.MNT_NOATIME != 0 { opts += ",noatime" } if stat.Flags&unix.MNT_NOCLUSTERR != 0 { opts += ",noclusterr" } if stat.Flags&unix.MNT_NOCLUSTERW != 0 { opts += ",noclusterw" } if stat.Flags&unix.MNT_NFS4ACLS != 0 { opts += ",nfsv4acls" } device := byteToString(stat.Mntfromname[:]) mountPoint := byteToString(stat.Mntonname[:]) fsType := byteToString(stat.Fstypename[:]) if len(device) == 0 { continue } d := Mount{ Device: device, Mountpoint: mountPoint, Fstype: fsType, Type: fsType, Opts: opts, Metadata: stat, Total: (uint64(stat.Blocks) * uint64(stat.Bsize)), Free: (uint64(stat.Bavail) * uint64(stat.Bsize)), Used: (uint64(stat.Blocks) - uint64(stat.Bfree)) * uint64(stat.Bsize), Inodes: stat.Files, InodesFree: uint64(stat.Ffree), InodesUsed: stat.Files - uint64(stat.Ffree), Blocks: uint64(stat.Blocks), BlockSize: uint64(stat.Bsize), } d.DeviceType = deviceType(d) ret = append(ret, d) } return ret, warnings, nil } ================================================ FILE: mounts_linux.go ================================================ //go:build linux // +build linux package main import ( "fmt" "os" "path/filepath" "regexp" "strconv" "strings" "golang.org/x/sys/unix" ) const ( // A line of self/mountinfo has the following structure: // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue // (0) (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) // // (0) mount ID: unique identifier of the mount (may be reused after umount). //mountinfoMountID = 0 // (1) parent ID: ID of parent (or of self for the top of the mount tree). //mountinfoParentID = 1 // (2) major:minor: value of st_dev for files on filesystem. //mountinfoMajorMinor = 2 // (3) root: root of the mount within the filesystem. //mountinfoRoot = 3 // (4) mount point: mount point relative to the process's root. mountinfoMountPoint = 4 // (5) mount options: per mount options. mountinfoMountOpts = 5 // (6) optional fields: zero or more fields terminated by "-". mountinfoOptionalFields = 6 // (7) separator between optional fields. //mountinfoSeparator = 7 // (8) filesystem type: name of filesystem of the form. mountinfoFsType = 8 // (9) mount source: filesystem specific information or "none". mountinfoMountSource = 9 // (10) super options: per super block options. mountinfoSuperOptions = 10 ) // Stat returns the mountpoint's stat information. func (m *Mount) Stat() unix.Statfs_t { return m.Metadata.(unix.Statfs_t) } func mounts() ([]Mount, []string, error) { var warnings []string filename := "/proc/self/mountinfo" lines, err := readLines(filename) if err != nil { // wrapcheck: add context to the error. return nil, nil, fmt.Errorf("reading mountinfo %q: %w", filename, err) } ret := make([]Mount, 0, len(lines)) for _, line := range lines { nb, fields := parseMountInfoLine(line) if nb == 0 { continue } // if the number of fields does not match the structure of mountinfo, // emit a warning and ignore the line. if nb < 10 || nb > 11 { warnings = append(warnings, fmt.Sprintf("found invalid mountinfo line: %s", line)) continue } // blockDeviceID := fields[mountinfoMountID] mountPoint := fields[mountinfoMountPoint] mountOpts := fields[mountinfoMountOpts] fstype := fields[mountinfoFsType] device := fields[mountinfoMountSource] var stat unix.Statfs_t err := unix.Statfs(mountPoint, &stat) if err != nil { if err != os.ErrPermission { warnings = append(warnings, fmt.Sprintf("%s: %s", mountPoint, err)) continue } stat = unix.Statfs_t{} } d := Mount{ Device: device, Mountpoint: mountPoint, Fstype: fstype, Type: fsTypeMap[int64(stat.Type)], //nolint:unconvert Opts: mountOpts, Metadata: stat, Total: (uint64(stat.Blocks) * uint64(stat.Bsize)), //nolint:unconvert Free: (uint64(stat.Bavail) * uint64(stat.Bsize)), //nolint:unconvert Used: (uint64(stat.Blocks) - uint64(stat.Bfree)) * uint64(stat.Bsize), //nolint:unconvert Inodes: stat.Files, InodesFree: stat.Ffree, InodesUsed: stat.Files - stat.Ffree, Blocks: uint64(stat.Blocks), //nolint:unconvert BlockSize: uint64(stat.Bsize), } d.DeviceType = deviceType(d) // Resolve /dev/mapper/* device names. if strings.HasPrefix(d.Device, "/dev/mapper/") { re := regexp.MustCompile(`^/dev/mapper/(.*)-(.*)`) match := re.FindAllStringSubmatch(d.Device, -1) if len(match) > 0 && len(match[0]) == 3 { d.Device = filepath.Join("/dev", match[0][1], match[0][2]) } } ret = append(ret, d) } return ret, warnings, nil } // splitMountInfoFields splits a mountinfo line into its fields. // It treats spaces and tabs as field separators and decodes certain octal escapes. func splitMountInfoFields(line string) []string { var fields []string var buf strings.Builder for i := 0; i < len(line); i++ { c := line[i] // Treat both space and tab as separators if c == ' ' || c == '\t' { if buf.Len() > 0 { fields = append(fields, buf.String()) buf.Reset() } continue } if c == '\\' && i+3 < len(line) { oct := line[i+1 : i+4] if v, err := strconv.ParseInt(oct, 8, 0); err == nil { switch byte(v) { case ' ', '\t', '\n': buf.WriteByte(byte(v)) i += 3 continue default: // keep unknown escapes as-is buf.WriteString("\\" + oct) i += 3 continue } } } buf.WriteByte(c) } if buf.Len() > 0 { fields = append(fields, buf.String()) } return fields } // parseMountInfoLine parses a line of /proc/self/mountinfo and returns the // amount of parsed fields and their values. func parseMountInfoLine(line string) (int, [11]string) { var fields [11]string if len(line) == 0 || line[0] == '#' { // ignore comments and empty lines return 0, fields } all := splitMountInfoFields(line) var i int sawSep := false sawSup := false for _, f := range all { if i >= len(fields) { break } if i == mountinfoOptionalFields { // (6) optional fields: zero or more fields of the form "tag[:value]"; see below. // (7) separator: the end of the optional fields is marked by a single hyphen. if f != "-" { // Join tokens with spaces for mountinfoOptionalFields. fields[i] = strings.TrimSpace(fields[i] + " " + f) continue } // Found separator. sawSep = true i++ fields[i] = f i++ continue } if i == mountinfoSuperOptions { // join tokens with spaces for WSL2 path=... they are splitted around spaces. fields[i] = strings.TrimSpace(fields[i] + " " + f) sawSup = true continue } // Default case: copy with unescape for certain fields switch i { case mountinfoMountPoint, mountinfoMountSource, mountinfoFsType: fields[i] = unescapeFstab(f) default: fields[i] = f } i++ } // Handle malformed line (no "-" found). if !sawSep && len(all) > mountinfoOptionalFields { i = mountinfoOptionalFields } // When super options are present, the index is one less than 11. if sawSup { i++ } // clear trailing empties. for j := i + 1; j < len(fields); j++ { fields[j] = "" } return i, fields } ================================================ FILE: mounts_linux_test.go ================================================ //go:build linux // +build linux package main import ( "reflect" "testing" ) func TestGetFields(t *testing.T) { var tt = []struct { input string number int expected [11]string }{ // Empty lines { input: "", number: 0, }, { input: " ", number: 0, }, { input: " ", number: 0, }, { input: " ", number: 0, }, // Comments { input: "#", number: 0, }, { input: "# ", number: 0, }, { input: "# ", number: 0, }, { input: "# I'm a lazy dog", number: 0, }, // Bad fields { input: "1 2", number: 2, expected: [11]string{"1", "2"}, }, { input: "1 2", number: 2, expected: [11]string{"1", "2"}, }, { input: "1 2 3", number: 3, expected: [11]string{"1", "2", "3"}, }, { input: "1 2 3 4", number: 4, expected: [11]string{"1", "2", "3", "4"}, }, // No optional separator or no options { input: "1 2 3 4 5 6 7 NotASeparator 9 10 11", number: 6, expected: [11]string{"1", "2", "3", "4", "5", "6", "7 NotASeparator 9 10 11"}, }, { input: "1 2 3 4 5 6 7 8 9 10 11", number: 6, expected: [11]string{"1", "2", "3", "4", "5", "6", "7 8 9 10 11"}, }, { input: "1 2 3 4 5 6 - 9 10 11", number: 11, expected: [11]string{"1", "2", "3", "4", "5", "6", "", "-", "9", "10", "11"}, }, // Normal mount table line { input: "22 27 0:21 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw", number: 11, expected: [11]string{"22", "27", "0:21", "/", "/proc", "rw,nosuid,nodev,noexec,relatime", "shared:5", "-", "proc", "proc", "rw"}, }, { input: "31 23 0:27 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:9 - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot", number: 11, expected: [11]string{"31", "23", "0:27", "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", "shared:9", "-", "cgroup2", "cgroup2", "rw,nsdelegate,memory_recursiveprot"}, }, { input: "40 27 0:33 / /tmp rw,nosuid,nodev shared:18 - tmpfs tmpfs", number: 10, expected: [11]string{"40", "27", "0:33", "/", "/tmp", "rw,nosuid,nodev", "shared:18", "-", "tmpfs", "tmpfs"}, }, { input: "40 27 0:33 / /tmp rw,nosuid,nodev shared:18 shared:22 - tmpfs tmpfs", number: 10, expected: [11]string{"40", "27", "0:33", "/", "/tmp", "rw,nosuid,nodev", "shared:18 shared:22", "-", "tmpfs", "tmpfs"}, }, { input: "50 27 0:33 / /tmp rw,nosuid,nodev - tmpfs tmpfs", number: 10, expected: [11]string{"50", "27", "0:33", "/", "/tmp", "rw,nosuid,nodev", "", "-", "tmpfs", "tmpfs"}, }, // Exceptional mount table lines { input: "328 27 0:73 / /mnt/a rw,relatime shared:206 - tmpfs - rw,inode64", number: 11, expected: [11]string{"328", "27", "0:73", "/", "/mnt/a", "rw,relatime", "shared:206", "-", "tmpfs", "-", "rw,inode64"}, }, { input: "330 27 0:73 / /mnt/a rw,relatime shared:206 - tmpfs 👾 rw,inode64", number: 11, expected: [11]string{"330", "27", "0:73", "/", "/mnt/a", "rw,relatime", "shared:206", "-", "tmpfs", "👾", "rw,inode64"}, }, { input: "335 27 0:73 / /mnt/👾 rw,relatime shared:206 - tmpfs 👾 rw,inode64", number: 11, expected: [11]string{"335", "27", "0:73", "/", "/mnt/👾", "rw,relatime", "shared:206", "-", "tmpfs", "👾", "rw,inode64"}, }, { input: "509 27 0:78 / /mnt/- rw,relatime shared:223 - tmpfs 👾 rw,inode64", number: 11, expected: [11]string{"509", "27", "0:78", "/", "/mnt/-", "rw,relatime", "shared:223", "-", "tmpfs", "👾", "rw,inode64"}, }, { input: "362 27 0:76 / /mnt/a\\040b rw,relatime shared:215 - tmpfs 👾 rw,inode64", number: 11, expected: [11]string{"362", "27", "0:76", "/", "/mnt/a b", "rw,relatime", "shared:215", "-", "tmpfs", "👾", "rw,inode64"}, }, { input: "1 2 3:3 / /mnt/\\011 rw shared:7 - tmpfs - rw,inode64", number: 11, expected: [11]string{"1", "2", "3:3", "/", "/mnt/\t", "rw", "shared:7", "-", "tmpfs", "-", "rw,inode64"}, }, { input: "11 2 3:3 / /mnt/a\\012b rw shared:7 - tmpfs - rw,inode64", number: 11, expected: [11]string{"11", "2", "3:3", "/", "/mnt/a\nb", "rw", "shared:7", "-", "tmpfs", "-", "rw,inode64"}, }, { input: "111 2 3:3 / /mnt/a\\134b rw shared:7 - tmpfs - rw,inode64", number: 11, expected: [11]string{"111", "2", "3:3", "/", "/mnt/a\\b", "rw", "shared:7", "-", "tmpfs", "-", "rw,inode64"}, }, { input: "1111 2 3:3 / /mnt/a\\042b rw shared:7 - tmpfs - rw,inode64", number: 11, expected: [11]string{"1111", "2", "3:3", "/", "/mnt/a\"b", "rw", "shared:7", "-", "tmpfs", "-", "rw,inode64"}, }, // WSL2 9p mount table line. { input: `380 383 0:33 / /usr/lib/wsl/drivers ro,nosuid,nodev,noatime - 9p drivers ro,dirsync,aname=drivers;fmask=222;dmask=222,mmap,access=client,msize=65536,trans=fd,rfd=8,wfd=8`, number: 11, expected: [11]string{"380", "383", "0:33", "/", "/usr/lib/wsl/drivers", "ro,nosuid,nodev,noatime", "", "-", "9p", "drivers", "ro,dirsync,aname=drivers;fmask=222;dmask=222,mmap,access=client,msize=65536,trans=fd,rfd=8,wfd=8"}, }, { input: `488 383 0:128 / /mnt/c rw,noatime - 9p C:\134 rw,dirsync,aname=drvfs;path=C:\;uid=1000;gid=1000;symlinkroot=/mnt/,mmap,access=client,msize=65536,trans=fd,rfd=5,wfd=5`, number: 11, expected: [11]string{"488", "383", "0:128", "/", "/mnt/c", "rw,noatime", "", "-", "9p", "C:\\", "rw,dirsync,aname=drvfs;path=C:\\;uid=1000;gid=1000;symlinkroot=/mnt/,mmap,access=client,msize=65536,trans=fd,rfd=5,wfd=5"}, }, { input: `516 78 0:136 / /Docker/host rw,noatime - 9p C:\134Program\040Files\134Docker\134Docker\134resources rw,dirsync,aname=drvfs;path=C:\Program Files\Docker\Docker\resources;symlinkroot=/mnt/,mmap,access=client,msize=65536,trans=fd,rfd=3,wfd=3`, number: 11, expected: [11]string{"516", "78", "0:136", "/", "/Docker/host", "rw,noatime", "", "-", "9p", "C:\\Program Files\\Docker\\Docker\\resources", "rw,dirsync,aname=drvfs;path=C:\\Program Files\\Docker\\Docker\\resources;symlinkroot=/mnt/,mmap,access=client,msize=65536,trans=fd,rfd=3,wfd=3"}, }, } for _, tc := range tt { nb, actual := parseMountInfoLine(tc.input) if nb != tc.number || !reflect.DeepEqual(actual, tc.expected) { t.Errorf("\nparseMountInfoLine(%q) == \n(%d) %q, \nexpected (%d) %q", tc.input, nb, actual, tc.number, tc.expected) } } } ================================================ FILE: mounts_openbsd.go ================================================ //go:build openbsd // +build openbsd package main import ( "golang.org/x/sys/unix" ) func (m *Mount) Stat() unix.Statfs_t { return m.Metadata.(unix.Statfs_t) } func mounts() ([]Mount, []string, error) { var ret []Mount var warnings []string count, err := unix.Getfsstat(nil, unix.MNT_WAIT) if err != nil { return nil, nil, err } fs := make([]unix.Statfs_t, count) if _, err = unix.Getfsstat(fs, unix.MNT_WAIT); err != nil { return nil, nil, err } for _, stat := range fs { opts := "rw" if stat.F_flags&unix.MNT_RDONLY != 0 { opts = "ro" } if stat.F_flags&unix.MNT_SYNCHRONOUS != 0 { opts += ",sync" } if stat.F_flags&unix.MNT_NOEXEC != 0 { opts += ",noexec" } if stat.F_flags&unix.MNT_NOSUID != 0 { opts += ",nosuid" } if stat.F_flags&unix.MNT_NODEV != 0 { opts += ",nodev" } if stat.F_flags&unix.MNT_ASYNC != 0 { opts += ",async" } if stat.F_flags&unix.MNT_SOFTDEP != 0 { opts += ",softdep" } if stat.F_flags&unix.MNT_NOATIME != 0 { opts += ",noatime" } if stat.F_flags&unix.MNT_WXALLOWED != 0 { opts += ",wxallowed" } device := byteToString(stat.F_mntfromname[:]) mountPoint := byteToString(stat.F_mntonname[:]) fsType := byteToString(stat.F_fstypename[:]) if len(device) == 0 { continue } d := Mount{ Device: device, Mountpoint: mountPoint, Fstype: fsType, Type: fsType, Opts: opts, Metadata: stat, Total: (uint64(stat.F_blocks) * uint64(stat.F_bsize)), Free: (uint64(stat.F_bavail) * uint64(stat.F_bsize)), Used: (uint64(stat.F_blocks) - uint64(stat.F_bfree)) * uint64(stat.F_bsize), Inodes: stat.F_files, InodesFree: uint64(stat.F_ffree), InodesUsed: stat.F_files - uint64(stat.F_ffree), Blocks: uint64(stat.F_blocks), BlockSize: uint64(stat.F_bsize), } d.DeviceType = deviceType(d) ret = append(ret, d) } return ret, warnings, nil } ================================================ FILE: mounts_windows.go ================================================ //go:build windows // +build windows package main import ( "fmt" "golang.org/x/sys/windows" "math" "path/filepath" "strings" "syscall" "unsafe" ) // Local devices const ( guidBufLen = windows.MAX_PATH + 1 volumeNameBufLen = windows.MAX_PATH + 1 rootPathBufLen = windows.MAX_PATH + 1 fileSystemBufLen = windows.MAX_PATH + 1 ) func getMountPoint(guidBuf []uint16) (mountPoint string, err error) { var rootPathLen uint32 rootPathBuf := make([]uint16, rootPathBufLen) err = windows.GetVolumePathNamesForVolumeName(&guidBuf[0], &rootPathBuf[0], rootPathBufLen*2, &rootPathLen) if err != nil && err.(windows.Errno) == windows.ERROR_MORE_DATA { // Retry if buffer size is too small rootPathBuf = make([]uint16, (rootPathLen+1)/2) err = windows.GetVolumePathNamesForVolumeName( &guidBuf[0], &rootPathBuf[0], rootPathLen, &rootPathLen) } return windows.UTF16ToString(rootPathBuf), err } func getVolumeInfo(guidOrMountPointBuf []uint16) (volumeName string, fsType string, err error) { volumeNameBuf := make([]uint16, volumeNameBufLen) fsTypeBuf := make([]uint16, fileSystemBufLen) err = windows.GetVolumeInformation(&guidOrMountPointBuf[0], &volumeNameBuf[0], volumeNameBufLen*2, nil, nil, nil, &fsTypeBuf[0], fileSystemBufLen*2) return windows.UTF16ToString(volumeNameBuf), windows.UTF16ToString(fsTypeBuf), err } func getSpaceInfo(guidOrMountPointBuf []uint16) (totalBytes uint64, freeBytes uint64, err error) { err = windows.GetDiskFreeSpaceEx(&guidOrMountPointBuf[0], nil, &totalBytes, &freeBytes) return } func getClusterInfo(guidOrMountPointBuf []uint16) (totalClusters uint32, clusterSize uint32, err error) { var sectorsPerCluster uint32 var bytesPerSector uint32 err = GetDiskFreeSpace(&guidOrMountPointBuf[0], §orsPerCluster, &bytesPerSector, nil, &totalClusters) clusterSize = bytesPerSector * sectorsPerCluster return } func getMount(guidOrMountPointBuf []uint16, isGUID bool) (m Mount, skip bool, warnings []string) { var err error guidOrMountPoint := windows.UTF16ToString(guidOrMountPointBuf) mountPoint := guidOrMountPoint if isGUID { mountPoint, err = getMountPoint(guidOrMountPointBuf) if err != nil { warnings = append(warnings, fmt.Sprintf("%s: %s", guidOrMountPoint, err)) } // Skip unmounted volumes if len(mountPoint) == 0 { skip = true return } } // Get volume name & filesystem type volumeName, fsType, err := getVolumeInfo(guidOrMountPointBuf) if err != nil { warnings = append(warnings, fmt.Sprintf("%s: %s", guidOrMountPoint, err)) } // Get space info totalBytes, freeBytes, err := getSpaceInfo(guidOrMountPointBuf) if err != nil { warnings = append(warnings, fmt.Sprintf("%s: %s", guidOrMountPoint, err)) } // Get cluster info totalClusters, clusterSize, err := getClusterInfo(guidOrMountPointBuf) if err != nil { warnings = append(warnings, fmt.Sprintf("%s: %s", guidOrMountPoint, err)) } m = Mount{ Device: volumeName, Mountpoint: mountPoint, Fstype: fsType, Type: fsType, Opts: "", Total: totalBytes, Free: freeBytes, Used: totalBytes - freeBytes, Blocks: uint64(totalClusters), BlockSize: uint64(clusterSize), } m.DeviceType = deviceType(m) return } func getMountFromGUID(guidBuf []uint16) (m Mount, skip bool, warnings []string) { m, skip, warnings = getMount(guidBuf, true) // Use GUID as volume name if no label was set if len(m.Device) == 0 { m.Device = windows.UTF16ToString(guidBuf) } return } func getMountFromMountPoint(mountPointBuf []uint16) (m Mount, warnings []string) { m, _, warnings = getMount(mountPointBuf, false) // Use mount point as volume name if no label was set if len(m.Device) == 0 { m.Device = windows.UTF16ToString(mountPointBuf) } return m, warnings } func appendLocalMounts(mounts []Mount, warnings []string) ([]Mount, []string, error) { guidBuf := make([]uint16, guidBufLen) hFindVolume, err := windows.FindFirstVolume(&guidBuf[0], guidBufLen*2) if err != nil { return mounts, warnings, err } VolumeLoop: for ; ; err = windows.FindNextVolume(hFindVolume, &guidBuf[0], guidBufLen*2) { if err != nil { switch err.(windows.Errno) { case windows.ERROR_NO_MORE_FILES: break VolumeLoop default: warnings = append(warnings, fmt.Sprintf("%s: %s", windows.UTF16ToString(guidBuf), err)) continue VolumeLoop } } if m, skip, w := getMountFromGUID(guidBuf); !skip { mounts = append(mounts, m) warnings = append(warnings, w...) } } if err = windows.FindVolumeClose(hFindVolume); err != nil { warnings = append(warnings, fmt.Sprintf("%s", err)) } return mounts, warnings, nil } // Network devices func getMountFromNetResource(netResource NetResource) (m Mount, warnings []string) { mountPoint := windows.UTF16PtrToString(netResource.LocalName) if !strings.HasSuffix(mountPoint, string(filepath.Separator)) { mountPoint += string(filepath.Separator) } mountPointBuf := windows.StringToUTF16(mountPoint) m, _, warnings = getMount(mountPointBuf, false) // Use remote name as volume name if no label was set if len(m.Device) == 0 { m.Device = windows.UTF16PtrToString(netResource.RemoteName) } return } func appendNetworkMounts(mounts []Mount, warnings []string) ([]Mount, []string, error) { hEnumResource, err := WNetOpenEnum(RESOURCE_CONNECTED, RESOURCETYPE_DISK, RESOURCEUSAGE_CONNECTABLE, nil) if err != nil { return mounts, warnings, err } EnumLoop: for { // Reference: https://docs.microsoft.com/en-us/windows/win32/wnet/enumerating-network-resources var nrBuf [16384]byte count := uint32(math.MaxUint32) size := uint32(len(nrBuf)) if err := WNetEnumResource(hEnumResource, &count, &nrBuf[0], &size); err != nil { switch err.(windows.Errno) { case windows.ERROR_NO_MORE_ITEMS: break EnumLoop default: warnings = append(warnings, err.Error()) break EnumLoop } } for i := uint32(0); i < count; i++ { nr := (*NetResource)(unsafe.Pointer(&nrBuf[uintptr(i)*NetResourceSize])) m, w := getMountFromNetResource(*nr) mounts = append(mounts, m) warnings = append(warnings, w...) } } if err = WNetCloseEnum(hEnumResource); err != nil { warnings = append(warnings, fmt.Sprintf("%s", err)) } return mounts, warnings, nil } func mountPointAlreadyPresent(mounts []Mount, mountPoint string) bool { for _, m := range mounts { if m.Mountpoint == mountPoint { return true } } return false } func appendLogicalDrives(mounts []Mount, warnings []string) ([]Mount, []string) { driveBitmap, err := windows.GetLogicalDrives() if err != nil { warnings = append(warnings, fmt.Sprintf("GetLogicalDrives(): %s", err)) return mounts, warnings } for drive := 'A'; drive <= 'Z'; drive, driveBitmap = drive+1, driveBitmap>>1 { if driveBitmap&0x1 == 0 { continue } mountPoint := fmt.Sprintf("%c:\\", drive) if mountPointAlreadyPresent(mounts, mountPoint) { continue } mountPointBuf := windows.StringToUTF16(mountPoint) m, w := getMountFromMountPoint(mountPointBuf) mounts = append(mounts, m) warnings = append(warnings, w...) } return mounts, warnings } func mounts() (ret []Mount, warnings []string, err error) { ret = make([]Mount, 0) // Local devices if ret, warnings, err = appendLocalMounts(ret, warnings); err != nil { return } // Network devices if ret, warnings, err = appendNetworkMounts(ret, warnings); err != nil { return } // Logical devices (from GetLogicalDrives bitflag) // Check any possible logical drives, in case of some special virtual devices, such as RAM disk ret, warnings = appendLogicalDrives(ret, warnings) return ret, warnings, nil } // Windows API const ( // Windows Networking const // Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/nf-winnetwk-wnetopenenumw RESOURCE_CONNECTED = 0x00000001 RESOURCE_GLOBALNET = 0x00000002 RESOURCE_REMEMBERED = 0x00000003 RESOURCE_RECENT = 0x00000004 RESOURCE_CONTEXT = 0x00000005 RESOURCETYPE_ANY = 0x00000000 RESOURCETYPE_DISK = 0x00000001 RESOURCETYPE_PRINT = 0x00000002 RESOURCETYPE_RESERVED = 0x00000008 RESOURCETYPE_UNKNOWN = 0xFFFFFFFF RESOURCEUSAGE_CONNECTABLE = 0x00000001 RESOURCEUSAGE_CONTAINER = 0x00000002 RESOURCEUSAGE_NOLOCALDEVICE = 0x00000004 RESOURCEUSAGE_SIBLING = 0x00000008 RESOURCEUSAGE_ATTACHED = 0x00000010 RESOURCEUSAGE_ALL = RESOURCEUSAGE_CONNECTABLE | RESOURCEUSAGE_CONTAINER | RESOURCEUSAGE_ATTACHED RESOURCEUSAGE_RESERVED = 0x80000000 ) var ( // Windows syscall modmpr = windows.NewLazySystemDLL("mpr.dll") modkernel32 = windows.NewLazySystemDLL("kernel32.dll") procWNetOpenEnumW = modmpr.NewProc("WNetOpenEnumW") procWNetCloseEnum = modmpr.NewProc("WNetCloseEnum") procWNetEnumResourceW = modmpr.NewProc("WNetEnumResourceW") procGetDiskFreeSpaceW = modkernel32.NewProc("GetDiskFreeSpaceW") NetResourceSize = unsafe.Sizeof(NetResource{}) ) // Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/ns-winnetwk-netresourcew type NetResource struct { Scope uint32 Type uint32 DisplayType uint32 Usage uint32 LocalName *uint16 RemoteName *uint16 Comment *uint16 Provider *uint16 } // Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/nf-winnetwk-wnetopenenumw func WNetOpenEnum(scope uint32, resourceType uint32, usage uint32, resource *NetResource) (handle windows.Handle, err error) { r1, _, e1 := syscall.Syscall6(procWNetOpenEnumW.Addr(), 5, uintptr(scope), uintptr(resourceType), uintptr(usage), uintptr(unsafe.Pointer(resource)), uintptr(unsafe.Pointer(&handle)), 0) if r1 != windows.NO_ERROR { if e1 != 0 { err = e1 } else { err = syscall.EINVAL } } return } // Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/nf-winnetwk-wnetenumresourcew func WNetEnumResource(enumResource windows.Handle, count *uint32, buffer *byte, bufferSize *uint32) (err error) { r1, _, e1 := syscall.Syscall6(procWNetEnumResourceW.Addr(), 4, uintptr(enumResource), uintptr(unsafe.Pointer(count)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(bufferSize)), 0, 0) if r1 != windows.NO_ERROR { if e1 != 0 { err = e1 } else { err = syscall.EINVAL } } return } // Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/nf-winnetwk-wnetcloseenum func WNetCloseEnum(enumResource windows.Handle) (err error) { r1, _, e1 := syscall.Syscall(procWNetCloseEnum.Addr(), 1, uintptr(enumResource), 0, 0) if r1 != windows.NO_ERROR { if e1 != 0 { err = e1 } else { err = syscall.EINVAL } } return } // Reference: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdiskfreespacew func GetDiskFreeSpace(directoryName *uint16, sectorsPerCluster *uint32, bytesPerSector *uint32, numberOfFreeClusters *uint32, totalNumberOfClusters *uint32) (err error) { r1, _, e1 := syscall.Syscall6(procGetDiskFreeSpaceW.Addr(), 5, uintptr(unsafe.Pointer(directoryName)), uintptr(unsafe.Pointer(sectorsPerCluster)), uintptr(unsafe.Pointer(bytesPerSector)), uintptr(unsafe.Pointer(numberOfFreeClusters)), uintptr(unsafe.Pointer(totalNumberOfClusters)), 0) if r1 == 0 { if e1 != 0 { err = e1 } else { err = syscall.EINVAL } } return } ================================================ FILE: style.go ================================================ package main import "github.com/mattn/go-runewidth" func defaultStyleName() string { /* Due to a bug in github.com/mattn/go-runewidth v0.0.9, the width of unicode rune(such as '╭') could not be correctly calculated. Degrade to ascii to prevent broken table structure. Remove this once the bug is fixed. */ if runewidth.RuneWidth('╭') > 1 { return "ascii" } return "unicode" } ================================================ FILE: table.go ================================================ package main import ( "fmt" "os" "regexp" "strconv" "strings" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" "github.com/mattn/go-runewidth" "github.com/muesli/termenv" ) // TableOptions contains all options for the table. type TableOptions struct { Columns []int SortBy int Style table.Style StyleName string } // Column defines a column. type Column struct { ID string Name string SortIndex int Width int } // "Mounted on", "Size", "Used", "Avail", "Use%", "Inodes", "IUsed", "IAvail", "IUse%", "Type", "Filesystem" // mountpoint, size, used, avail, usage, inodes, inodes_used, inodes_avail, inodes_usage, type, filesystem var columns = []Column{ {ID: "mountpoint", Name: "Mounted on", SortIndex: 1}, {ID: "size", Name: "Size", SortIndex: 12, Width: 7}, {ID: "used", Name: "Used", SortIndex: 13, Width: 7}, {ID: "avail", Name: "Avail", SortIndex: 14, Width: 7}, {ID: "usage", Name: "Use%", SortIndex: 15, Width: 6}, {ID: "inodes", Name: "Inodes", SortIndex: 16, Width: 7}, {ID: "inodes_used", Name: "IUsed", SortIndex: 17, Width: 7}, {ID: "inodes_avail", Name: "IAvail", SortIndex: 18, Width: 7}, {ID: "inodes_usage", Name: "IUse%", SortIndex: 19, Width: 6}, {ID: "type", Name: "Type", SortIndex: 10}, {ID: "filesystem", Name: "Filesystem", SortIndex: 11}, } // initializeTable sets up the table writer with initial configurations. func initializeTable(tab table.Writer, opts TableOptions) { tab.SetAllowedRowLength(int(*width)) tab.SetOutputMirror(os.Stdout) tab.Style().Options.SeparateColumns = true tab.SetStyle(opts.Style) } // appendHeaders adds the header row to the table. func appendHeaders(tab table.Writer) { headers := table.Row{} for _, v := range columns { headers = append(headers, v.Name) } tab.AppendHeader(headers) } // appendRows adds data rows to the table for each mount. func appendRows(tab table.Writer, m []Mount) { for _, v := range m { var usage, inodeUsage float64 if v.Total > 0 { usage = float64(v.Used) / float64(v.Total) if usage > 1.0 { usage = 1.0 } } if v.Inodes > 0 { inodeUsage = float64(v.InodesUsed) / float64(v.Inodes) if inodeUsage > 1.0 { inodeUsage = 1.0 } } tab.AppendRow([]interface{}{ termenv.String(v.Mountpoint).Foreground(theme.colorBlue), // mounted on v.Total, // size v.Used, // used v.Free, // avail usage, // use% v.Inodes, // inodes v.InodesUsed, // inodes used v.InodesFree, // inodes avail inodeUsage, // inodes use% termenv.String(v.Fstype).Foreground(theme.colorGray), // type termenv.String(v.Device).Foreground(theme.colorGray), // filesystem v.Total, // size sorting helper v.Used, // used sorting helper v.Free, // avail sorting helper usage, // use% sorting helper v.Inodes, // inodes sorting helper v.InodesUsed, // inodes used sorting helper v.InodesFree, // inodes avail sorting helper inodeUsage, // inodes use% sorting helper }) } } // computeMaxContentWidths calculates the maximum content width for each visible column. func computeMaxContentWidths(m []Mount, opts TableOptions) map[int]int { visibleCols := append([]int{}, opts.Columns...) maxColContent := map[int]int{} // Seed with headers for _, ci := range visibleCols { maxColContent[ci] = runewidth.StringWidth(columns[ci-1].Name) } for _, v := range m { if inColumns(opts.Columns, 1) { if w := runewidth.StringWidth(v.Mountpoint); w > maxColContent[1] { maxColContent[1] = w } } if inColumns(opts.Columns, 2) { if w := runewidth.StringWidth(sizeToString(v.Total)); w > maxColContent[2] { maxColContent[2] = w } } if inColumns(opts.Columns, 3) { if w := runewidth.StringWidth(sizeToString(v.Used)); w > maxColContent[3] { maxColContent[3] = w } } if inColumns(opts.Columns, 4) { if w := runewidth.StringWidth(sizeToString(v.Free)); w > maxColContent[4] { maxColContent[4] = w } } if inColumns(opts.Columns, 5) { var usage float64 if v.Total > 0 { usage = float64(v.Used) / float64(v.Total) if usage > 1.0 { usage = 1.0 } } percentStr := fmt.Sprintf("%.1f%%", usage*100) if w := runewidth.StringWidth(percentStr); w > maxColContent[5] { maxColContent[5] = w } } if inColumns(opts.Columns, 6) { if w := runewidth.StringWidth(strconv.FormatUint(v.Inodes, 10)); w > maxColContent[6] { maxColContent[6] = w } } if inColumns(opts.Columns, 7) { if w := runewidth.StringWidth(strconv.FormatUint(v.InodesUsed, 10)); w > maxColContent[7] { maxColContent[7] = w } } if inColumns(opts.Columns, 8) { if w := runewidth.StringWidth(strconv.FormatUint(v.InodesFree, 10)); w > maxColContent[8] { maxColContent[8] = w } } if inColumns(opts.Columns, 9) { var usage float64 if v.Inodes > 0 { usage = float64(v.InodesUsed) / float64(v.Inodes) if usage > 1.0 { usage = 1.0 } } percentStr := fmt.Sprintf("%.1f%%", usage*100) if w := runewidth.StringWidth(percentStr); w > maxColContent[9] { maxColContent[9] = w } } if inColumns(opts.Columns, 10) { if w := runewidth.StringWidth(v.Fstype); w > maxColContent[10] { maxColContent[10] = w } } if inColumns(opts.Columns, 11) { if w := runewidth.StringWidth(v.Device); w > maxColContent[11] { maxColContent[11] = w } } } return maxColContent } // computeAssignedWidths computes the assigned widths for dynamic columns (1, 10, 11). func computeAssignedWidths(maxColContent map[int]int, opts TableOptions) (map[int]int, int) { visibleCols := append([]int{}, opts.Columns...) nVis := len(visibleCols) // Non-content overhead sepWidth := 1 paddingPerCol := 2 overhead := (nVis+1)*sepWidth + nVis*paddingPerCol totalAllowed := int(*width) // Determine targets and their need targets := []int{} weights := map[int]float64{1: 0.4, 10: 0.2, 11: 0.4} weightSum := 0.0 for _, t := range []int{1, 10, 11} { if inColumns(opts.Columns, t) { targets = append(targets, t) weightSum += weights[t] } } // Sum fixed widths of non-target visible columns fixedContentWidth := 0 for _, ci := range visibleCols { if ci == 1 || ci == 10 || ci == 11 { continue } fixedContentWidth += maxColContent[ci] } availableContent := totalAllowed - overhead - fixedContentWidth if availableContent < 0 { availableContent = 0 } // Cap target allocations by their max content need assigned := map[int]int{} used := 0 if availableContent > 0 && len(targets) > 0 { for _, t := range targets { share := int(float64(availableContent) * (weights[t] / weightSum)) if share > maxColContent[t] { share = maxColContent[t] } assigned[t] = share used += share } // remainder distribution remainder := availableContent - used for remainder > 0 { bestCol := 0 bestNeed := 0 for _, t := range targets { need := maxColContent[t] - assigned[t] if need > bestNeed { bestNeed = need bestCol = t } } if bestNeed <= 0 { break } take := remainder if take > bestNeed { take = bestNeed } assigned[bestCol] += take remainder -= take } } // Calculate final slack predictedTotal := overhead + fixedContentWidth for _, t := range targets { predictedTotal += assigned[t] } slack := totalAllowed - predictedTotal return assigned, slack } // setColumnConfigs configures the columns for the table. func setColumnConfigs(tab table.Writer, maxColContent map[int]int, assigned map[int]int, opts TableOptions, barTransformerFunc func(interface{}) string) { cfgs := []table.ColumnConfig{ {Number: 1, Hidden: !inColumns(opts.Columns, 1), WidthMax: assigned[1]}, {Number: 2, Hidden: !inColumns(opts.Columns, 2), Transformer: sizeTransformer, Align: text.AlignRight, AlignHeader: text.AlignRight, WidthMax: maxColContent[2]}, {Number: 3, Hidden: !inColumns(opts.Columns, 3), Transformer: sizeTransformer, Align: text.AlignRight, AlignHeader: text.AlignRight, WidthMax: maxColContent[3]}, {Number: 4, Hidden: !inColumns(opts.Columns, 4), Transformer: spaceTransformer, Align: text.AlignRight, AlignHeader: text.AlignRight, WidthMax: maxColContent[4]}, {Number: 5, Hidden: !inColumns(opts.Columns, 5), Transformer: barTransformerFunc, AlignHeader: text.AlignCenter, WidthMax: maxColContent[5]}, {Number: 6, Hidden: !inColumns(opts.Columns, 6), Align: text.AlignRight, AlignHeader: text.AlignRight, WidthMax: maxColContent[6]}, {Number: 7, Hidden: !inColumns(opts.Columns, 7), Align: text.AlignRight, AlignHeader: text.AlignRight, WidthMax: maxColContent[7]}, {Number: 8, Hidden: !inColumns(opts.Columns, 8), Align: text.AlignRight, AlignHeader: text.AlignRight, WidthMax: maxColContent[8]}, {Number: 9, Hidden: !inColumns(opts.Columns, 9), Transformer: barTransformerFunc, AlignHeader: text.AlignCenter, WidthMax: maxColContent[9]}, {Number: 10, Hidden: !inColumns(opts.Columns, 10), WidthMax: assigned[10]}, {Number: 11, Hidden: !inColumns(opts.Columns, 11), WidthMax: assigned[11]}, {Number: 12, Hidden: true}, // sortBy helper for size {Number: 13, Hidden: true}, // sortBy helper for used {Number: 14, Hidden: true}, // sortBy helper for avail {Number: 15, Hidden: true}, // sortBy helper for usage {Number: 16, Hidden: true}, // sortBy helper for inodes size {Number: 17, Hidden: true}, // sortBy helper for inodes used {Number: 18, Hidden: true}, // sortBy helper for inodes avail {Number: 19, Hidden: true}, // sortBy helper for inodes usage } tab.SetColumnConfigs(cfgs) } // printTable prints an individual table of mounts. func printTable(title string, m []Mount, opts TableOptions) { tab := table.NewWriter() initializeTable(tab, opts) appendHeaders(tab) appendRows(tab, m) if tab.Length() == 0 { return } maxColContent := computeMaxContentWidths(m, opts) assigned, slack := computeAssignedWidths(maxColContent, opts) origPercentWidth5 := maxColContent[5] origPercentWidth9 := maxColContent[9] percentWidth := origPercentWidth5 if origPercentWidth9 > percentWidth { percentWidth = origPercentWidth9 } barWidth := 0 numBars := 0 if inColumns(opts.Columns, 5) { numBars++ } if inColumns(opts.Columns, 9) { numBars++ } if numBars > 0 && slack >= 6 { // Each bar consumes: barWidth + 1 (for space) // So for numBars, total consumption is: numBars * (barWidth + 1) maxBarWidth := min((slack/numBars)-1, 20) if maxBarWidth > 0 { barWidth = maxBarWidth if inColumns(opts.Columns, 5) { maxColContent[5] = barWidth + 1 + percentWidth } if inColumns(opts.Columns, 9) { maxColContent[9] = barWidth + 1 + percentWidth } } } // Define barTransformerFunc barTransformerFunc := func(val interface{}) string { usage := val.(float64) if barWidth <= 0 { s := fmt.Sprintf("%*s", percentWidth, fmt.Sprintf("%.1f%%", usage*100)) return termenv.String(s).String() } bw := barWidth var filledChar, halfChar, emptyChar string if opts.StyleName == "unicode" { filledChar = "█" halfChar = "▌" emptyChar = " " } else { bw -= 2 filledChar = "#" halfChar = "#" emptyChar = "." } filled := int(usage * float64(bw)) partial := usage*float64(bw) - float64(filled) empty := bw - filled var filledStr, emptyStr string filledStr = strings.Repeat(filledChar, filled) // If we have a sufficiently large partial, render a half block. if partial >= 0.5 { filledStr += halfChar empty-- } if empty < 0 { empty = 0 } emptyStr = strings.Repeat(emptyChar, empty) var format string if opts.StyleName == "unicode" { format = "%s%s %*s" } else { format = "[%s%s] %*s" } // Apply colors redUsage, _ := strconv.ParseFloat(strings.Split(*usageThreshold, ",")[1], 64) yellowUsage, _ := strconv.ParseFloat(strings.Split(*usageThreshold, ",")[0], 64) var fgColor termenv.Color switch { case usage >= redUsage: fgColor = theme.colorRed case usage >= yellowUsage: fgColor = theme.colorYellow default: fgColor = theme.colorGreen } filledPart := termenv.String(filledStr).Foreground(fgColor) emptyPart := termenv.String(emptyStr) if opts.StyleName == "unicode" { // Add background to filled part to prevent black spaces in half blocks // Use a background color that complements the foreground var bgColor termenv.Color switch { case usage >= redUsage: bgColor = theme.colorBgRed case usage >= yellowUsage: bgColor = theme.colorBgYellow default: bgColor = theme.colorBgGreen } filledPart = filledPart.Background(bgColor).Foreground(fgColor) // Use a neutral background for empty areas emptyPart = emptyPart.Background(bgColor) } s := fmt.Sprintf(format, filledPart, emptyPart, percentWidth, fmt.Sprintf("%.1f%%", usage*100)) return termenv.String(s).String() } setColumnConfigs(tab, maxColContent, assigned, opts, barTransformerFunc) suffix := "device" if tab.Length() > 1 { suffix = "devices" } tab.SetTitle("%d %s %s", tab.Length(), title, suffix) // tab.AppendFooter(table.Row{fmt.Sprintf("%d %s", tab.Length(), title)}) sortMode := table.Asc if opts.SortBy >= 12 { sortMode = table.AscNumeric } tab.SortBy([]table.SortBy{{Number: opts.SortBy, Mode: sortMode}}) tab.Render() } // sizeTransformer makes a size human-readable. func sizeTransformer(val interface{}) string { return sizeToString(val.(uint64)) } // spaceTransformer makes a size human-readable and applies a color coding. func spaceTransformer(val interface{}) string { free := val.(uint64) s := termenv.String(sizeToString(free)) redAvail, _ := stringToSize(strings.Split(*availThreshold, ",")[1]) yellowAvail, _ := stringToSize(strings.Split(*availThreshold, ",")[0]) switch { case free < redAvail: s = s.Foreground(theme.colorRed) case free < yellowAvail: s = s.Foreground(theme.colorYellow) default: s = s.Foreground(theme.colorGreen) } return s.String() } // inColumns return true if the column with index i is in the slice of visible // columns cols. func inColumns(cols []int, i int) bool { for _, v := range cols { if v == i { return true } } return false } // sizeToString prettifies sizes. func sizeToString(size uint64) (str string) { b := float64(size) switch { case size >= 1<<60: str = fmt.Sprintf("%.1fE", b/(1<<60)) case size >= 1<<50: str = fmt.Sprintf("%.1fP", b/(1<<50)) case size >= 1<<40: str = fmt.Sprintf("%.1fT", b/(1<<40)) case size >= 1<<30: str = fmt.Sprintf("%.1fG", b/(1<<30)) case size >= 1<<20: str = fmt.Sprintf("%.1fM", b/(1<<20)) case size >= 1<<10: str = fmt.Sprintf("%.1fK", b/(1<<10)) default: str = fmt.Sprintf("%dB", size) } return } // stringToSize transforms an SI size into a number. func stringToSize(s string) (size uint64, err error) { regex := regexp.MustCompile(`^(\d+)([KMGTPE]?)$`) matches := regex.FindStringSubmatch(s) if len(matches) == 0 { return 0, fmt.Errorf("'%s' is not valid, must have integer with optional SI prefix", s) } num, err := strconv.ParseUint(matches[1], 10, 64) if err != nil { return 0, err } if matches[2] != "" { prefix := matches[2] switch prefix { case "K": size = num << 10 case "M": size = num << 20 case "G": size = num << 30 case "T": size = num << 40 case "P": size = num << 50 case "E": size = num << 60 default: err = fmt.Errorf("prefix '%s' not allowed, valid prefixes are K, M, G, T, P, E", prefix) return } } else { size = num } return } // stringToColumn converts a column name to its index. func stringToColumn(s string) (int, error) { s = strings.ToLower(s) for i, v := range columns { if v.ID == s { return i + 1, nil } } return 0, fmt.Errorf("unknown column: %s (valid: %s)", s, strings.Join(columnIDs(), ", ")) } // stringToSortIndex converts a column name to its sort index. func stringToSortIndex(s string) (int, error) { s = strings.ToLower(s) for _, v := range columns { if v.ID == s { return v.SortIndex, nil } } return 0, fmt.Errorf("unknown column: %s (valid: %s)", s, strings.Join(columnIDs(), ", ")) } // columnsIDs returns a slice of all column IDs. func columnIDs() []string { s := make([]string, len(columns)) for i, v := range columns { s[i] = v.ID } return s } ================================================ FILE: themes.go ================================================ package main import ( "fmt" "github.com/muesli/termenv" ) // Theme defines a color theme used for printing tables. type Theme struct { colorRed termenv.Color colorYellow termenv.Color colorGreen termenv.Color colorBlue termenv.Color colorGray termenv.Color colorMagenta termenv.Color colorCyan termenv.Color colorBgRed termenv.Color colorBgYellow termenv.Color colorBgGreen termenv.Color } func defaultThemeName() string { if !termenv.HasDarkBackground() { return "light" } return "dark" } func loadTheme(theme string) (Theme, error) { themes := make(map[string]Theme) themes["dark"] = Theme{ colorRed: env.Color("#E88388"), colorYellow: env.Color("#DBAB79"), colorGreen: env.Color("#A8CC8C"), colorBlue: env.Color("#71BEF2"), colorGray: env.Color("#B9BFCA"), colorMagenta: env.Color("#D290E4"), colorCyan: env.Color("#66C2CD"), colorBgRed: env.Color("#2d1b1b"), colorBgYellow: env.Color("#2d2d1b"), colorBgGreen: env.Color("#1b2d1b"), } themes["light"] = Theme{ colorRed: env.Color("#D70000"), colorYellow: env.Color("#FFAF00"), colorGreen: env.Color("#005F00"), colorBlue: env.Color("#000087"), colorGray: env.Color("#303030"), colorMagenta: env.Color("#AF00FF"), colorCyan: env.Color("#0087FF"), colorBgRed: env.Color("#ffdede"), colorBgYellow: env.Color("#fff4d0"), colorBgGreen: env.Color("#e6ffe6"), } themes["ansi"] = Theme{ colorRed: env.Color("9"), colorYellow: env.Color("11"), colorGreen: env.Color("10"), colorBlue: env.Color("12"), colorGray: env.Color("7"), colorMagenta: env.Color("13"), colorCyan: env.Color("8"), colorBgRed: env.Color("1"), colorBgYellow: env.Color("3"), colorBgGreen: env.Color("2"), } if _, ok := themes[theme]; !ok { return Theme{}, fmt.Errorf("unknown theme: %s", theme) } return themes[theme], nil }