├── .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 ├── duf.png ├── 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 /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: muesli 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | labels: 8 | - "dependencies" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | labels: 14 | - "dependencies" 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | strategy: 7 | matrix: 8 | go-version: [~1.17, ^1] 9 | os: [ubuntu-latest, macos-latest, windows-latest] 10 | runs-on: ${{ matrix.os }} 11 | env: 12 | GO111MODULE: "on" 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v4.0.0 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | 19 | - name: Checkout code 20 | uses: actions/checkout@v3 21 | 22 | - name: Download Go modules 23 | run: go mod download 24 | 25 | - name: Build 26 | run: go build -v ./... 27 | 28 | - name: Test 29 | run: go test ./... 30 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | goreleaser: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | - name: Set up Go 16 | uses: actions/setup-go@v4.0.0 17 | - name: Run GoReleaser 18 | uses: goreleaser/goreleaser-action@v4 19 | with: 20 | version: latest 21 | args: release --snapshot --skip-publish --skip-sign --rm-dist 22 | -------------------------------------------------------------------------------- /.github/workflows/lint-soft.yml: -------------------------------------------------------------------------------- 1 | name: lint-soft 2 | on: 3 | push: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 9 | pull-requests: read 10 | 11 | jobs: 12 | golangci: 13 | name: lint-soft 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v3 19 | with: 20 | # Optional: golangci-lint command line arguments. 21 | args: --config .golangci-soft.yml --issues-exit-code=0 22 | # Optional: show only new issues if it's a pull request. The default value is `false`. 23 | only-new-issues: true 24 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 9 | pull-requests: read 10 | 11 | jobs: 12 | golangci: 13 | name: lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v3 19 | with: 20 | # Optional: golangci-lint command line arguments. 21 | #args: 22 | # Optional: show only new issues if it's a pull request. The default value is `false`. 23 | only-new-issues: true 24 | -------------------------------------------------------------------------------- /.github/workflows/manpage.yml: -------------------------------------------------------------------------------- 1 | name: manpage 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | manpage: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Install Go 13 | uses: actions/setup-go@v4.0.0 14 | with: 15 | go-version: 1.17 16 | 17 | - name: Checkout code 18 | uses: actions/checkout@v3 19 | 20 | - name: Download Go modules 21 | run: go mod download 22 | 23 | - name: Build 24 | run: go build -v -tags mango 25 | 26 | - name: Generate man-page 27 | run: ./duf > duf.1 28 | 29 | - name: Commit 30 | uses: stefanzweifel/git-auto-commit-action@v4 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | commit_message: "docs: update man page" 35 | branch: master 36 | commit_user_name: mango 🤖 37 | commit_user_email: actions@github.com 38 | commit_author: mango 🤖 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | duf 18 | 19 | dist/ 20 | -------------------------------------------------------------------------------- /.golangci-soft.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | 4 | issues: 5 | include: 6 | - EXC0001 7 | - EXC0005 8 | - EXC0011 9 | - EXC0012 10 | - EXC0013 11 | 12 | max-issues-per-linter: 0 13 | max-same-issues: 0 14 | 15 | linters: 16 | enable: 17 | # - dupl 18 | - exhaustive 19 | # - exhaustivestruct 20 | - goconst 21 | - godot 22 | - godox 23 | - gomnd 24 | - gomoddirectives 25 | - goprintffuncname 26 | - ifshort 27 | # - lll 28 | - misspell 29 | - nakedret 30 | - nestif 31 | - noctx 32 | - nolintlint 33 | - prealloc 34 | - wrapcheck 35 | 36 | # disable default linters, they are already enabled in .golangci.yml 37 | disable: 38 | - deadcode 39 | - errcheck 40 | - gosimple 41 | - govet 42 | - ineffassign 43 | - staticcheck 44 | - structcheck 45 | - typecheck 46 | - unused 47 | - varcheck 48 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | 4 | issues: 5 | include: 6 | - EXC0001 7 | - EXC0005 8 | - EXC0011 9 | - EXC0012 10 | - EXC0013 11 | 12 | max-issues-per-linter: 0 13 | max-same-issues: 0 14 | 15 | linters: 16 | enable: 17 | - bodyclose 18 | - exportloopref 19 | - goimports 20 | - gosec 21 | - nilerr 22 | - predeclared 23 | - revive 24 | - rowserrcheck 25 | - sqlclosecheck 26 | - tparallel 27 | - unconvert 28 | - unparam 29 | - whitespace 30 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | - CGO_ENABLED=0 4 | before: 5 | hooks: 6 | - go mod tidy 7 | builds: 8 | - binary: duf 9 | flags: 10 | - -trimpath 11 | ldflags: -s -w -X main.Version={{ .Version }} -X main.CommitSHA={{ .Commit }} 12 | goos: 13 | - linux 14 | - freebsd 15 | - openbsd 16 | - darwin 17 | - windows 18 | goarch: 19 | - amd64 20 | - arm64 21 | - 386 22 | - arm 23 | - ppc64le 24 | goarm: 25 | - 6 26 | - 7 27 | 28 | archives: 29 | - format_overrides: 30 | - goos: windows 31 | format: zip 32 | replacements: 33 | windows: Windows 34 | darwin: Darwin 35 | 386: i386 36 | amd64: x86_64 37 | 38 | nfpms: 39 | - builds: 40 | - duf 41 | vendor: muesli 42 | homepage: "https://fribbledom.com/" 43 | maintainer: "Christian Muehlhaeuser " 44 | description: "Disk Usage/Free Utility" 45 | license: MIT 46 | formats: 47 | - apk 48 | - deb 49 | - rpm 50 | bindir: /usr/bin 51 | 52 | brews: 53 | - goarm: 6 54 | tap: 55 | owner: muesli 56 | name: homebrew-tap 57 | commit_author: 58 | name: "Christian Muehlhaeuser" 59 | email: "muesli@gmail.com" 60 | homepage: "https://fribbledom.com/" 61 | description: "Disk Usage/Free Utility" 62 | # skip_upload: true 63 | 64 | signs: 65 | - artifacts: checksum 66 | 67 | checksum: 68 | name_template: "checksums.txt" 69 | snapshot: 70 | name_template: "{{ .Tag }}-next" 71 | changelog: 72 | sort: asc 73 | filters: 74 | exclude: 75 | - "^docs:" 76 | - "^test:" 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Christian Muehlhaeuser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | --- 24 | 25 | Portions of duf's code are copied and modified from 26 | https://github.com/shirou/gopsutil. 27 | 28 | gopsutil is distributed under BSD license reproduced below. 29 | 30 | Copyright (c) 2014, WAKAYAMA Shirou 31 | All rights reserved. 32 | 33 | Redistribution and use in source and binary forms, with or without modification, 34 | are permitted provided that the following conditions are met: 35 | 36 | * Redistributions of source code must retain the above copyright notice, this 37 | list of conditions and the following disclaimer. 38 | * Redistributions in binary form must reproduce the above copyright notice, 39 | this list of conditions and the following disclaimer in the documentation 40 | and/or other materials provided with the distribution. 41 | * Neither the name of the gopsutil authors nor the names of its contributors 42 | may be used to endorse or promote products derived from this software without 43 | specific prior written permission. 44 | 45 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 46 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 47 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 48 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 49 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 50 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 51 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 52 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 53 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 54 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # duf 2 | 3 | [![Latest Release](https://img.shields.io/github/release/muesli/duf.svg?style=for-the-badge)](https://github.com/muesli/duf/releases) 4 | [![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge)](https://pkg.go.dev/github.com/muesli/duf) 5 | [![Software License](https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge)](/LICENSE) 6 | [![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) 7 | [![Go ReportCard](https://goreportcard.com/badge/github.com/muesli/duf?style=for-the-badge)](https://goreportcard.com/report/muesli/duf) 8 | 9 | Disk Usage/Free Utility (Linux, BSD, macOS & Windows) 10 | 11 | ![duf](/duf.png) 12 | 13 | ## Features 14 | 15 | - [x] User-friendly, colorful output 16 | - [x] Adjusts to your terminal's theme & width 17 | - [x] Sort the results according to your needs 18 | - [x] Groups & filters devices 19 | - [x] Can conveniently output JSON 20 | 21 | ## Installation 22 | 23 | ### Packages 24 | 25 | #### Linux 26 | - Arch Linux: `pacman -S duf` 27 | - Ubuntu 22.04 / Debian unstable: `apt install duf` 28 | - Nix: `nix-env -iA nixpkgs.duf` 29 | - Void Linux: `xbps-install -S duf` 30 | - Gentoo Linux: `emerge sys-fs/duf` 31 | - [Packages](https://github.com/muesli/duf/releases) in Alpine, Debian & RPM formats 32 | 33 | #### BSD 34 | - FreeBSD: `pkg install duf` 35 | - OpenBSD: `pkg_add duf` 36 | 37 | #### macOS 38 | - with [Homebrew](https://brew.sh/): `brew install duf` 39 | - with [MacPorts](https://www.macports.org): `sudo port selfupdate && sudo port install duf` 40 | 41 | #### Windows 42 | - with [Chocolatey](https://chocolatey.org/): `choco install duf` 43 | - with [scoop](https://scoop.sh/): `scoop install duf` 44 | 45 | #### Android 46 | - Android (via termux): `pkg install duf` 47 | 48 | ### Binaries 49 | - [Binaries](https://github.com/muesli/duf/releases) for Linux, FreeBSD, OpenBSD, macOS, Windows 50 | 51 | ### From source 52 | 53 | Make sure you have a working Go environment (Go 1.17 or higher is required). 54 | See the [install instructions](https://golang.org/doc/install.html). 55 | 56 | Compiling duf is easy, simply run: 57 | 58 | git clone https://github.com/muesli/duf.git 59 | cd duf 60 | go build 61 | 62 | ## Usage 63 | 64 | You can simply start duf without any command-line arguments: 65 | 66 | duf 67 | 68 | If you supply arguments, duf will only list specific devices & mount points: 69 | 70 | duf /home /some/file 71 | 72 | If you want to list everything (including pseudo, duplicate, inaccessible file systems): 73 | 74 | duf --all 75 | 76 | ### Filtering 77 | 78 | You can show and hide specific tables: 79 | 80 | duf --only local,network,fuse,special,loops,binds 81 | duf --hide local,network,fuse,special,loops,binds 82 | 83 | You can also show and hide specific filesystems: 84 | 85 | duf --only-fs tmpfs,vfat 86 | duf --hide-fs tmpfs,vfat 87 | 88 | ...or specific mount points: 89 | 90 | duf --only-mp /,/home,/dev 91 | duf --hide-mp /,/home,/dev 92 | 93 | Wildcards inside quotes work: 94 | 95 | duf --only-mp '/sys/*,/dev/*' 96 | 97 | ### Display options 98 | 99 | Sort the output: 100 | 101 | duf --sort size 102 | 103 | Valid keys are: `mountpoint`, `size`, `used`, `avail`, `usage`, `inodes`, 104 | `inodes_used`, `inodes_avail`, `inodes_usage`, `type`, `filesystem`. 105 | 106 | Show or hide specific columns: 107 | 108 | duf --output mountpoint,size,usage 109 | 110 | Valid keys are: `mountpoint`, `size`, `used`, `avail`, `usage`, `inodes`, 111 | `inodes_used`, `inodes_avail`, `inodes_usage`, `type`, `filesystem`. 112 | 113 | List inode information instead of block usage: 114 | 115 | duf --inodes 116 | 117 | If duf doesn't detect your terminal's colors correctly, you can set a theme: 118 | 119 | duf --theme light 120 | 121 | ### Color-coding & Thresholds 122 | 123 | duf highlights the availability & usage columns in red, green, or yellow, 124 | depending on how much space is still available. You can set your own thresholds: 125 | 126 | duf --avail-threshold="10G,1G" 127 | duf --usage-threshold="0.5,0.9" 128 | 129 | ### Bonus 130 | 131 | If you prefer your output as JSON: 132 | 133 | duf --json 134 | 135 | ## Troubleshooting 136 | 137 | Users of `oh-my-zsh` should be aware that it already defines an alias called 138 | `duf`, which you will have to remove in order to use `duf`: 139 | 140 | unalias duf 141 | 142 | ## Feedback 143 | 144 | Got some feedback or suggestions? Please open an issue or drop me a note! 145 | 146 | * [Twitter](https://twitter.com/mueslix) 147 | * [The Fediverse](https://mastodon.social/@fribbledom) 148 | -------------------------------------------------------------------------------- /duf.1: -------------------------------------------------------------------------------- 1 | .TH DUF 1 "2023-09-20" "duf" "Disk Usage/Free Utility" 2 | .SH NAME 3 | duf - Disk Usage/Free Utility 4 | .SH SYNOPSIS 5 | \fBduf\fP [\fIoptions\&.\&.\&.\fP] [\fIargument\&.\&.\&.\fP] 6 | .SH DESCRIPTION 7 | Simple Disk Usage/Free Utility\&. 8 | .PP 9 | Features: 10 | .PP 11 | .RS 12 | .IP \(bu 3 13 | User-friendly, colorful output\&. 14 | .IP \(bu 3 15 | Adjusts to your terminal's theme & width\&. 16 | .IP \(bu 3 17 | Sort the results according to your needs\&. 18 | .IP \(bu 3 19 | Groups & filters devices\&. 20 | .IP \(bu 3 21 | Can conveniently output JSON\&. 22 | .SH OPTIONS 23 | .TP 24 | \fB-all\fP 25 | include pseudo, duplicate, inaccessible file systems 26 | .TP 27 | \fB-avail-threshold\fP 28 | specifies the coloring threshold (yellow, red) of the avail column, must be integer with optional SI prefixes 29 | .TP 30 | \fB-hide\fP 31 | hide specific devices, separated with commas: local, network, fuse, special, loops, binds 32 | .TP 33 | \fB-hide-fs\fP 34 | hide specific filesystems, separated with commas 35 | .TP 36 | \fB-hide-mp\fP 37 | hide specific mount points, separated with commas (supports wildcards) 38 | .TP 39 | \fB-inodes\fP 40 | list inode information instead of block usage 41 | .TP 42 | \fB-json\fP 43 | output all devices in JSON format 44 | .TP 45 | \fB-only\fP 46 | show only specific devices, separated with commas: local, network, fuse, special, loops, binds 47 | .TP 48 | \fB-only-fs\fP 49 | only specific filesystems, separated with commas 50 | .TP 51 | \fB-only-mp\fP 52 | only specific mount points, separated with commas (supports wildcards) 53 | .TP 54 | \fB-output\fP 55 | output fields: mountpoint, size, used, avail, usage, inodes, inodes_used, inodes_avail, inodes_usage, type, filesystem 56 | .TP 57 | \fB-sort\fP 58 | sort output by: mountpoint, size, used, avail, usage, inodes, inodes_used, inodes_avail, inodes_usage, type, filesystem 59 | .TP 60 | \fB-style\fP 61 | style: unicode, ascii 62 | .TP 63 | \fB-theme\fP 64 | color themes: dark, light, ansi 65 | .TP 66 | \fB-usage-threshold\fP 67 | specifies the coloring threshold (yellow, red) of the usage bars as a floating point number from 0 to 1 68 | .TP 69 | \fB-version\fP 70 | display version 71 | .TP 72 | \fB-warnings\fP 73 | output all warnings to STDERR 74 | .TP 75 | \fB-width\fP 76 | max output width 77 | .SH USAGE 78 | You can simply start duf without any command-line arguments: 79 | .PP 80 | .PP 81 | $ duf 82 | .PP 83 | .PP 84 | If you supply arguments, duf will only list specific devices & mount points: 85 | .PP 86 | .PP 87 | $ duf /home /some/file 88 | .PP 89 | .PP 90 | If you want to list everything (including pseudo, duplicate, inaccessible file systems): 91 | .PP 92 | .PP 93 | $ duf --all 94 | .PP 95 | .PP 96 | You can show and hide specific tables: 97 | .PP 98 | .PP 99 | $ duf --only local,network,fuse,special,loops,binds 100 | .PP 101 | $ duf --hide local,network,fuse,special,loops,binds 102 | .PP 103 | .PP 104 | You can also show and hide specific filesystems: 105 | .PP 106 | .PP 107 | $ duf --only-fs tmpfs,vfat 108 | .PP 109 | $ duf --hide-fs tmpfs,vfat 110 | .PP 111 | .PP 112 | \&.\&.\&.or specific mount points: 113 | .PP 114 | .PP 115 | $ duf --only-mp /,/home,/dev 116 | .PP 117 | $ duf --hide-mp /,/home,/dev 118 | .PP 119 | .PP 120 | Wildcards inside quotes work: 121 | .PP 122 | .PP 123 | $ duf --only-mp '/sys/*,/dev/*' 124 | .PP 125 | .PP 126 | Sort the output: 127 | .PP 128 | .PP 129 | $ duf --sort size 130 | .PP 131 | .PP 132 | Valid keys are: mountpoint, size, used, avail, usage, inodes, inodes_used, inodes_avail, inodes_usage, type, filesystem\&. 133 | .PP 134 | .PP 135 | Show or hide specific columns: 136 | .PP 137 | .PP 138 | $ duf --output mountpoint,size,usage 139 | .PP 140 | .PP 141 | Valid keys are: mountpoint, size, used, avail, usage, inodes, inodes_used, inodes_avail, inodes_usage, type, filesystem\&. 142 | .PP 143 | .PP 144 | List inode information instead of block usage: 145 | .PP 146 | .PP 147 | $ duf --inodes 148 | .PP 149 | .PP 150 | If duf doesn't detect your terminal's colors correctly, you can set a theme: 151 | .PP 152 | .PP 153 | $ duf --theme light 154 | .PP 155 | .PP 156 | 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: 157 | .PP 158 | .PP 159 | $ duf --avail-threshold="10G,1G" 160 | .PP 161 | $ duf --usage-threshold="0\&.5,0\&.9" 162 | .PP 163 | .PP 164 | If you prefer your output as JSON: 165 | .PP 166 | .PP 167 | $ duf --json 168 | .PP 169 | .SH NOTES 170 | Portions of duf's code are copied and modified from https://github\&.com/shirou/gopsutil\&. 171 | .PP 172 | gopsutil was written by WAKAYAMA Shirou and is distributed under BSD-3-Clause\&. 173 | .SH AUTHORS 174 | duf was written by Christian Muehlhaeuser 175 | .SH COPYRIGHT 176 | Copyright (C) 2020-2022 Christian Muehlhaeuser 177 | .PP 178 | Released under MIT license\&. 179 | -------------------------------------------------------------------------------- /duf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muesli/duf/ae480f3d59342a8963ffb7b4a5070a32086314fb/duf.png -------------------------------------------------------------------------------- /filesystems.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | func findMounts(mounts []Mount, path string) ([]Mount, error) { 10 | var err error 11 | path, err = filepath.Abs(path) 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | path, err = filepath.EvalSymlinks(path) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | _, err = os.Stat(path) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | var m []Mount 27 | for _, v := range mounts { 28 | if path == v.Device { 29 | return []Mount{v}, nil 30 | } 31 | 32 | if strings.HasPrefix(path, v.Mountpoint) { 33 | var nm []Mount 34 | 35 | // keep all entries that are as close or closer to the target 36 | for _, mv := range m { 37 | if len(mv.Mountpoint) >= len(v.Mountpoint) { 38 | nm = append(nm, mv) 39 | } 40 | } 41 | m = nm 42 | 43 | // add entry only if we didn't already find something closer 44 | if len(nm) == 0 || len(v.Mountpoint) >= len(nm[0].Mountpoint) { 45 | m = append(m, v) 46 | } 47 | } 48 | } 49 | 50 | return m, nil 51 | } 52 | 53 | func deviceType(m Mount) string { 54 | if isNetworkFs(m) { 55 | return networkDevice 56 | } 57 | if isSpecialFs(m) { 58 | return specialDevice 59 | } 60 | if isFuseFs(m) { 61 | return fuseDevice 62 | } 63 | 64 | return localDevice 65 | } 66 | 67 | // remote: [ "nfs", "smbfs", "cifs", "ncpfs", "afs", "coda", "ftpfs", "mfs", "sshfs", "fuse.sshfs", "nfs4" ] 68 | // special: [ "tmpfs", "devpts", "devtmpfs", "proc", "sysfs", "usbfs", "devfs", "fdescfs", "linprocfs" ] 69 | -------------------------------------------------------------------------------- /filesystems_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package main 5 | 6 | func isFuseFs(m Mount) bool { 7 | //FIXME: implement 8 | return false 9 | } 10 | 11 | func isNetworkFs(m Mount) bool { 12 | //FIXME: implement 13 | return false 14 | } 15 | 16 | func isSpecialFs(m Mount) bool { 17 | return m.Fstype == "devfs" 18 | } 19 | 20 | func isHiddenFs(m Mount) bool { 21 | return false 22 | } 23 | -------------------------------------------------------------------------------- /filesystems_freebsd.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd 2 | // +build freebsd 3 | 4 | package main 5 | 6 | func isFuseFs(m Mount) bool { 7 | //FIXME: implement 8 | return false 9 | } 10 | 11 | func isNetworkFs(m Mount) bool { 12 | fs := []string{"nfs", "smbfs"} 13 | 14 | for _, v := range fs { 15 | if m.Fstype == v { 16 | return true 17 | } 18 | } 19 | 20 | return false 21 | } 22 | 23 | func isSpecialFs(m Mount) bool { 24 | fs := []string{"devfs", "tmpfs", "linprocfs", "linsysfs", "fdescfs", "procfs"} 25 | 26 | for _, v := range fs { 27 | if m.Fstype == v { 28 | return true 29 | } 30 | } 31 | 32 | return false 33 | } 34 | 35 | func isHiddenFs(m Mount) bool { 36 | return false 37 | } 38 | -------------------------------------------------------------------------------- /filesystems_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package main 5 | 6 | import "strings" 7 | 8 | //nolint:revive,deadcode 9 | const ( 10 | // man statfs 11 | ADFS_SUPER_MAGIC = 0xadf5 12 | AFFS_SUPER_MAGIC = 0xADFF 13 | AUTOFS_SUPER_MAGIC = 0x0187 14 | BDEVFS_MAGIC = 0x62646576 15 | BEFS_SUPER_MAGIC = 0x42465331 16 | BFS_MAGIC = 0x1BADFACE 17 | BINFMTFS_MAGIC = 0x42494e4d 18 | BPF_FS_MAGIC = 0xcafe4a11 19 | BTRFS_SUPER_MAGIC = 0x9123683E 20 | CGROUP_SUPER_MAGIC = 0x27e0eb 21 | CGROUP2_SUPER_MAGIC = 0x63677270 22 | CIFS_MAGIC_NUMBER = 0xFF534D42 23 | CODA_SUPER_MAGIC = 0x73757245 24 | COH_SUPER_MAGIC = 0x012FF7B7 25 | CONFIGFS_MAGIC = 0x62656570 26 | CRAMFS_MAGIC = 0x28cd3d45 27 | DEBUGFS_MAGIC = 0x64626720 28 | DEVFS_SUPER_MAGIC = 0x1373 29 | DEVPTS_SUPER_MAGIC = 0x1cd1 30 | EFIVARFS_MAGIC = 0xde5e81e4 31 | EFS_SUPER_MAGIC = 0x00414A53 32 | EXT_SUPER_MAGIC = 0x137D 33 | EXT2_OLD_SUPER_MAGIC = 0xEF51 34 | EXT2_SUPER_MAGIC = 0xEF53 35 | EXT3_SUPER_MAGIC = 0xEF53 36 | EXT4_SUPER_MAGIC = 0xEF53 37 | FUSE_SUPER_MAGIC = 0x65735546 38 | FUTEXFS_SUPER_MAGIC = 0xBAD1DEA 39 | HFS_SUPER_MAGIC = 0x4244 40 | HFSPLUS_SUPER_MAGIC = 0x482b 41 | HOSTFS_SUPER_MAGIC = 0x00c0ffee 42 | HPFS_SUPER_MAGIC = 0xF995E849 43 | HUGETLBFS_MAGIC = 0x958458f6 44 | ISOFS_SUPER_MAGIC = 0x9660 45 | JFFS2_SUPER_MAGIC = 0x72b6 46 | JFS_SUPER_MAGIC = 0x3153464a 47 | MINIX_SUPER_MAGIC = 0x137F /* orig. minix */ 48 | MINIX_SUPER_MAGIC2 = 0x138F /* 30 char minix */ 49 | MINIX2_SUPER_MAGIC = 0x2468 /* minix V2 */ 50 | MINIX2_SUPER_MAGIC2 = 0x2478 /* minix V2, 30 char names */ 51 | MINIX3_SUPER_MAGIC = 0x4d5a /* minix V3 fs, 60 char names */ 52 | MQUEUE_MAGIC = 0x19800202 53 | MSDOS_SUPER_MAGIC = 0x4d44 54 | NCP_SUPER_MAGIC = 0x564c 55 | NFS_SUPER_MAGIC = 0x6969 56 | NILFS_SUPER_MAGIC = 0x3434 57 | NTFS_SB_MAGIC = 0x5346544e 58 | OCFS2_SUPER_MAGIC = 0x7461636f 59 | OPENPROM_SUPER_MAGIC = 0x9fa1 60 | PIPEFS_MAGIC = 0x50495045 61 | PROC_SUPER_MAGIC = 0x9fa0 62 | PSTOREFS_MAGIC = 0x6165676C 63 | QNX4_SUPER_MAGIC = 0x002f 64 | QNX6_SUPER_MAGIC = 0x68191122 65 | RAMFS_MAGIC = 0x858458f6 66 | REISERFS_SUPER_MAGIC = 0x52654973 67 | ROMFS_MAGIC = 0x7275 68 | SELINUX_MAGIC = 0xf97cff8c 69 | SMACK_MAGIC = 0x43415d53 70 | SMB_SUPER_MAGIC = 0x517B 71 | SMB2_MAGIC_NUMBER = 0xfe534d42 72 | SOCKFS_MAGIC = 0x534F434B 73 | SQUASHFS_MAGIC = 0x73717368 74 | SYSFS_MAGIC = 0x62656572 75 | SYSV2_SUPER_MAGIC = 0x012FF7B6 76 | SYSV4_SUPER_MAGIC = 0x012FF7B5 77 | TMPFS_MAGIC = 0x01021994 78 | TRACEFS_MAGIC = 0x74726163 79 | UDF_SUPER_MAGIC = 0x15013346 80 | UFS_MAGIC = 0x00011954 81 | USBDEVICE_SUPER_MAGIC = 0x9fa2 82 | V9FS_MAGIC = 0x01021997 83 | VXFS_SUPER_MAGIC = 0xa501FCF5 84 | XENFS_SUPER_MAGIC = 0xabba1974 85 | XENIX_SUPER_MAGIC = 0x012FF7B4 86 | XFS_SUPER_MAGIC = 0x58465342 87 | _XIAFS_SUPER_MAGIC = 0x012FD16D 88 | 89 | AFS_SUPER_MAGIC = 0x5346414F 90 | AUFS_SUPER_MAGIC = 0x61756673 91 | ANON_INODE_FS_SUPER_MAGIC = 0x09041934 92 | CEPH_SUPER_MAGIC = 0x00C36400 93 | ECRYPTFS_SUPER_MAGIC = 0xF15F 94 | FAT_SUPER_MAGIC = 0x4006 95 | FHGFS_SUPER_MAGIC = 0x19830326 96 | FUSEBLK_SUPER_MAGIC = 0x65735546 97 | FUSECTL_SUPER_MAGIC = 0x65735543 98 | GFS_SUPER_MAGIC = 0x1161970 99 | GPFS_SUPER_MAGIC = 0x47504653 100 | MTD_INODE_FS_SUPER_MAGIC = 0x11307854 101 | INOTIFYFS_SUPER_MAGIC = 0x2BAD1DEA 102 | ISOFS_R_WIN_SUPER_MAGIC = 0x4004 103 | ISOFS_WIN_SUPER_MAGIC = 0x4000 104 | JFFS_SUPER_MAGIC = 0x07C0 105 | KAFS_SUPER_MAGIC = 0x6B414653 106 | LUSTRE_SUPER_MAGIC = 0x0BD00BD0 107 | NFSD_SUPER_MAGIC = 0x6E667364 108 | PANFS_SUPER_MAGIC = 0xAAD7AAEA 109 | RPC_PIPEFS_SUPER_MAGIC = 0x67596969 110 | SECURITYFS_SUPER_MAGIC = 0x73636673 111 | UFS_BYTESWAPPED_SUPER_MAGIC = 0x54190100 112 | VMHGFS_SUPER_MAGIC = 0xBACBACBC 113 | VZFS_SUPER_MAGIC = 0x565A4653 114 | ZFS_SUPER_MAGIC = 0x2FC12FC1 115 | ) 116 | 117 | // coreutils/src/stat.c 118 | var fsTypeMap = map[int64]string{ 119 | ADFS_SUPER_MAGIC: "adfs", /* 0xADF5 local */ 120 | AFFS_SUPER_MAGIC: "affs", /* 0xADFF local */ 121 | AFS_SUPER_MAGIC: "afs", /* 0x5346414F remote */ 122 | ANON_INODE_FS_SUPER_MAGIC: "anon-inode FS", /* 0x09041934 local */ 123 | AUFS_SUPER_MAGIC: "aufs", /* 0x61756673 remote */ 124 | AUTOFS_SUPER_MAGIC: "autofs", /* 0x0187 local */ 125 | BEFS_SUPER_MAGIC: "befs", /* 0x42465331 local */ 126 | BDEVFS_MAGIC: "bdevfs", /* 0x62646576 local */ 127 | BFS_MAGIC: "bfs", /* 0x1BADFACE local */ 128 | BINFMTFS_MAGIC: "binfmt_misc", /* 0x42494E4D local */ 129 | BTRFS_SUPER_MAGIC: "btrfs", /* 0x9123683E local */ 130 | CEPH_SUPER_MAGIC: "ceph", /* 0x00C36400 remote */ 131 | CGROUP_SUPER_MAGIC: "cgroupfs", /* 0x0027E0EB local */ 132 | CIFS_MAGIC_NUMBER: "cifs", /* 0xFF534D42 remote */ 133 | CODA_SUPER_MAGIC: "coda", /* 0x73757245 remote */ 134 | COH_SUPER_MAGIC: "coh", /* 0x012FF7B7 local */ 135 | CRAMFS_MAGIC: "cramfs", /* 0x28CD3D45 local */ 136 | DEBUGFS_MAGIC: "debugfs", /* 0x64626720 local */ 137 | DEVFS_SUPER_MAGIC: "devfs", /* 0x1373 local */ 138 | DEVPTS_SUPER_MAGIC: "devpts", /* 0x1CD1 local */ 139 | ECRYPTFS_SUPER_MAGIC: "ecryptfs", /* 0xF15F local */ 140 | EFS_SUPER_MAGIC: "efs", /* 0x00414A53 local */ 141 | EXT_SUPER_MAGIC: "ext", /* 0x137D local */ 142 | EXT2_SUPER_MAGIC: "ext2/ext3", /* 0xEF53 local */ 143 | EXT2_OLD_SUPER_MAGIC: "ext2", /* 0xEF51 local */ 144 | FAT_SUPER_MAGIC: "fat", /* 0x4006 local */ 145 | FHGFS_SUPER_MAGIC: "fhgfs", /* 0x19830326 remote */ 146 | FUSEBLK_SUPER_MAGIC: "fuseblk", /* 0x65735546 remote */ 147 | FUSECTL_SUPER_MAGIC: "fusectl", /* 0x65735543 remote */ 148 | FUTEXFS_SUPER_MAGIC: "futexfs", /* 0x0BAD1DEA local */ 149 | GFS_SUPER_MAGIC: "gfs/gfs2", /* 0x1161970 remote */ 150 | GPFS_SUPER_MAGIC: "gpfs", /* 0x47504653 remote */ 151 | HFS_SUPER_MAGIC: "hfs", /* 0x4244 local */ 152 | HFSPLUS_SUPER_MAGIC: "hfsplus", /* 0x482b local */ 153 | HPFS_SUPER_MAGIC: "hpfs", /* 0xF995E849 local */ 154 | HUGETLBFS_MAGIC: "hugetlbfs", /* 0x958458F6 local */ 155 | MTD_INODE_FS_SUPER_MAGIC: "inodefs", /* 0x11307854 local */ 156 | INOTIFYFS_SUPER_MAGIC: "inotifyfs", /* 0x2BAD1DEA local */ 157 | ISOFS_SUPER_MAGIC: "isofs", /* 0x9660 local */ 158 | ISOFS_R_WIN_SUPER_MAGIC: "isofs", /* 0x4004 local */ 159 | ISOFS_WIN_SUPER_MAGIC: "isofs", /* 0x4000 local */ 160 | JFFS_SUPER_MAGIC: "jffs", /* 0x07C0 local */ 161 | JFFS2_SUPER_MAGIC: "jffs2", /* 0x72B6 local */ 162 | JFS_SUPER_MAGIC: "jfs", /* 0x3153464A local */ 163 | KAFS_SUPER_MAGIC: "k-afs", /* 0x6B414653 remote */ 164 | LUSTRE_SUPER_MAGIC: "lustre", /* 0x0BD00BD0 remote */ 165 | MINIX_SUPER_MAGIC: "minix", /* 0x137F local */ 166 | MINIX_SUPER_MAGIC2: "minix (30 char.)", /* 0x138F local */ 167 | MINIX2_SUPER_MAGIC: "minix v2", /* 0x2468 local */ 168 | MINIX2_SUPER_MAGIC2: "minix v2 (30 char.)", /* 0x2478 local */ 169 | MINIX3_SUPER_MAGIC: "minix3", /* 0x4D5A local */ 170 | MQUEUE_MAGIC: "mqueue", /* 0x19800202 local */ 171 | MSDOS_SUPER_MAGIC: "msdos", /* 0x4D44 local */ 172 | NCP_SUPER_MAGIC: "novell", /* 0x564C remote */ 173 | NFS_SUPER_MAGIC: "nfs", /* 0x6969 remote */ 174 | NFSD_SUPER_MAGIC: "nfsd", /* 0x6E667364 remote */ 175 | NILFS_SUPER_MAGIC: "nilfs", /* 0x3434 local */ 176 | NTFS_SB_MAGIC: "ntfs", /* 0x5346544E local */ 177 | OPENPROM_SUPER_MAGIC: "openprom", /* 0x9FA1 local */ 178 | OCFS2_SUPER_MAGIC: "ocfs2", /* 0x7461636f remote */ 179 | PANFS_SUPER_MAGIC: "panfs", /* 0xAAD7AAEA remote */ 180 | PIPEFS_MAGIC: "pipefs", /* 0x50495045 remote */ 181 | PROC_SUPER_MAGIC: "proc", /* 0x9FA0 local */ 182 | PSTOREFS_MAGIC: "pstorefs", /* 0x6165676C local */ 183 | QNX4_SUPER_MAGIC: "qnx4", /* 0x002F local */ 184 | QNX6_SUPER_MAGIC: "qnx6", /* 0x68191122 local */ 185 | RAMFS_MAGIC: "ramfs", /* 0x858458F6 local */ 186 | REISERFS_SUPER_MAGIC: "reiserfs", /* 0x52654973 local */ 187 | ROMFS_MAGIC: "romfs", /* 0x7275 local */ 188 | RPC_PIPEFS_SUPER_MAGIC: "rpc_pipefs", /* 0x67596969 local */ 189 | SECURITYFS_SUPER_MAGIC: "securityfs", /* 0x73636673 local */ 190 | SELINUX_MAGIC: "selinux", /* 0xF97CFF8C local */ 191 | SMB_SUPER_MAGIC: "smb", /* 0x517B remote */ 192 | SMB2_MAGIC_NUMBER: "smb2", /* 0xfe534d42 remote */ 193 | SOCKFS_MAGIC: "sockfs", /* 0x534F434B local */ 194 | SQUASHFS_MAGIC: "squashfs", /* 0x73717368 local */ 195 | SYSFS_MAGIC: "sysfs", /* 0x62656572 local */ 196 | SYSV2_SUPER_MAGIC: "sysv2", /* 0x012FF7B6 local */ 197 | SYSV4_SUPER_MAGIC: "sysv4", /* 0x012FF7B5 local */ 198 | TMPFS_MAGIC: "tmpfs", /* 0x01021994 local */ 199 | UDF_SUPER_MAGIC: "udf", /* 0x15013346 local */ 200 | UFS_MAGIC: "ufs", /* 0x00011954 local */ 201 | UFS_BYTESWAPPED_SUPER_MAGIC: "ufs", /* 0x54190100 local */ 202 | USBDEVICE_SUPER_MAGIC: "usbdevfs", /* 0x9FA2 local */ 203 | V9FS_MAGIC: "v9fs", /* 0x01021997 local */ 204 | VMHGFS_SUPER_MAGIC: "vmhgfs", /* 0xBACBACBC remote */ 205 | VXFS_SUPER_MAGIC: "vxfs", /* 0xA501FCF5 local */ 206 | VZFS_SUPER_MAGIC: "vzfs", /* 0x565A4653 local */ 207 | XENFS_SUPER_MAGIC: "xenfs", /* 0xABBA1974 local */ 208 | XENIX_SUPER_MAGIC: "xenix", /* 0x012FF7B4 local */ 209 | XFS_SUPER_MAGIC: "xfs", /* 0x58465342 local */ 210 | _XIAFS_SUPER_MAGIC: "xia", /* 0x012FD16D local */ 211 | ZFS_SUPER_MAGIC: "zfs", /* 0x2FC12FC1 local */ 212 | } 213 | 214 | /* 215 | var localMap = map[int64]bool{ 216 | AFS_SUPER_MAGIC: true, 217 | BTRFS_SUPER_MAGIC: true, 218 | EXT_SUPER_MAGIC: true, 219 | EXT2_OLD_SUPER_MAGIC: true, 220 | EXT2_SUPER_MAGIC: true, 221 | FAT_SUPER_MAGIC: true, 222 | HPFS_SUPER_MAGIC: true, 223 | MSDOS_SUPER_MAGIC: true, 224 | NTFS_SB_MAGIC: true, 225 | REISERFS_SUPER_MAGIC: true, 226 | UDF_SUPER_MAGIC: true, 227 | XFS_SUPER_MAGIC: true, 228 | ZFS_SUPER_MAGIC: true, 229 | } 230 | */ 231 | 232 | var networkMap = map[int64]bool{ 233 | CIFS_MAGIC_NUMBER: true, 234 | NFS_SUPER_MAGIC: true, 235 | SMB_SUPER_MAGIC: true, 236 | SMB2_MAGIC_NUMBER: true, 237 | } 238 | 239 | var specialMap = map[int64]bool{ 240 | AUTOFS_SUPER_MAGIC: true, 241 | BINFMTFS_MAGIC: true, 242 | BPF_FS_MAGIC: true, 243 | CGROUP_SUPER_MAGIC: true, 244 | CGROUP2_SUPER_MAGIC: true, 245 | CONFIGFS_MAGIC: true, 246 | DEBUGFS_MAGIC: true, 247 | DEVPTS_SUPER_MAGIC: true, 248 | EFIVARFS_MAGIC: true, 249 | FUSECTL_SUPER_MAGIC: true, 250 | HUGETLBFS_MAGIC: true, 251 | MQUEUE_MAGIC: true, 252 | PROC_SUPER_MAGIC: true, 253 | PSTOREFS_MAGIC: true, 254 | SECURITYFS_SUPER_MAGIC: true, 255 | SYSFS_MAGIC: true, 256 | TMPFS_MAGIC: true, 257 | TRACEFS_MAGIC: true, 258 | } 259 | 260 | /* 261 | func isLocalFs(m Mount) bool { 262 | return localMap[int64(m.Stat().Type)] //nolint:unconvert 263 | } 264 | */ 265 | 266 | func isFuseFs(m Mount) bool { 267 | return m.Stat().Type == FUSEBLK_SUPER_MAGIC || 268 | m.Stat().Type == FUSE_SUPER_MAGIC 269 | } 270 | 271 | func isNetworkFs(m Mount) bool { 272 | return networkMap[int64(m.Stat().Type)] //nolint:unconvert 273 | } 274 | 275 | func isSpecialFs(m Mount) bool { 276 | if m.Device == "nsfs" { 277 | return true 278 | } 279 | 280 | return specialMap[int64(m.Stat().Type)] //nolint:unconvert 281 | } 282 | 283 | func isHiddenFs(m Mount) bool { 284 | switch m.Device { 285 | case "shm": 286 | return true 287 | case "overlay": 288 | return true 289 | } 290 | 291 | switch m.Fstype { 292 | case "autofs": 293 | return true 294 | case "squashfs": 295 | if strings.HasPrefix(m.Mountpoint, "/snap") { 296 | return true 297 | } 298 | } 299 | 300 | return false 301 | } 302 | -------------------------------------------------------------------------------- /filesystems_openbsd.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd 2 | // +build openbsd 3 | 4 | package main 5 | 6 | func isFuseFs(m Mount) bool { 7 | //FIXME: implement 8 | return false 9 | } 10 | 11 | func isNetworkFs(m Mount) bool { 12 | //FIXME: implement 13 | return false 14 | } 15 | 16 | func isSpecialFs(m Mount) bool { 17 | return m.Fstype == "devfs" 18 | } 19 | 20 | func isHiddenFs(m Mount) bool { 21 | return false 22 | } 23 | -------------------------------------------------------------------------------- /filesystems_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | import ( 7 | "golang.org/x/sys/windows/registry" 8 | ) 9 | 10 | const ( 11 | WindowsSandboxMountPointRegistryPath = `Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2\CPC\LocalMOF` 12 | ) 13 | 14 | var windowsSandboxMountPoints = loadRegisteredWindowsSandboxMountPoints() 15 | 16 | func loadRegisteredWindowsSandboxMountPoints() (ret map[string]struct{}) { 17 | ret = make(map[string]struct{}) 18 | key, err := registry.OpenKey(registry.CURRENT_USER, WindowsSandboxMountPointRegistryPath, registry.READ) 19 | if err != nil { 20 | return 21 | } 22 | 23 | keyInfo, err := key.Stat() 24 | if err != nil { 25 | return 26 | } 27 | 28 | mountPoints, err := key.ReadValueNames(int(keyInfo.ValueCount)) 29 | if err != nil { 30 | return 31 | } 32 | 33 | for _, val := range mountPoints { 34 | ret[val] = struct{}{} 35 | } 36 | return ret 37 | } 38 | 39 | func isFuseFs(m Mount) bool { 40 | //FIXME: implement 41 | return false 42 | } 43 | 44 | func isNetworkFs(m Mount) bool { 45 | _, ok := m.Metadata.(*NetResource) 46 | return ok 47 | } 48 | 49 | func isSpecialFs(m Mount) bool { 50 | _, ok := windowsSandboxMountPoints[m.Mountpoint] 51 | return ok 52 | } 53 | 54 | func isHiddenFs(m Mount) bool { 55 | return false 56 | } 57 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/muesli/duf 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/IGLOU-EU/go-wildcard v1.0.3 7 | github.com/jedib0t/go-pretty/v6 v6.4.6 8 | github.com/mattn/go-runewidth v0.0.14 9 | github.com/muesli/mango v0.2.0 10 | github.com/muesli/roff v0.1.0 11 | github.com/muesli/termenv v0.15.1 12 | golang.org/x/sys v0.7.0 13 | golang.org/x/term v0.7.0 14 | ) 15 | 16 | require ( 17 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 18 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 19 | github.com/mattn/go-isatty v0.0.17 // indirect 20 | github.com/rivo/uniseg v0.2.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/IGLOU-EU/go-wildcard v1.0.3 h1:r8T46+8/9V1STciXJomTWRpPEv4nGJATDbJkdU0Nou0= 2 | github.com/IGLOU-EU/go-wildcard v1.0.3/go.mod h1:/qeV4QLmydCbwH0UMQJmXDryrFKJknWi/jjO8IiuQfY= 3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw= 9 | github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= 10 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 11 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 12 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 13 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 14 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 15 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 16 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 17 | github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ= 18 | github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= 19 | github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= 20 | github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= 21 | github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= 22 | github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= 23 | github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 27 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 29 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 30 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 31 | github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= 32 | github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 33 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= 37 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= 39 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 41 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 43 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | -------------------------------------------------------------------------------- /groups.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | const ( 8 | localDevice = "local" 9 | networkDevice = "network" 10 | fuseDevice = "fuse" 11 | specialDevice = "special" 12 | loopsDevice = "loops" 13 | bindsMount = "binds" 14 | ) 15 | 16 | // FilterOptions contains all filters. 17 | type FilterOptions struct { 18 | HiddenDevices map[string]struct{} 19 | OnlyDevices map[string]struct{} 20 | 21 | HiddenFilesystems map[string]struct{} 22 | OnlyFilesystems map[string]struct{} 23 | 24 | HiddenMountPoints map[string]struct{} 25 | OnlyMountPoints map[string]struct{} 26 | } 27 | 28 | // renderTables renders all tables. 29 | func renderTables(m []Mount, filters FilterOptions, opts TableOptions) { 30 | deviceMounts := make(map[string][]Mount) 31 | hasOnlyDevices := len(filters.OnlyDevices) != 0 32 | 33 | _, hideLocal := filters.HiddenDevices[localDevice] 34 | _, hideNetwork := filters.HiddenDevices[networkDevice] 35 | _, hideFuse := filters.HiddenDevices[fuseDevice] 36 | _, hideSpecial := filters.HiddenDevices[specialDevice] 37 | _, hideLoops := filters.HiddenDevices[loopsDevice] 38 | _, hideBinds := filters.HiddenDevices[bindsMount] 39 | 40 | _, onlyLocal := filters.OnlyDevices[localDevice] 41 | _, onlyNetwork := filters.OnlyDevices[networkDevice] 42 | _, onlyFuse := filters.OnlyDevices[fuseDevice] 43 | _, onlySpecial := filters.OnlyDevices[specialDevice] 44 | _, onlyLoops := filters.OnlyDevices[loopsDevice] 45 | _, onlyBinds := filters.OnlyDevices[bindsMount] 46 | 47 | // sort/filter devices 48 | for _, v := range m { 49 | if len(filters.OnlyFilesystems) != 0 { 50 | // skip not onlyFs 51 | if _, ok := filters.OnlyFilesystems[strings.ToLower(v.Fstype)]; !ok { 52 | continue 53 | } 54 | } else { 55 | // skip hideFs 56 | if _, ok := filters.HiddenFilesystems[strings.ToLower(v.Fstype)]; ok { 57 | continue 58 | } 59 | } 60 | 61 | // skip hidden devices 62 | if isHiddenFs(v) && !*all { 63 | continue 64 | } 65 | 66 | // skip bind-mounts 67 | if strings.Contains(v.Opts, "bind") { 68 | if (hasOnlyDevices && !onlyBinds) || (hideBinds && !*all) { 69 | continue 70 | } 71 | } 72 | 73 | // skip loop devices 74 | if strings.HasPrefix(v.Device, "/dev/loop") { 75 | if (hasOnlyDevices && !onlyLoops) || (hideLoops && !*all) { 76 | continue 77 | } 78 | } 79 | 80 | // skip special devices 81 | if v.Blocks == 0 && !*all { 82 | continue 83 | } 84 | 85 | // skip zero size devices 86 | if v.BlockSize == 0 && !*all { 87 | continue 88 | } 89 | 90 | // skip not only mount point 91 | if len(filters.OnlyMountPoints) != 0 { 92 | if !findInKey(v.Mountpoint, filters.OnlyMountPoints) { 93 | continue 94 | } 95 | } 96 | 97 | // skip hidden mount point 98 | if len(filters.HiddenMountPoints) != 0 { 99 | if findInKey(v.Mountpoint, filters.HiddenMountPoints) { 100 | continue 101 | } 102 | } 103 | 104 | t := deviceType(v) 105 | deviceMounts[t] = append(deviceMounts[t], v) 106 | } 107 | 108 | // print tables 109 | for _, devType := range groups { 110 | mounts := deviceMounts[devType] 111 | 112 | shouldPrint := *all 113 | if !shouldPrint { 114 | switch devType { 115 | case localDevice: 116 | shouldPrint = (hasOnlyDevices && onlyLocal) || (!hasOnlyDevices && !hideLocal) 117 | case networkDevice: 118 | shouldPrint = (hasOnlyDevices && onlyNetwork) || (!hasOnlyDevices && !hideNetwork) 119 | case fuseDevice: 120 | shouldPrint = (hasOnlyDevices && onlyFuse) || (!hasOnlyDevices && !hideFuse) 121 | case specialDevice: 122 | shouldPrint = (hasOnlyDevices && onlySpecial) || (!hasOnlyDevices && !hideSpecial) 123 | } 124 | } 125 | 126 | if shouldPrint { 127 | printTable(devType, mounts, opts) 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strconv" 9 | "strings" 10 | 11 | wildcard "github.com/IGLOU-EU/go-wildcard" 12 | "github.com/jedib0t/go-pretty/v6/table" 13 | "github.com/muesli/termenv" 14 | "golang.org/x/term" 15 | ) 16 | 17 | var ( 18 | // Version contains the application version number. It's set via ldflags 19 | // when building. 20 | Version = "" 21 | 22 | // CommitSHA contains the SHA of the commit that this application was built 23 | // against. It's set via ldflags when building. 24 | CommitSHA = "" 25 | 26 | env = termenv.EnvColorProfile() 27 | theme Theme 28 | 29 | groups = []string{localDevice, networkDevice, fuseDevice, specialDevice, loopsDevice, bindsMount} 30 | allowedValues = strings.Join(groups, ", ") 31 | 32 | all = flag.Bool("all", false, "include pseudo, duplicate, inaccessible file systems") 33 | hideDevices = flag.String("hide", "", "hide specific devices, separated with commas:\n"+allowedValues) 34 | hideFs = flag.String("hide-fs", "", "hide specific filesystems, separated with commas") 35 | hideMp = flag.String("hide-mp", "", "hide specific mount points, separated with commas (supports wildcards)") 36 | onlyDevices = flag.String("only", "", "show only specific devices, separated with commas:\n"+allowedValues) 37 | onlyFs = flag.String("only-fs", "", "only specific filesystems, separated with commas") 38 | onlyMp = flag.String("only-mp", "", "only specific mount points, separated with commas (supports wildcards)") 39 | 40 | output = flag.String("output", "", "output fields: "+strings.Join(columnIDs(), ", ")) 41 | sortBy = flag.String("sort", "mountpoint", "sort output by: "+strings.Join(columnIDs(), ", ")) 42 | width = flag.Uint("width", 0, "max output width") 43 | themeOpt = flag.String("theme", defaultThemeName(), "color themes: dark, light, ansi") 44 | styleOpt = flag.String("style", defaultStyleName(), "style: unicode, ascii") 45 | 46 | availThreshold = flag.String("avail-threshold", "10G,1G", "specifies the coloring threshold (yellow, red) of the avail column, must be integer with optional SI prefixes") 47 | 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") 48 | 49 | inodes = flag.Bool("inodes", false, "list inode information instead of block usage") 50 | jsonOutput = flag.Bool("json", false, "output all devices in JSON format") 51 | warns = flag.Bool("warnings", false, "output all warnings to STDERR") 52 | version = flag.Bool("version", false, "display version") 53 | ) 54 | 55 | // renderJSON encodes the JSON output and prints it. 56 | func renderJSON(m []Mount) error { 57 | output, err := json.MarshalIndent(m, "", " ") 58 | if err != nil { 59 | return fmt.Errorf("error formatting the json output: %s", err) 60 | } 61 | 62 | fmt.Println(string(output)) 63 | return nil 64 | } 65 | 66 | // parseColumns parses the supplied output flag into a slice of column indices. 67 | func parseColumns(cols string) ([]int, error) { 68 | var i []int 69 | 70 | s := strings.Split(cols, ",") 71 | for _, v := range s { 72 | v = strings.TrimSpace(v) 73 | if len(v) == 0 { 74 | continue 75 | } 76 | 77 | col, err := stringToColumn(v) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | i = append(i, col) 83 | } 84 | 85 | return i, nil 86 | } 87 | 88 | // parseStyle converts user-provided style option into a table.Style. 89 | func parseStyle(styleOpt string) (table.Style, error) { 90 | switch styleOpt { 91 | case "unicode": 92 | return table.StyleRounded, nil 93 | case "ascii": 94 | return table.StyleDefault, nil 95 | default: 96 | return table.Style{}, fmt.Errorf("unknown style option: %s", styleOpt) 97 | } 98 | } 99 | 100 | // parseCommaSeparatedValues parses comma separated string into a map. 101 | func parseCommaSeparatedValues(values string) map[string]struct{} { 102 | m := make(map[string]struct{}) 103 | for _, v := range strings.Split(values, ",") { 104 | v = strings.TrimSpace(v) 105 | if len(v) == 0 { 106 | continue 107 | } 108 | 109 | v = strings.ToLower(v) 110 | m[v] = struct{}{} 111 | } 112 | return m 113 | } 114 | 115 | // validateGroups validates the parsed group maps. 116 | func validateGroups(m map[string]struct{}) error { 117 | for k := range m { 118 | found := false 119 | for _, g := range groups { 120 | if g == k { 121 | found = true 122 | break 123 | } 124 | } 125 | 126 | if !found { 127 | return fmt.Errorf("unknown device group: %s", k) 128 | } 129 | } 130 | 131 | return nil 132 | } 133 | 134 | // findInKey parse a slice of pattern to match the given key. 135 | func findInKey(str string, km map[string]struct{}) bool { 136 | for p := range km { 137 | if wildcard.Match(p, str) { 138 | return true 139 | } 140 | } 141 | 142 | return false 143 | } 144 | 145 | func main() { 146 | flag.Parse() 147 | 148 | if *version { 149 | if len(CommitSHA) > 7 { 150 | CommitSHA = CommitSHA[:7] 151 | } 152 | if Version == "" { 153 | Version = "(built from source)" 154 | } 155 | 156 | fmt.Printf("duf %s", Version) 157 | if len(CommitSHA) > 0 { 158 | fmt.Printf(" (%s)", CommitSHA) 159 | } 160 | 161 | fmt.Println() 162 | os.Exit(0) 163 | } 164 | 165 | // read mount table 166 | m, warnings, err := mounts() 167 | if err != nil { 168 | fmt.Fprintln(os.Stderr, err) 169 | os.Exit(1) 170 | } 171 | 172 | // print JSON 173 | if *jsonOutput { 174 | if err = renderJSON(m); err != nil { 175 | fmt.Fprintln(os.Stderr, err) 176 | } 177 | return 178 | } 179 | 180 | // validate theme 181 | theme, err = loadTheme(*themeOpt) 182 | if err != nil { 183 | fmt.Fprintln(os.Stderr, err) 184 | os.Exit(1) 185 | } 186 | if env == termenv.ANSI { 187 | // enforce ANSI theme for limited color support 188 | theme, err = loadTheme("ansi") 189 | if err != nil { 190 | fmt.Fprintln(os.Stderr, err) 191 | os.Exit(1) 192 | } 193 | } 194 | 195 | // validate style 196 | style, err := parseStyle(*styleOpt) 197 | if err != nil { 198 | fmt.Fprintln(os.Stderr, err) 199 | os.Exit(1) 200 | } 201 | 202 | // validate output columns 203 | columns, err := parseColumns(*output) 204 | if err != nil { 205 | fmt.Fprintln(os.Stderr, err) 206 | os.Exit(1) 207 | } 208 | if len(columns) == 0 { 209 | // no columns supplied, use defaults 210 | if *inodes { 211 | columns = []int{1, 6, 7, 8, 9, 10, 11} 212 | } else { 213 | columns = []int{1, 2, 3, 4, 5, 10, 11} 214 | } 215 | } 216 | 217 | // validate sort column 218 | sortCol, err := stringToSortIndex(*sortBy) 219 | if err != nil { 220 | fmt.Fprintln(os.Stderr, err) 221 | os.Exit(1) 222 | } 223 | 224 | // validate filters 225 | filters := FilterOptions{ 226 | HiddenDevices: parseCommaSeparatedValues(*hideDevices), 227 | OnlyDevices: parseCommaSeparatedValues(*onlyDevices), 228 | HiddenFilesystems: parseCommaSeparatedValues(*hideFs), 229 | OnlyFilesystems: parseCommaSeparatedValues(*onlyFs), 230 | HiddenMountPoints: parseCommaSeparatedValues(*hideMp), 231 | OnlyMountPoints: parseCommaSeparatedValues(*onlyMp), 232 | } 233 | err = validateGroups(filters.HiddenDevices) 234 | if err != nil { 235 | fmt.Println(err) 236 | os.Exit(1) 237 | } 238 | err = validateGroups(filters.OnlyDevices) 239 | if err != nil { 240 | fmt.Println(err) 241 | os.Exit(1) 242 | } 243 | 244 | // validate arguments 245 | if len(flag.Args()) > 0 { 246 | var mounts []Mount 247 | 248 | for _, v := range flag.Args() { 249 | var fm []Mount 250 | fm, err = findMounts(m, v) 251 | if err != nil { 252 | fmt.Println(err) 253 | os.Exit(1) 254 | } 255 | 256 | mounts = append(mounts, fm...) 257 | } 258 | 259 | m = mounts 260 | } 261 | 262 | // validate availability thresholds 263 | availbilityThresholds := strings.Split(*availThreshold, ",") 264 | if len(availbilityThresholds) != 2 { 265 | fmt.Fprintln(os.Stderr, fmt.Errorf("error parsing avail-threshold: invalid option '%s'", *availThreshold)) 266 | os.Exit(1) 267 | } 268 | for _, threshold := range availbilityThresholds { 269 | _, err = stringToSize(threshold) 270 | if err != nil { 271 | fmt.Fprintln(os.Stderr, "error parsing avail-threshold:", err) 272 | os.Exit(1) 273 | } 274 | } 275 | 276 | // validate usage thresholds 277 | usageThresholds := strings.Split(*usageThreshold, ",") 278 | if len(usageThresholds) != 2 { 279 | fmt.Fprintln(os.Stderr, fmt.Errorf("error parsing usage-threshold: invalid option '%s'", *usageThreshold)) 280 | os.Exit(1) 281 | } 282 | for _, threshold := range usageThresholds { 283 | _, err = strconv.ParseFloat(threshold, 64) 284 | if err != nil { 285 | fmt.Fprintln(os.Stderr, "error parsing usage-threshold:", err) 286 | os.Exit(1) 287 | } 288 | } 289 | 290 | // print out warnings 291 | if *warns { 292 | for _, warning := range warnings { 293 | fmt.Fprintln(os.Stderr, warning) 294 | } 295 | } 296 | 297 | // detect terminal width 298 | isTerminal := term.IsTerminal(int(os.Stdout.Fd())) 299 | if isTerminal && *width == 0 { 300 | w, _, err := term.GetSize(int(os.Stdout.Fd())) 301 | if err == nil { 302 | *width = uint(w) 303 | } 304 | } 305 | if *width == 0 { 306 | *width = 80 307 | } 308 | 309 | // print tables 310 | renderTables(m, filters, TableOptions{ 311 | Columns: columns, 312 | SortBy: sortCol, 313 | Style: style, 314 | }) 315 | } 316 | -------------------------------------------------------------------------------- /man.go: -------------------------------------------------------------------------------- 1 | //go:build mango 2 | // +build mango 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "os" 10 | 11 | "github.com/muesli/mango" 12 | "github.com/muesli/mango/mflag" 13 | "github.com/muesli/roff" 14 | ) 15 | 16 | func init() { 17 | usage := `You can simply start duf without any command-line arguments: 18 | 19 | $ duf 20 | 21 | If you supply arguments, duf will only list specific devices & mount points: 22 | 23 | $ duf /home /some/file 24 | 25 | If you want to list everything (including pseudo, duplicate, inaccessible file systems): 26 | 27 | $ duf --all 28 | 29 | You can show and hide specific tables: 30 | 31 | $ duf --only local,network,fuse,special,loops,binds 32 | $ duf --hide local,network,fuse,special,loops,binds 33 | 34 | You can also show and hide specific filesystems: 35 | 36 | $ duf --only-fs tmpfs,vfat 37 | $ duf --hide-fs tmpfs,vfat 38 | 39 | ...or specific mount points: 40 | 41 | $ duf --only-mp /,/home,/dev 42 | $ duf --hide-mp /,/home,/dev 43 | 44 | Wildcards inside quotes work: 45 | 46 | $ duf --only-mp '/sys/*,/dev/*' 47 | 48 | Sort the output: 49 | 50 | $ duf --sort size 51 | 52 | Valid keys are: mountpoint, size, used, avail, usage, inodes, inodes_used, inodes_avail, inodes_usage, type, filesystem. 53 | 54 | Show or hide specific columns: 55 | 56 | $ duf --output mountpoint,size,usage 57 | 58 | Valid keys are: mountpoint, size, used, avail, usage, inodes, inodes_used, inodes_avail, inodes_usage, type, filesystem. 59 | 60 | List inode information instead of block usage: 61 | 62 | $ duf --inodes 63 | 64 | If duf doesn't detect your terminal's colors correctly, you can set a theme: 65 | 66 | $ duf --theme light 67 | 68 | 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: 69 | 70 | $ duf --avail-threshold="10G,1G" 71 | $ duf --usage-threshold="0.5,0.9" 72 | 73 | If you prefer your output as JSON: 74 | 75 | $ duf --json 76 | ` 77 | 78 | manPage := mango.NewManPage(1, "duf", "Disk Usage/Free Utility"). 79 | WithLongDescription("Simple Disk Usage/Free Utility.\n"+ 80 | "Features:\n"+ 81 | "* User-friendly, colorful output.\n"+ 82 | "* Adjusts to your terminal's theme & width.\n"+ 83 | "* Sort the results according to your needs.\n"+ 84 | "* Groups & filters devices.\n"+ 85 | "* Can conveniently output JSON."). 86 | WithSection("Usage", usage). 87 | WithSection("Notes", "Portions of duf's code are copied and modified from https://github.com/shirou/gopsutil.\n"+ 88 | "gopsutil was written by WAKAYAMA Shirou and is distributed under BSD-3-Clause."). 89 | WithSection("Authors", "duf was written by Christian Muehlhaeuser "). 90 | WithSection("Copyright", "Copyright (C) 2020-2022 Christian Muehlhaeuser \n"+ 91 | "Released under MIT license.") 92 | 93 | flag.VisitAll(mflag.FlagVisitor(manPage)) 94 | fmt.Println(manPage.Build(roff.NewDocument())) 95 | os.Exit(0) 96 | } 97 | -------------------------------------------------------------------------------- /mounts.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | // Mount contains all metadata for a single filesystem mount. 10 | type Mount struct { 11 | Device string `json:"device"` 12 | DeviceType string `json:"device_type"` 13 | Mountpoint string `json:"mount_point"` 14 | Fstype string `json:"fs_type"` 15 | Type string `json:"type"` 16 | Opts string `json:"opts"` 17 | Total uint64 `json:"total"` 18 | Free uint64 `json:"free"` 19 | Used uint64 `json:"used"` 20 | Inodes uint64 `json:"inodes"` 21 | InodesFree uint64 `json:"inodes_free"` 22 | InodesUsed uint64 `json:"inodes_used"` 23 | Blocks uint64 `json:"blocks"` 24 | BlockSize uint64 `json:"block_size"` 25 | Metadata interface{} `json:"-"` 26 | } 27 | 28 | func readLines(filename string) ([]string, error) { 29 | file, err := os.Open(filename) 30 | if err != nil { 31 | return nil, err 32 | } 33 | defer file.Close() //nolint:errcheck // ignore error 34 | 35 | scanner := bufio.NewScanner(file) 36 | var s []string 37 | for scanner.Scan() { 38 | s = append(s, scanner.Text()) 39 | } 40 | 41 | return s, scanner.Err() 42 | } 43 | 44 | func unescapeFstab(path string) string { 45 | escaped, err := strconv.Unquote(`"` + path + `"`) 46 | if err != nil { 47 | return path 48 | } 49 | return escaped 50 | } 51 | 52 | //nolint:deadcode,unused // used on BSD 53 | func byteToString(orig []byte) string { 54 | n := -1 55 | l := -1 56 | for i, b := range orig { 57 | // skip left side null 58 | if l == -1 && b == 0 { 59 | continue 60 | } 61 | if l == -1 { 62 | l = i 63 | } 64 | 65 | if b == 0 { 66 | break 67 | } 68 | n = i + 1 69 | } 70 | if n == -1 { 71 | return string(orig) 72 | } 73 | return string(orig[l:n]) 74 | } 75 | 76 | //nolint:deadcode,unused // used on OpenBSD 77 | func intToString(orig []int8) string { 78 | ret := make([]byte, len(orig)) 79 | size := -1 80 | for i, o := range orig { 81 | if o == 0 { 82 | size = i 83 | break 84 | } 85 | ret[i] = byte(o) 86 | } 87 | if size == -1 { 88 | size = len(orig) 89 | } 90 | 91 | return string(ret[0:size]) 92 | } 93 | -------------------------------------------------------------------------------- /mounts_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package main 5 | 6 | import ( 7 | "golang.org/x/sys/unix" 8 | ) 9 | 10 | func (m *Mount) Stat() unix.Statfs_t { 11 | return m.Metadata.(unix.Statfs_t) 12 | } 13 | 14 | func mounts() ([]Mount, []string, error) { 15 | var ret []Mount 16 | var warnings []string 17 | 18 | count, err := unix.Getfsstat(nil, unix.MNT_WAIT) 19 | if err != nil { 20 | return nil, nil, err 21 | } 22 | fs := make([]unix.Statfs_t, count) 23 | if _, err = unix.Getfsstat(fs, unix.MNT_WAIT); err != nil { 24 | return nil, nil, err 25 | } 26 | 27 | for _, stat := range fs { 28 | opts := "rw" 29 | if stat.Flags&unix.MNT_RDONLY != 0 { 30 | opts = "ro" 31 | } 32 | if stat.Flags&unix.MNT_SYNCHRONOUS != 0 { 33 | opts += ",sync" 34 | } 35 | if stat.Flags&unix.MNT_NOEXEC != 0 { 36 | opts += ",noexec" 37 | } 38 | if stat.Flags&unix.MNT_NOSUID != 0 { 39 | opts += ",nosuid" 40 | } 41 | if stat.Flags&unix.MNT_UNION != 0 { 42 | opts += ",union" 43 | } 44 | if stat.Flags&unix.MNT_ASYNC != 0 { 45 | opts += ",async" 46 | } 47 | if stat.Flags&unix.MNT_DONTBROWSE != 0 { 48 | opts += ",nobrowse" 49 | } 50 | if stat.Flags&unix.MNT_AUTOMOUNTED != 0 { 51 | opts += ",automounted" 52 | } 53 | if stat.Flags&unix.MNT_JOURNALED != 0 { 54 | opts += ",journaled" 55 | } 56 | if stat.Flags&unix.MNT_MULTILABEL != 0 { 57 | opts += ",multilabel" 58 | } 59 | if stat.Flags&unix.MNT_NOATIME != 0 { 60 | opts += ",noatime" 61 | } 62 | if stat.Flags&unix.MNT_NODEV != 0 { 63 | opts += ",nodev" 64 | } 65 | 66 | device := byteToString(stat.Mntfromname[:]) 67 | mountPoint := byteToString(stat.Mntonname[:]) 68 | fsType := byteToString(stat.Fstypename[:]) 69 | 70 | if len(device) == 0 { 71 | continue 72 | } 73 | 74 | d := Mount{ 75 | Device: device, 76 | Mountpoint: mountPoint, 77 | Fstype: fsType, 78 | Type: fsType, 79 | Opts: opts, 80 | Metadata: stat, 81 | Total: stat.Blocks * uint64(stat.Bsize), 82 | Free: stat.Bavail * uint64(stat.Bsize), 83 | Used: (stat.Blocks - stat.Bfree) * uint64(stat.Bsize), 84 | Inodes: stat.Files, 85 | InodesFree: stat.Ffree, 86 | InodesUsed: stat.Files - stat.Ffree, 87 | Blocks: stat.Blocks, 88 | BlockSize: uint64(stat.Bsize), 89 | } 90 | d.DeviceType = deviceType(d) 91 | 92 | ret = append(ret, d) 93 | } 94 | 95 | return ret, warnings, nil 96 | } 97 | -------------------------------------------------------------------------------- /mounts_freebsd.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd 2 | // +build freebsd 3 | 4 | package main 5 | 6 | import ( 7 | "golang.org/x/sys/unix" 8 | ) 9 | 10 | func (m *Mount) Stat() unix.Statfs_t { 11 | return m.Metadata.(unix.Statfs_t) 12 | } 13 | 14 | func mounts() ([]Mount, []string, error) { 15 | var ret []Mount 16 | var warnings []string 17 | 18 | count, err := unix.Getfsstat(nil, unix.MNT_WAIT) 19 | if err != nil { 20 | return nil, nil, err 21 | } 22 | fs := make([]unix.Statfs_t, count) 23 | if _, err = unix.Getfsstat(fs, unix.MNT_WAIT); err != nil { 24 | return nil, nil, err 25 | } 26 | 27 | for _, stat := range fs { 28 | opts := "rw" 29 | if stat.Flags&unix.MNT_RDONLY != 0 { 30 | opts = "ro" 31 | } 32 | if stat.Flags&unix.MNT_SYNCHRONOUS != 0 { 33 | opts += ",sync" 34 | } 35 | if stat.Flags&unix.MNT_NOEXEC != 0 { 36 | opts += ",noexec" 37 | } 38 | if stat.Flags&unix.MNT_NOSUID != 0 { 39 | opts += ",nosuid" 40 | } 41 | if stat.Flags&unix.MNT_UNION != 0 { 42 | opts += ",union" 43 | } 44 | if stat.Flags&unix.MNT_ASYNC != 0 { 45 | opts += ",async" 46 | } 47 | if stat.Flags&unix.MNT_SUIDDIR != 0 { 48 | opts += ",suiddir" 49 | } 50 | if stat.Flags&unix.MNT_SOFTDEP != 0 { 51 | opts += ",softdep" 52 | } 53 | if stat.Flags&unix.MNT_NOSYMFOLLOW != 0 { 54 | opts += ",nosymfollow" 55 | } 56 | if stat.Flags&unix.MNT_GJOURNAL != 0 { 57 | opts += ",gjournal" 58 | } 59 | if stat.Flags&unix.MNT_MULTILABEL != 0 { 60 | opts += ",multilabel" 61 | } 62 | if stat.Flags&unix.MNT_ACLS != 0 { 63 | opts += ",acls" 64 | } 65 | if stat.Flags&unix.MNT_NOATIME != 0 { 66 | opts += ",noatime" 67 | } 68 | if stat.Flags&unix.MNT_NOCLUSTERR != 0 { 69 | opts += ",noclusterr" 70 | } 71 | if stat.Flags&unix.MNT_NOCLUSTERW != 0 { 72 | opts += ",noclusterw" 73 | } 74 | if stat.Flags&unix.MNT_NFS4ACLS != 0 { 75 | opts += ",nfsv4acls" 76 | } 77 | 78 | device := byteToString(stat.Mntfromname[:]) 79 | mountPoint := byteToString(stat.Mntonname[:]) 80 | fsType := byteToString(stat.Fstypename[:]) 81 | 82 | if len(device) == 0 { 83 | continue 84 | } 85 | 86 | d := Mount{ 87 | Device: device, 88 | Mountpoint: mountPoint, 89 | Fstype: fsType, 90 | Type: fsType, 91 | Opts: opts, 92 | Metadata: stat, 93 | Total: (uint64(stat.Blocks) * uint64(stat.Bsize)), 94 | Free: (uint64(stat.Bavail) * uint64(stat.Bsize)), 95 | Used: (uint64(stat.Blocks) - uint64(stat.Bfree)) * uint64(stat.Bsize), 96 | Inodes: stat.Files, 97 | InodesFree: uint64(stat.Ffree), 98 | InodesUsed: stat.Files - uint64(stat.Ffree), 99 | Blocks: uint64(stat.Blocks), 100 | BlockSize: uint64(stat.Bsize), 101 | } 102 | d.DeviceType = deviceType(d) 103 | 104 | ret = append(ret, d) 105 | } 106 | 107 | return ret, warnings, nil 108 | } 109 | -------------------------------------------------------------------------------- /mounts_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "regexp" 11 | "strings" 12 | 13 | "golang.org/x/sys/unix" 14 | ) 15 | 16 | const ( 17 | // A line of self/mountinfo has the following structure: 18 | // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue 19 | // (0) (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) 20 | // 21 | // (0) mount ID: unique identifier of the mount (may be reused after umount). 22 | //mountinfoMountID = 0 23 | // (1) parent ID: ID of parent (or of self for the top of the mount tree). 24 | //mountinfoParentID = 1 25 | // (2) major:minor: value of st_dev for files on filesystem. 26 | //mountinfoMajorMinor = 2 27 | // (3) root: root of the mount within the filesystem. 28 | //mountinfoRoot = 3 29 | // (4) mount point: mount point relative to the process's root. 30 | mountinfoMountPoint = 4 31 | // (5) mount options: per mount options. 32 | mountinfoMountOpts = 5 33 | // (6) optional fields: zero or more fields terminated by "-". 34 | mountinfoOptionalFields = 6 35 | // (7) separator between optional fields. 36 | //mountinfoSeparator = 7 37 | // (8) filesystem type: name of filesystem of the form. 38 | mountinfoFsType = 8 39 | // (9) mount source: filesystem specific information or "none". 40 | mountinfoMountSource = 9 41 | // (10) super options: per super block options. 42 | //mountinfoSuperOptions = 10 43 | ) 44 | 45 | // Stat returns the mountpoint's stat information. 46 | func (m *Mount) Stat() unix.Statfs_t { 47 | return m.Metadata.(unix.Statfs_t) 48 | } 49 | 50 | func mounts() ([]Mount, []string, error) { 51 | var warnings []string 52 | 53 | filename := "/proc/self/mountinfo" 54 | lines, err := readLines(filename) 55 | if err != nil { 56 | return nil, nil, err 57 | } 58 | 59 | ret := make([]Mount, 0, len(lines)) 60 | for _, line := range lines { 61 | nb, fields := parseMountInfoLine(line) 62 | if nb == 0 { 63 | continue 64 | } 65 | 66 | // if the number of fields does not match the structure of mountinfo, 67 | // emit a warning and ignore the line. 68 | if nb < 10 || nb > 11 { 69 | warnings = append(warnings, fmt.Sprintf("found invalid mountinfo line: %s", line)) 70 | continue 71 | } 72 | 73 | // blockDeviceID := fields[mountinfoMountID] 74 | mountPoint := fields[mountinfoMountPoint] 75 | mountOpts := fields[mountinfoMountOpts] 76 | fstype := fields[mountinfoFsType] 77 | device := fields[mountinfoMountSource] 78 | 79 | var stat unix.Statfs_t 80 | err := unix.Statfs(mountPoint, &stat) 81 | if err != nil { 82 | if err != os.ErrPermission { 83 | warnings = append(warnings, fmt.Sprintf("%s: %s", mountPoint, err)) 84 | continue 85 | } 86 | 87 | stat = unix.Statfs_t{} 88 | } 89 | 90 | d := Mount{ 91 | Device: device, 92 | Mountpoint: mountPoint, 93 | Fstype: fstype, 94 | Type: fsTypeMap[int64(stat.Type)], //nolint:unconvert 95 | Opts: mountOpts, 96 | Metadata: stat, 97 | Total: (uint64(stat.Blocks) * uint64(stat.Bsize)), //nolint:unconvert 98 | Free: (uint64(stat.Bavail) * uint64(stat.Bsize)), //nolint:unconvert 99 | Used: (uint64(stat.Blocks) - uint64(stat.Bfree)) * uint64(stat.Bsize), //nolint:unconvert 100 | Inodes: stat.Files, 101 | InodesFree: stat.Ffree, 102 | InodesUsed: stat.Files - stat.Ffree, 103 | Blocks: uint64(stat.Blocks), //nolint:unconvert 104 | BlockSize: uint64(stat.Bsize), 105 | } 106 | d.DeviceType = deviceType(d) 107 | 108 | // resolve /dev/mapper/* device names 109 | if strings.HasPrefix(d.Device, "/dev/mapper/") { 110 | re := regexp.MustCompile(`^\/dev\/mapper\/(.*)-(.*)`) 111 | match := re.FindAllStringSubmatch(d.Device, -1) 112 | if len(match) > 0 && len(match[0]) == 3 { 113 | d.Device = filepath.Join("/dev", match[0][1], match[0][2]) 114 | } 115 | } 116 | 117 | ret = append(ret, d) 118 | } 119 | 120 | return ret, warnings, nil 121 | } 122 | 123 | // parseMountInfoLine parses a line of /proc/self/mountinfo and returns the 124 | // amount of parsed fields and their values. 125 | func parseMountInfoLine(line string) (int, [11]string) { 126 | var fields [11]string 127 | 128 | if len(line) == 0 || line[0] == '#' { 129 | // ignore comments and empty lines 130 | return 0, fields 131 | } 132 | 133 | var i int 134 | for _, f := range strings.Fields(line) { 135 | // when parsing the optional fields, loop until we find the separator 136 | if i == mountinfoOptionalFields { 137 | // (6) optional fields: zero or more fields of the form 138 | // "tag[:value]"; see below. 139 | // (7) separator: the end of the optional fields is marked 140 | // by a single hyphen. 141 | if f != "-" { 142 | if fields[i] == "" { 143 | fields[i] += f 144 | } else { 145 | fields[i] += " " + f 146 | } 147 | 148 | // keep reading until we reach the separator 149 | continue 150 | } 151 | 152 | // separator found, continue parsing 153 | i++ 154 | } 155 | 156 | switch i { 157 | case mountinfoMountPoint: 158 | fallthrough 159 | case mountinfoMountSource: 160 | fallthrough 161 | case mountinfoFsType: 162 | fields[i] = unescapeFstab(f) 163 | 164 | default: 165 | fields[i] = f 166 | } 167 | 168 | i++ 169 | } 170 | 171 | return i, fields 172 | } 173 | -------------------------------------------------------------------------------- /mounts_linux_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package main 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestGetFields(t *testing.T) { 12 | var tt = []struct { 13 | input string 14 | number int 15 | expected [11]string 16 | }{ 17 | // Empty lines 18 | { 19 | input: "", 20 | number: 0, 21 | }, 22 | { 23 | input: " ", 24 | number: 0, 25 | }, 26 | { 27 | input: " ", 28 | number: 0, 29 | }, 30 | { 31 | input: " ", 32 | number: 0, 33 | }, 34 | 35 | // Comments 36 | { 37 | input: "#", 38 | number: 0, 39 | }, 40 | { 41 | input: "# ", 42 | number: 0, 43 | }, 44 | { 45 | input: "# ", 46 | number: 0, 47 | }, 48 | { 49 | input: "# I'm a lazy dog", 50 | number: 0, 51 | }, 52 | 53 | // Bad fields 54 | { 55 | input: "1 2", 56 | number: 2, 57 | expected: [11]string{"1", "2"}, 58 | }, 59 | { 60 | input: "1 2", 61 | number: 2, 62 | expected: [11]string{"1", "2"}, 63 | }, 64 | { 65 | input: "1 2 3", 66 | number: 3, 67 | expected: [11]string{"1", "2", "3"}, 68 | }, 69 | { 70 | input: "1 2 3 4", 71 | number: 4, 72 | expected: [11]string{"1", "2", "3", "4"}, 73 | }, 74 | 75 | // No optional separator or no options 76 | { 77 | input: "1 2 3 4 5 6 7 NotASeparator 9 10 11", 78 | number: 6, 79 | expected: [11]string{"1", "2", "3", "4", "5", "6", "7 NotASeparator 9 10 11"}, 80 | }, 81 | { 82 | input: "1 2 3 4 5 6 7 8 9 10 11", 83 | number: 6, 84 | expected: [11]string{"1", "2", "3", "4", "5", "6", "7 8 9 10 11"}, 85 | }, 86 | { 87 | input: "1 2 3 4 5 6 - 9 10 11", 88 | number: 11, 89 | expected: [11]string{"1", "2", "3", "4", "5", "6", "", "-", "9", "10", "11"}, 90 | }, 91 | 92 | // Normal mount table line 93 | { 94 | input: "22 27 0:21 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw", 95 | number: 11, 96 | expected: [11]string{"22", "27", "0:21", "/", "/proc", "rw,nosuid,nodev,noexec,relatime", "shared:5", "-", "proc", "proc", "rw"}, 97 | }, 98 | { 99 | input: "31 23 0:27 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:9 - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot", 100 | number: 11, 101 | expected: [11]string{"31", "23", "0:27", "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", "shared:9", "-", "cgroup2", "cgroup2", "rw,nsdelegate,memory_recursiveprot"}, 102 | }, 103 | { 104 | input: "40 27 0:33 / /tmp rw,nosuid,nodev shared:18 - tmpfs tmpfs", 105 | number: 10, 106 | expected: [11]string{"40", "27", "0:33", "/", "/tmp", "rw,nosuid,nodev", "shared:18", "-", "tmpfs", "tmpfs"}, 107 | }, 108 | { 109 | input: "40 27 0:33 / /tmp rw,nosuid,nodev shared:18 shared:22 - tmpfs tmpfs", 110 | number: 10, 111 | expected: [11]string{"40", "27", "0:33", "/", "/tmp", "rw,nosuid,nodev", "shared:18 shared:22", "-", "tmpfs", "tmpfs"}, 112 | }, 113 | { 114 | input: "50 27 0:33 / /tmp rw,nosuid,nodev - tmpfs tmpfs", 115 | number: 10, 116 | expected: [11]string{"50", "27", "0:33", "/", "/tmp", "rw,nosuid,nodev", "", "-", "tmpfs", "tmpfs"}, 117 | }, 118 | 119 | // Exceptional mount table lines 120 | { 121 | input: "328 27 0:73 / /mnt/a rw,relatime shared:206 - tmpfs - rw,inode64", 122 | number: 11, 123 | expected: [11]string{"328", "27", "0:73", "/", "/mnt/a", "rw,relatime", "shared:206", "-", "tmpfs", "-", "rw,inode64"}, 124 | }, 125 | { 126 | input: "330 27 0:73 / /mnt/a rw,relatime shared:206 - tmpfs 👾 rw,inode64", 127 | number: 11, 128 | expected: [11]string{"330", "27", "0:73", "/", "/mnt/a", "rw,relatime", "shared:206", "-", "tmpfs", "👾", "rw,inode64"}, 129 | }, 130 | { 131 | input: "335 27 0:73 / /mnt/👾 rw,relatime shared:206 - tmpfs 👾 rw,inode64", 132 | number: 11, 133 | expected: [11]string{"335", "27", "0:73", "/", "/mnt/👾", "rw,relatime", "shared:206", "-", "tmpfs", "👾", "rw,inode64"}, 134 | }, 135 | { 136 | input: "509 27 0:78 / /mnt/- rw,relatime shared:223 - tmpfs 👾 rw,inode64", 137 | number: 11, 138 | expected: [11]string{"509", "27", "0:78", "/", "/mnt/-", "rw,relatime", "shared:223", "-", "tmpfs", "👾", "rw,inode64"}, 139 | }, 140 | { 141 | input: "362 27 0:76 / /mnt/a\\040b rw,relatime shared:215 - tmpfs 👾 rw,inode64", 142 | number: 11, 143 | expected: [11]string{"362", "27", "0:76", "/", "/mnt/a b", "rw,relatime", "shared:215", "-", "tmpfs", "👾", "rw,inode64"}, 144 | }, 145 | { 146 | input: "1 2 3:3 / /mnt/\\011 rw shared:7 - tmpfs - rw,inode64", 147 | number: 11, 148 | expected: [11]string{"1", "2", "3:3", "/", "/mnt/\t", "rw", "shared:7", "-", "tmpfs", "-", "rw,inode64"}, 149 | }, 150 | { 151 | input: "11 2 3:3 / /mnt/a\\012b rw shared:7 - tmpfs - rw,inode64", 152 | number: 11, 153 | expected: [11]string{"11", "2", "3:3", "/", "/mnt/a\nb", "rw", "shared:7", "-", "tmpfs", "-", "rw,inode64"}, 154 | }, 155 | { 156 | input: "111 2 3:3 / /mnt/a\\134b rw shared:7 - tmpfs - rw,inode64", 157 | number: 11, 158 | expected: [11]string{"111", "2", "3:3", "/", "/mnt/a\\b", "rw", "shared:7", "-", "tmpfs", "-", "rw,inode64"}, 159 | }, 160 | { 161 | input: "1111 2 3:3 / /mnt/a\\042b rw shared:7 - tmpfs - rw,inode64", 162 | number: 11, 163 | expected: [11]string{"1111", "2", "3:3", "/", "/mnt/a\"b", "rw", "shared:7", "-", "tmpfs", "-", "rw,inode64"}, 164 | }, 165 | } 166 | 167 | for _, tc := range tt { 168 | nb, actual := parseMountInfoLine(tc.input) 169 | if nb != tc.number || !reflect.DeepEqual(actual, tc.expected) { 170 | t.Errorf("\nparseMountInfoLine(%q) == \n(%d) %q, \nexpected (%d) %q", tc.input, nb, actual, tc.number, tc.expected) 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /mounts_openbsd.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd 2 | // +build openbsd 3 | 4 | package main 5 | 6 | import ( 7 | "golang.org/x/sys/unix" 8 | ) 9 | 10 | func (m *Mount) Stat() unix.Statfs_t { 11 | return m.Metadata.(unix.Statfs_t) 12 | } 13 | 14 | func mounts() ([]Mount, []string, error) { 15 | var ret []Mount 16 | var warnings []string 17 | 18 | count, err := unix.Getfsstat(nil, unix.MNT_WAIT) 19 | if err != nil { 20 | return nil, nil, err 21 | } 22 | fs := make([]unix.Statfs_t, count) 23 | if _, err = unix.Getfsstat(fs, unix.MNT_WAIT); err != nil { 24 | return nil, nil, err 25 | } 26 | 27 | for _, stat := range fs { 28 | opts := "rw" 29 | if stat.F_flags&unix.MNT_RDONLY != 0 { 30 | opts = "ro" 31 | } 32 | if stat.F_flags&unix.MNT_SYNCHRONOUS != 0 { 33 | opts += ",sync" 34 | } 35 | if stat.F_flags&unix.MNT_NOEXEC != 0 { 36 | opts += ",noexec" 37 | } 38 | if stat.F_flags&unix.MNT_NOSUID != 0 { 39 | opts += ",nosuid" 40 | } 41 | if stat.F_flags&unix.MNT_NODEV != 0 { 42 | opts += ",nodev" 43 | } 44 | if stat.F_flags&unix.MNT_ASYNC != 0 { 45 | opts += ",async" 46 | } 47 | if stat.F_flags&unix.MNT_SOFTDEP != 0 { 48 | opts += ",softdep" 49 | } 50 | if stat.F_flags&unix.MNT_NOATIME != 0 { 51 | opts += ",noatime" 52 | } 53 | if stat.F_flags&unix.MNT_WXALLOWED != 0 { 54 | opts += ",wxallowed" 55 | } 56 | 57 | device := byteToString(stat.F_mntfromname[:]) 58 | mountPoint := byteToString(stat.F_mntonname[:]) 59 | fsType := byteToString(stat.F_fstypename[:]) 60 | 61 | if len(device) == 0 { 62 | continue 63 | } 64 | 65 | d := Mount{ 66 | Device: device, 67 | Mountpoint: mountPoint, 68 | Fstype: fsType, 69 | Type: fsType, 70 | Opts: opts, 71 | Metadata: stat, 72 | Total: (uint64(stat.F_blocks) * uint64(stat.F_bsize)), 73 | Free: (uint64(stat.F_bavail) * uint64(stat.F_bsize)), 74 | Used: (uint64(stat.F_blocks) - uint64(stat.F_bfree)) * uint64(stat.F_bsize), 75 | Inodes: stat.F_files, 76 | InodesFree: uint64(stat.F_ffree), 77 | InodesUsed: stat.F_files - uint64(stat.F_ffree), 78 | Blocks: uint64(stat.F_blocks), 79 | BlockSize: uint64(stat.F_bsize), 80 | } 81 | d.DeviceType = deviceType(d) 82 | 83 | ret = append(ret, d) 84 | } 85 | 86 | return ret, warnings, nil 87 | } 88 | -------------------------------------------------------------------------------- /mounts_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "golang.org/x/sys/windows" 9 | "math" 10 | "path/filepath" 11 | "strings" 12 | "syscall" 13 | "unsafe" 14 | ) 15 | 16 | // Local devices 17 | const ( 18 | guidBufLen = windows.MAX_PATH + 1 19 | volumeNameBufLen = windows.MAX_PATH + 1 20 | rootPathBufLen = windows.MAX_PATH + 1 21 | fileSystemBufLen = windows.MAX_PATH + 1 22 | ) 23 | 24 | func getMountPoint(guidBuf []uint16) (mountPoint string, err error) { 25 | var rootPathLen uint32 26 | rootPathBuf := make([]uint16, rootPathBufLen) 27 | 28 | err = windows.GetVolumePathNamesForVolumeName(&guidBuf[0], &rootPathBuf[0], rootPathBufLen*2, &rootPathLen) 29 | if err != nil && err.(windows.Errno) == windows.ERROR_MORE_DATA { 30 | // Retry if buffer size is too small 31 | rootPathBuf = make([]uint16, (rootPathLen+1)/2) 32 | err = windows.GetVolumePathNamesForVolumeName( 33 | &guidBuf[0], &rootPathBuf[0], rootPathLen, &rootPathLen) 34 | } 35 | return windows.UTF16ToString(rootPathBuf), err 36 | } 37 | 38 | func getVolumeInfo(guidOrMountPointBuf []uint16) (volumeName string, fsType string, err error) { 39 | volumeNameBuf := make([]uint16, volumeNameBufLen) 40 | fsTypeBuf := make([]uint16, fileSystemBufLen) 41 | 42 | err = windows.GetVolumeInformation(&guidOrMountPointBuf[0], &volumeNameBuf[0], volumeNameBufLen*2, 43 | nil, nil, nil, 44 | &fsTypeBuf[0], fileSystemBufLen*2) 45 | 46 | return windows.UTF16ToString(volumeNameBuf), windows.UTF16ToString(fsTypeBuf), err 47 | } 48 | 49 | func getSpaceInfo(guidOrMountPointBuf []uint16) (totalBytes uint64, freeBytes uint64, err error) { 50 | err = windows.GetDiskFreeSpaceEx(&guidOrMountPointBuf[0], nil, &totalBytes, &freeBytes) 51 | return 52 | } 53 | 54 | func getClusterInfo(guidOrMountPointBuf []uint16) (totalClusters uint32, clusterSize uint32, err error) { 55 | var sectorsPerCluster uint32 56 | var bytesPerSector uint32 57 | err = GetDiskFreeSpace(&guidOrMountPointBuf[0], §orsPerCluster, &bytesPerSector, nil, &totalClusters) 58 | clusterSize = bytesPerSector * sectorsPerCluster 59 | return 60 | } 61 | 62 | func getMount(guidOrMountPointBuf []uint16, isGUID bool) (m Mount, skip bool, warnings []string) { 63 | var err error 64 | guidOrMountPoint := windows.UTF16ToString(guidOrMountPointBuf) 65 | 66 | mountPoint := guidOrMountPoint 67 | if isGUID { 68 | mountPoint, err = getMountPoint(guidOrMountPointBuf) 69 | if err != nil { 70 | warnings = append(warnings, fmt.Sprintf("%s: %s", guidOrMountPoint, err)) 71 | } 72 | // Skip unmounted volumes 73 | if len(mountPoint) == 0 { 74 | skip = true 75 | return 76 | } 77 | } 78 | 79 | // Get volume name & filesystem type 80 | volumeName, fsType, err := getVolumeInfo(guidOrMountPointBuf) 81 | if err != nil { 82 | warnings = append(warnings, fmt.Sprintf("%s: %s", guidOrMountPoint, err)) 83 | } 84 | 85 | // Get space info 86 | totalBytes, freeBytes, err := getSpaceInfo(guidOrMountPointBuf) 87 | if err != nil { 88 | warnings = append(warnings, fmt.Sprintf("%s: %s", guidOrMountPoint, err)) 89 | } 90 | 91 | // Get cluster info 92 | totalClusters, clusterSize, err := getClusterInfo(guidOrMountPointBuf) 93 | if err != nil { 94 | warnings = append(warnings, fmt.Sprintf("%s: %s", guidOrMountPoint, err)) 95 | } 96 | 97 | m = Mount{ 98 | Device: volumeName, 99 | Mountpoint: mountPoint, 100 | Fstype: fsType, 101 | Type: fsType, 102 | Opts: "", 103 | Total: totalBytes, 104 | Free: freeBytes, 105 | Used: totalBytes - freeBytes, 106 | Blocks: uint64(totalClusters), 107 | BlockSize: uint64(clusterSize), 108 | } 109 | m.DeviceType = deviceType(m) 110 | return 111 | } 112 | 113 | func getMountFromGUID(guidBuf []uint16) (m Mount, skip bool, warnings []string) { 114 | m, skip, warnings = getMount(guidBuf, true) 115 | 116 | // Use GUID as volume name if no label was set 117 | if len(m.Device) == 0 { 118 | m.Device = windows.UTF16ToString(guidBuf) 119 | } 120 | 121 | return 122 | } 123 | 124 | func getMountFromMountPoint(mountPointBuf []uint16) (m Mount, warnings []string) { 125 | m, _, warnings = getMount(mountPointBuf, false) 126 | 127 | // Use mount point as volume name if no label was set 128 | if len(m.Device) == 0 { 129 | m.Device = windows.UTF16ToString(mountPointBuf) 130 | } 131 | 132 | return m, warnings 133 | } 134 | 135 | func appendLocalMounts(mounts []Mount, warnings []string) ([]Mount, []string, error) { 136 | guidBuf := make([]uint16, guidBufLen) 137 | 138 | hFindVolume, err := windows.FindFirstVolume(&guidBuf[0], guidBufLen*2) 139 | if err != nil { 140 | return mounts, warnings, err 141 | } 142 | 143 | VolumeLoop: 144 | for ; ; err = windows.FindNextVolume(hFindVolume, &guidBuf[0], guidBufLen*2) { 145 | if err != nil { 146 | switch err.(windows.Errno) { 147 | case windows.ERROR_NO_MORE_FILES: 148 | break VolumeLoop 149 | default: 150 | warnings = append(warnings, fmt.Sprintf("%s: %s", windows.UTF16ToString(guidBuf), err)) 151 | continue VolumeLoop 152 | } 153 | } 154 | 155 | if m, skip, w := getMountFromGUID(guidBuf); !skip { 156 | mounts = append(mounts, m) 157 | warnings = append(warnings, w...) 158 | } 159 | } 160 | 161 | if err = windows.FindVolumeClose(hFindVolume); err != nil { 162 | warnings = append(warnings, fmt.Sprintf("%s", err)) 163 | } 164 | return mounts, warnings, nil 165 | } 166 | 167 | // Network devices 168 | func getMountFromNetResource(netResource NetResource) (m Mount, warnings []string) { 169 | mountPoint := windows.UTF16PtrToString(netResource.LocalName) 170 | if !strings.HasSuffix(mountPoint, string(filepath.Separator)) { 171 | mountPoint += string(filepath.Separator) 172 | } 173 | mountPointBuf := windows.StringToUTF16(mountPoint) 174 | 175 | m, _, warnings = getMount(mountPointBuf, false) 176 | 177 | // Use remote name as volume name if no label was set 178 | if len(m.Device) == 0 { 179 | m.Device = windows.UTF16PtrToString(netResource.RemoteName) 180 | } 181 | 182 | return 183 | } 184 | 185 | func appendNetworkMounts(mounts []Mount, warnings []string) ([]Mount, []string, error) { 186 | hEnumResource, err := WNetOpenEnum(RESOURCE_CONNECTED, RESOURCETYPE_DISK, RESOURCEUSAGE_CONNECTABLE, nil) 187 | if err != nil { 188 | return mounts, warnings, err 189 | } 190 | 191 | EnumLoop: 192 | for { 193 | // Reference: https://docs.microsoft.com/en-us/windows/win32/wnet/enumerating-network-resources 194 | var nrBuf [16384]byte 195 | count := uint32(math.MaxUint32) 196 | size := uint32(len(nrBuf)) 197 | if err := WNetEnumResource(hEnumResource, &count, &nrBuf[0], &size); err != nil { 198 | switch err.(windows.Errno) { 199 | case windows.ERROR_NO_MORE_ITEMS: 200 | break EnumLoop 201 | default: 202 | warnings = append(warnings, err.Error()) 203 | break EnumLoop 204 | } 205 | } 206 | 207 | for i := uint32(0); i < count; i++ { 208 | nr := (*NetResource)(unsafe.Pointer(&nrBuf[uintptr(i)*NetResourceSize])) 209 | m, w := getMountFromNetResource(*nr) 210 | mounts = append(mounts, m) 211 | warnings = append(warnings, w...) 212 | } 213 | } 214 | 215 | if err = WNetCloseEnum(hEnumResource); err != nil { 216 | warnings = append(warnings, fmt.Sprintf("%s", err)) 217 | } 218 | return mounts, warnings, nil 219 | } 220 | 221 | func mountPointAlreadyPresent(mounts []Mount, mountPoint string) bool { 222 | for _, m := range mounts { 223 | if m.Mountpoint == mountPoint { 224 | return true 225 | } 226 | } 227 | 228 | return false 229 | } 230 | 231 | func appendLogicalDrives(mounts []Mount, warnings []string) ([]Mount, []string) { 232 | driveBitmap, err := windows.GetLogicalDrives() 233 | if err != nil { 234 | warnings = append(warnings, fmt.Sprintf("GetLogicalDrives(): %s", err)) 235 | return mounts, warnings 236 | } 237 | 238 | for drive := 'A'; drive <= 'Z'; drive, driveBitmap = drive+1, driveBitmap>>1 { 239 | if driveBitmap&0x1 == 0 { 240 | continue 241 | } 242 | 243 | mountPoint := fmt.Sprintf("%c:\\", drive) 244 | if mountPointAlreadyPresent(mounts, mountPoint) { 245 | continue 246 | } 247 | 248 | mountPointBuf := windows.StringToUTF16(mountPoint) 249 | m, w := getMountFromMountPoint(mountPointBuf) 250 | mounts = append(mounts, m) 251 | warnings = append(warnings, w...) 252 | } 253 | 254 | return mounts, warnings 255 | } 256 | 257 | func mounts() (ret []Mount, warnings []string, err error) { 258 | ret = make([]Mount, 0) 259 | 260 | // Local devices 261 | if ret, warnings, err = appendLocalMounts(ret, warnings); err != nil { 262 | return 263 | } 264 | 265 | // Network devices 266 | if ret, warnings, err = appendNetworkMounts(ret, warnings); err != nil { 267 | return 268 | } 269 | 270 | // Logical devices (from GetLogicalDrives bitflag) 271 | // Check any possible logical drives, in case of some special virtual devices, such as RAM disk 272 | ret, warnings = appendLogicalDrives(ret, warnings) 273 | 274 | return ret, warnings, nil 275 | } 276 | 277 | // Windows API 278 | const ( 279 | // Windows Networking const 280 | // Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/nf-winnetwk-wnetopenenumw 281 | RESOURCE_CONNECTED = 0x00000001 282 | RESOURCE_GLOBALNET = 0x00000002 283 | RESOURCE_REMEMBERED = 0x00000003 284 | RESOURCE_RECENT = 0x00000004 285 | RESOURCE_CONTEXT = 0x00000005 286 | 287 | RESOURCETYPE_ANY = 0x00000000 288 | RESOURCETYPE_DISK = 0x00000001 289 | RESOURCETYPE_PRINT = 0x00000002 290 | RESOURCETYPE_RESERVED = 0x00000008 291 | RESOURCETYPE_UNKNOWN = 0xFFFFFFFF 292 | 293 | RESOURCEUSAGE_CONNECTABLE = 0x00000001 294 | RESOURCEUSAGE_CONTAINER = 0x00000002 295 | RESOURCEUSAGE_NOLOCALDEVICE = 0x00000004 296 | RESOURCEUSAGE_SIBLING = 0x00000008 297 | RESOURCEUSAGE_ATTACHED = 0x00000010 298 | RESOURCEUSAGE_ALL = RESOURCEUSAGE_CONNECTABLE | RESOURCEUSAGE_CONTAINER | RESOURCEUSAGE_ATTACHED 299 | RESOURCEUSAGE_RESERVED = 0x80000000 300 | ) 301 | 302 | var ( 303 | // Windows syscall 304 | modmpr = windows.NewLazySystemDLL("mpr.dll") 305 | modkernel32 = windows.NewLazySystemDLL("kernel32.dll") 306 | 307 | procWNetOpenEnumW = modmpr.NewProc("WNetOpenEnumW") 308 | procWNetCloseEnum = modmpr.NewProc("WNetCloseEnum") 309 | procWNetEnumResourceW = modmpr.NewProc("WNetEnumResourceW") 310 | procGetDiskFreeSpaceW = modkernel32.NewProc("GetDiskFreeSpaceW") 311 | 312 | NetResourceSize = unsafe.Sizeof(NetResource{}) 313 | ) 314 | 315 | // Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/ns-winnetwk-netresourcew 316 | type NetResource struct { 317 | Scope uint32 318 | Type uint32 319 | DisplayType uint32 320 | Usage uint32 321 | LocalName *uint16 322 | RemoteName *uint16 323 | Comment *uint16 324 | Provider *uint16 325 | } 326 | 327 | // Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/nf-winnetwk-wnetopenenumw 328 | func WNetOpenEnum(scope uint32, resourceType uint32, usage uint32, resource *NetResource) (handle windows.Handle, err error) { 329 | r1, _, e1 := syscall.Syscall6(procWNetOpenEnumW.Addr(), 5, uintptr(scope), uintptr(resourceType), uintptr(usage), uintptr(unsafe.Pointer(resource)), uintptr(unsafe.Pointer(&handle)), 0) 330 | if r1 != windows.NO_ERROR { 331 | if e1 != 0 { 332 | err = e1 333 | } else { 334 | err = syscall.EINVAL 335 | } 336 | } 337 | return 338 | } 339 | 340 | // Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/nf-winnetwk-wnetenumresourcew 341 | func WNetEnumResource(enumResource windows.Handle, count *uint32, buffer *byte, bufferSize *uint32) (err error) { 342 | r1, _, e1 := syscall.Syscall6(procWNetEnumResourceW.Addr(), 4, uintptr(enumResource), uintptr(unsafe.Pointer(count)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(bufferSize)), 0, 0) 343 | if r1 != windows.NO_ERROR { 344 | if e1 != 0 { 345 | err = e1 346 | } else { 347 | err = syscall.EINVAL 348 | } 349 | } 350 | return 351 | } 352 | 353 | // Reference: https://docs.microsoft.com/en-us/windows/win32/api/winnetwk/nf-winnetwk-wnetcloseenum 354 | func WNetCloseEnum(enumResource windows.Handle) (err error) { 355 | r1, _, e1 := syscall.Syscall(procWNetCloseEnum.Addr(), 1, uintptr(enumResource), 0, 0) 356 | if r1 != windows.NO_ERROR { 357 | if e1 != 0 { 358 | err = e1 359 | } else { 360 | err = syscall.EINVAL 361 | } 362 | } 363 | return 364 | } 365 | 366 | // Reference: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdiskfreespacew 367 | func GetDiskFreeSpace(directoryName *uint16, sectorsPerCluster *uint32, bytesPerSector *uint32, numberOfFreeClusters *uint32, totalNumberOfClusters *uint32) (err error) { 368 | 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) 369 | if r1 == 0 { 370 | if e1 != 0 { 371 | err = e1 372 | } else { 373 | err = syscall.EINVAL 374 | } 375 | } 376 | return 377 | } 378 | -------------------------------------------------------------------------------- /style.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/mattn/go-runewidth" 4 | 5 | func defaultStyleName() string { 6 | /* 7 | Due to a bug in github.com/mattn/go-runewidth v0.0.9, the width of unicode rune(such as '╭') could not be correctly 8 | calculated. Degrade to ascii to prevent broken table structure. Remove this once the bug is fixed. 9 | */ 10 | if runewidth.RuneWidth('╭') > 1 { 11 | return "ascii" 12 | } 13 | 14 | return "unicode" 15 | } 16 | -------------------------------------------------------------------------------- /table.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/jedib0t/go-pretty/v6/table" 11 | "github.com/jedib0t/go-pretty/v6/text" 12 | "github.com/muesli/termenv" 13 | ) 14 | 15 | // TableOptions contains all options for the table. 16 | type TableOptions struct { 17 | Columns []int 18 | SortBy int 19 | Style table.Style 20 | } 21 | 22 | // Column defines a column. 23 | type Column struct { 24 | ID string 25 | Name string 26 | SortIndex int 27 | Width int 28 | } 29 | 30 | // "Mounted on", "Size", "Used", "Avail", "Use%", "Inodes", "IUsed", "IAvail", "IUse%", "Type", "Filesystem" 31 | // mountpoint, size, used, avail, usage, inodes, inodes_used, inodes_avail, inodes_usage, type, filesystem 32 | var columns = []Column{ 33 | {ID: "mountpoint", Name: "Mounted on", SortIndex: 1}, 34 | {ID: "size", Name: "Size", SortIndex: 12, Width: 7}, 35 | {ID: "used", Name: "Used", SortIndex: 13, Width: 7}, 36 | {ID: "avail", Name: "Avail", SortIndex: 14, Width: 7}, 37 | {ID: "usage", Name: "Use%", SortIndex: 15, Width: 6}, 38 | {ID: "inodes", Name: "Inodes", SortIndex: 16, Width: 7}, 39 | {ID: "inodes_used", Name: "IUsed", SortIndex: 17, Width: 7}, 40 | {ID: "inodes_avail", Name: "IAvail", SortIndex: 18, Width: 7}, 41 | {ID: "inodes_usage", Name: "IUse%", SortIndex: 19, Width: 6}, 42 | {ID: "type", Name: "Type", SortIndex: 10}, 43 | {ID: "filesystem", Name: "Filesystem", SortIndex: 11}, 44 | } 45 | 46 | // printTable prints an individual table of mounts. 47 | func printTable(title string, m []Mount, opts TableOptions) { 48 | tab := table.NewWriter() 49 | tab.SetAllowedRowLength(int(*width)) 50 | tab.SetOutputMirror(os.Stdout) 51 | tab.Style().Options.SeparateColumns = true 52 | tab.SetStyle(opts.Style) 53 | 54 | if barWidth() > 0 { 55 | columns[4].Width = barWidth() + 7 56 | columns[8].Width = barWidth() + 7 57 | } 58 | twidth := tableWidth(opts.Columns, tab.Style().Options.SeparateColumns) 59 | 60 | tab.SetColumnConfigs([]table.ColumnConfig{ 61 | {Number: 1, Hidden: !inColumns(opts.Columns, 1), WidthMax: int(float64(twidth) * 0.4)}, 62 | {Number: 2, Hidden: !inColumns(opts.Columns, 2), Transformer: sizeTransformer, Align: text.AlignRight, AlignHeader: text.AlignRight}, 63 | {Number: 3, Hidden: !inColumns(opts.Columns, 3), Transformer: sizeTransformer, Align: text.AlignRight, AlignHeader: text.AlignRight}, 64 | {Number: 4, Hidden: !inColumns(opts.Columns, 4), Transformer: spaceTransformer, Align: text.AlignRight, AlignHeader: text.AlignRight}, 65 | {Number: 5, Hidden: !inColumns(opts.Columns, 5), Transformer: barTransformer, AlignHeader: text.AlignCenter}, 66 | {Number: 6, Hidden: !inColumns(opts.Columns, 6), Align: text.AlignRight, AlignHeader: text.AlignRight}, 67 | {Number: 7, Hidden: !inColumns(opts.Columns, 7), Align: text.AlignRight, AlignHeader: text.AlignRight}, 68 | {Number: 8, Hidden: !inColumns(opts.Columns, 8), Align: text.AlignRight, AlignHeader: text.AlignRight}, 69 | {Number: 9, Hidden: !inColumns(opts.Columns, 9), Transformer: barTransformer, AlignHeader: text.AlignCenter}, 70 | {Number: 10, Hidden: !inColumns(opts.Columns, 10), WidthMax: int(float64(twidth) * 0.2)}, 71 | {Number: 11, Hidden: !inColumns(opts.Columns, 11), WidthMax: int(float64(twidth) * 0.4)}, 72 | {Number: 12, Hidden: true}, // sortBy helper for size 73 | {Number: 13, Hidden: true}, // sortBy helper for used 74 | {Number: 14, Hidden: true}, // sortBy helper for avail 75 | {Number: 15, Hidden: true}, // sortBy helper for usage 76 | {Number: 16, Hidden: true}, // sortBy helper for inodes size 77 | {Number: 17, Hidden: true}, // sortBy helper for inodes used 78 | {Number: 18, Hidden: true}, // sortBy helper for inodes avail 79 | {Number: 19, Hidden: true}, // sortBy helper for inodes usage 80 | }) 81 | 82 | headers := table.Row{} 83 | for _, v := range columns { 84 | headers = append(headers, v.Name) 85 | } 86 | tab.AppendHeader(headers) 87 | 88 | for _, v := range m { 89 | // spew.Dump(v) 90 | 91 | var usage, inodeUsage float64 92 | if v.Total > 0 { 93 | usage = float64(v.Used) / float64(v.Total) 94 | if usage > 1.0 { 95 | usage = 1.0 96 | } 97 | } 98 | if v.Inodes > 0 { 99 | inodeUsage = float64(v.InodesUsed) / float64(v.Inodes) 100 | if inodeUsage > 1.0 { 101 | inodeUsage = 1.0 102 | } 103 | } 104 | 105 | tab.AppendRow([]interface{}{ 106 | termenv.String(v.Mountpoint).Foreground(theme.colorBlue), // mounted on 107 | v.Total, // size 108 | v.Used, // used 109 | v.Free, // avail 110 | usage, // use% 111 | v.Inodes, // inodes 112 | v.InodesUsed, // inodes used 113 | v.InodesFree, // inodes avail 114 | inodeUsage, // inodes use% 115 | termenv.String(v.Fstype).Foreground(theme.colorGray), // type 116 | termenv.String(v.Device).Foreground(theme.colorGray), // filesystem 117 | v.Total, // size sorting helper 118 | v.Used, // used sorting helper 119 | v.Free, // avail sorting helper 120 | usage, // use% sorting helper 121 | v.Inodes, // inodes sorting helper 122 | v.InodesUsed, // inodes used sorting helper 123 | v.InodesFree, // inodes avail sorting helper 124 | inodeUsage, // inodes use% sorting helper 125 | }) 126 | } 127 | 128 | if tab.Length() == 0 { 129 | return 130 | } 131 | 132 | suffix := "device" 133 | if tab.Length() > 1 { 134 | suffix = "devices" 135 | } 136 | tab.SetTitle("%d %s %s", tab.Length(), title, suffix) 137 | 138 | // tab.AppendFooter(table.Row{fmt.Sprintf("%d %s", tab.Length(), title)}) 139 | sortMode := table.Asc 140 | if opts.SortBy >= 12 { 141 | sortMode = table.AscNumeric 142 | } 143 | 144 | tab.SortBy([]table.SortBy{{Number: opts.SortBy, Mode: sortMode}}) 145 | tab.Render() 146 | } 147 | 148 | // sizeTransformer makes a size human-readable. 149 | func sizeTransformer(val interface{}) string { 150 | return sizeToString(val.(uint64)) 151 | } 152 | 153 | // spaceTransformer makes a size human-readable and applies a color coding. 154 | func spaceTransformer(val interface{}) string { 155 | free := val.(uint64) 156 | 157 | s := termenv.String(sizeToString(free)) 158 | redAvail, _ := stringToSize(strings.Split(*availThreshold, ",")[1]) 159 | yellowAvail, _ := stringToSize(strings.Split(*availThreshold, ",")[0]) 160 | switch { 161 | case free < redAvail: 162 | s = s.Foreground(theme.colorRed) 163 | case free < yellowAvail: 164 | s = s.Foreground(theme.colorYellow) 165 | default: 166 | s = s.Foreground(theme.colorGreen) 167 | } 168 | 169 | return s.String() 170 | } 171 | 172 | // barTransformer transforms a percentage into a progress-bar. 173 | func barTransformer(val interface{}) string { 174 | usage := val.(float64) 175 | s := termenv.String() 176 | if usage > 0 { 177 | if barWidth() > 0 { 178 | bw := barWidth() - 2 179 | s = termenv.String(fmt.Sprintf("[%s%s] %5.1f%%", 180 | strings.Repeat("#", int(usage*float64(bw))), 181 | strings.Repeat(".", bw-int(usage*float64(bw))), 182 | usage*100, 183 | )) 184 | } else { 185 | s = termenv.String(fmt.Sprintf("%5.1f%%", usage*100)) 186 | } 187 | } 188 | 189 | // apply color to progress-bar 190 | redUsage, _ := strconv.ParseFloat(strings.Split(*usageThreshold, ",")[1], 64) 191 | yellowUsage, _ := strconv.ParseFloat(strings.Split(*usageThreshold, ",")[0], 64) 192 | switch { 193 | case usage >= redUsage: 194 | s = s.Foreground(theme.colorRed) 195 | case usage >= yellowUsage: 196 | s = s.Foreground(theme.colorYellow) 197 | default: 198 | s = s.Foreground(theme.colorGreen) 199 | } 200 | 201 | return s.String() 202 | } 203 | 204 | // inColumns return true if the column with index i is in the slice of visible 205 | // columns cols. 206 | func inColumns(cols []int, i int) bool { 207 | for _, v := range cols { 208 | if v == i { 209 | return true 210 | } 211 | } 212 | 213 | return false 214 | } 215 | 216 | // barWidth returns the width of progress-bars for the given render width. 217 | func barWidth() int { 218 | switch { 219 | case *width < 100: 220 | return 0 221 | case *width < 120: 222 | return 12 223 | default: 224 | return 22 225 | } 226 | } 227 | 228 | // tableWidth returns the required minimum table width for the given columns. 229 | func tableWidth(cols []int, separators bool) int { 230 | var sw int 231 | if separators { 232 | sw = 1 233 | } 234 | 235 | twidth := int(*width) 236 | for i := 0; i < len(columns); i++ { 237 | if inColumns(cols, i+1) { 238 | twidth -= 2 + sw + columns[i].Width 239 | } 240 | } 241 | 242 | return twidth 243 | } 244 | 245 | // sizeToString prettifies sizes. 246 | func sizeToString(size uint64) (str string) { 247 | b := float64(size) 248 | 249 | switch { 250 | case size >= 1<<60: 251 | str = fmt.Sprintf("%.1fE", b/(1<<60)) 252 | case size >= 1<<50: 253 | str = fmt.Sprintf("%.1fP", b/(1<<50)) 254 | case size >= 1<<40: 255 | str = fmt.Sprintf("%.1fT", b/(1<<40)) 256 | case size >= 1<<30: 257 | str = fmt.Sprintf("%.1fG", b/(1<<30)) 258 | case size >= 1<<20: 259 | str = fmt.Sprintf("%.1fM", b/(1<<20)) 260 | case size >= 1<<10: 261 | str = fmt.Sprintf("%.1fK", b/(1<<10)) 262 | default: 263 | str = fmt.Sprintf("%dB", size) 264 | } 265 | 266 | return 267 | } 268 | 269 | // stringToSize transforms an SI size into a number. 270 | func stringToSize(s string) (size uint64, err error) { 271 | regex := regexp.MustCompile(`^(\d+)([KMGTPE]?)$`) 272 | matches := regex.FindStringSubmatch(s) 273 | if len(matches) == 0 { 274 | return 0, fmt.Errorf("'%s' is not valid, must have integer with optional SI prefix", s) 275 | } 276 | 277 | num, err := strconv.ParseUint(matches[1], 10, 64) 278 | if err != nil { 279 | return 0, err 280 | } 281 | if matches[2] != "" { 282 | prefix := matches[2] 283 | switch prefix { 284 | case "K": 285 | size = num << 10 286 | case "M": 287 | size = num << 20 288 | case "G": 289 | size = num << 30 290 | case "T": 291 | size = num << 40 292 | case "P": 293 | size = num << 50 294 | case "E": 295 | size = num << 60 296 | default: 297 | err = fmt.Errorf("prefix '%s' not allowed, valid prefixes are K, M, G, T, P, E", prefix) 298 | return 299 | } 300 | } else { 301 | size = num 302 | } 303 | return 304 | } 305 | 306 | // stringToColumn converts a column name to its index. 307 | func stringToColumn(s string) (int, error) { 308 | s = strings.ToLower(s) 309 | 310 | for i, v := range columns { 311 | if v.ID == s { 312 | return i + 1, nil 313 | } 314 | } 315 | 316 | return 0, fmt.Errorf("unknown column: %s (valid: %s)", s, strings.Join(columnIDs(), ", ")) 317 | } 318 | 319 | // stringToSortIndex converts a column name to its sort index. 320 | func stringToSortIndex(s string) (int, error) { 321 | s = strings.ToLower(s) 322 | 323 | for _, v := range columns { 324 | if v.ID == s { 325 | return v.SortIndex, nil 326 | } 327 | } 328 | 329 | return 0, fmt.Errorf("unknown column: %s (valid: %s)", s, strings.Join(columnIDs(), ", ")) 330 | } 331 | 332 | // columnsIDs returns a slice of all column IDs. 333 | func columnIDs() []string { 334 | s := make([]string, len(columns)) 335 | for i, v := range columns { 336 | s[i] = v.ID 337 | } 338 | 339 | return s 340 | } 341 | -------------------------------------------------------------------------------- /themes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/muesli/termenv" 7 | ) 8 | 9 | // Theme defines a color theme used for printing tables. 10 | type Theme struct { 11 | colorRed termenv.Color 12 | colorYellow termenv.Color 13 | colorGreen termenv.Color 14 | colorBlue termenv.Color 15 | colorGray termenv.Color 16 | colorMagenta termenv.Color 17 | colorCyan termenv.Color 18 | } 19 | 20 | func defaultThemeName() string { 21 | if !termenv.HasDarkBackground() { 22 | return "light" 23 | } 24 | return "dark" 25 | } 26 | 27 | func loadTheme(theme string) (Theme, error) { 28 | themes := make(map[string]Theme) 29 | 30 | themes["dark"] = Theme{ 31 | colorRed: env.Color("#E88388"), 32 | colorYellow: env.Color("#DBAB79"), 33 | colorGreen: env.Color("#A8CC8C"), 34 | colorBlue: env.Color("#71BEF2"), 35 | colorGray: env.Color("#B9BFCA"), 36 | colorMagenta: env.Color("#D290E4"), 37 | colorCyan: env.Color("#66C2CD"), 38 | } 39 | 40 | themes["light"] = Theme{ 41 | colorRed: env.Color("#D70000"), 42 | colorYellow: env.Color("#FFAF00"), 43 | colorGreen: env.Color("#005F00"), 44 | colorBlue: env.Color("#000087"), 45 | colorGray: env.Color("#303030"), 46 | colorMagenta: env.Color("#AF00FF"), 47 | colorCyan: env.Color("#0087FF"), 48 | } 49 | 50 | themes["ansi"] = Theme{ 51 | colorRed: env.Color("9"), 52 | colorYellow: env.Color("11"), 53 | colorGreen: env.Color("10"), 54 | colorBlue: env.Color("12"), 55 | colorGray: env.Color("7"), 56 | colorMagenta: env.Color("13"), 57 | colorCyan: env.Color("8"), 58 | } 59 | 60 | if _, ok := themes[theme]; !ok { 61 | return Theme{}, fmt.Errorf("unknown theme: %s", theme) 62 | } 63 | 64 | return themes[theme], nil 65 | } 66 | --------------------------------------------------------------------------------