Repository: Mikescher/better-docker-ps Branch: master Commit: 1f05ac9afc61 Files: 38 Total size: 114.5 KB Directory structure: gitextract_b7umzfrm/ ├── .gitignore ├── .idea/ │ ├── .gitignore │ ├── better-docker-ps.iml │ ├── inspectionProfiles/ │ │ └── Project_Default.xml │ ├── modules.xml │ └── vcs.xml ├── LICENSE ├── Makefile ├── README.md ├── _data/ │ └── package-data/ │ ├── aur-bin/ │ │ ├── .gitignore │ │ └── PKGBUILD │ ├── aur-bin.sh │ ├── aur-git/ │ │ ├── .gitignore │ │ └── PKGBUILD │ ├── aur-git.sh │ ├── homebrew/ │ │ └── dops.rb │ ├── homebrew.sh │ └── sanitycheck.sh ├── cli/ │ ├── argTuple.go │ ├── context.go │ ├── options.go │ └── parser.go ├── cmd/ │ └── dops/ │ └── main.go ├── consts/ │ ├── api.go │ ├── exitcode.go │ ├── version.go │ └── version.sh ├── docker/ │ ├── api.go │ ├── schema.go │ └── util.go ├── go.mod ├── go.sum ├── impl/ │ ├── columns.go │ └── impl.go ├── install.sh ├── printer/ │ └── printer.go └── pserr/ ├── err.go └── util.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ########## GOLAND ########## .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf .idea/**/aws.xml .idea/**/contentModel.xml .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml .idea/**/gradle.xml .idea/**/libraries .idea/**/mongoSettings.xml *.iws atlassian-ide-plugin.xml .idea/httpRequests .idea/caches/build_file_checksums.ser .idea/$CACHE_FILE$ ########## Linux ########## *~ .fuse_hidden* .directory .Trash-* .nfs* ########## Custom ########## _out/* input.test input1.test input2.test input3.test input.json input1.json input2.json input_small.json input1_small.json input2_small.json ================================================ FILE: .idea/.gitignore ================================================ # Default ignored files /shelf/ /workspace.xml # Editor-based HTTP Client requests /httpRequests/ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml ================================================ FILE: .idea/better-docker-ps.iml ================================================ ================================================ FILE: .idea/inspectionProfiles/Project_Default.xml ================================================ ================================================ FILE: .idea/modules.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Mike Schwörer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ build: go generate ./... CGO_ENABLED=0 go build -o _out/dops ./cmd/dops run: build ./_out/dops clean: go clean rm ./_out/* package: @echo "Make sure you have updated file://$(shell pwd)/consts/version.go" @echo "Make sure youhave created+pished a matching tag" @read -p "Continue?" go clean rm -rf ./_out/* _data/package-data/sanitycheck.sh GOARCH=386 GOOS=linux CGO_ENABLED=0 go build -o _out/dops_linux-386-static ./cmd/dops # Linux - 32 bit GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go build -o _out/dops_linux-amd64-static ./cmd/dops # Linux - 64 bit GOARCH=arm64 GOOS=linux CGO_ENABLED=0 go build -o _out/dops_linux-arm64-static ./cmd/dops # Linux - ARM GOARCH=386 GOOS=linux go build -o _out/dops_linux-386 ./cmd/dops # Linux - 32 bit GOARCH=amd64 GOOS=linux go build -o _out/dops_linux-amd64 ./cmd/dops # Linux - 64 bit GOARCH=arm64 GOOS=linux go build -o _out/dops_linux-arm64 ./cmd/dops # Linux - ARM GOARCH=arm GOOS=linux GOARM=5 go build -o _out/dops_linux-arm32v5 ./cmd/dops # Linux - ARM32 v5 (e.g. Raspberry 3) GOARCH=arm GOOS=linux GOARM=6 go build -o _out/dops_linux-arm32v6 ./cmd/dops # Linux - ARM32 v6 GOARCH=arm GOOS=linux GOARM=7 go build -o _out/dops_linux-arm32v7 ./cmd/dops # Linux - ARM32 v7 GOARCH=amd64 GOOS=darwin go build -o _out/dops_macos-amd64 ./cmd/dops # macOS - 64 bit GOARCH=arm64 GOOS=darwin go build -o _out/dops_macos-arm64 ./cmd/dops # macOS (Apple Silicon) GOARCH=amd64 GOOS=openbsd go build -o _out/dops_openbsd-amd64 ./cmd/dops # OpenBSD - 64 bit GOARCH=arm64 GOOS=openbsd go build -o _out/dops_openbsd-arm64 ./cmd/dops # OpenBSD - ARM GOARCH=amd64 GOOS=freebsd go build -o _out/dops_freebsd-amd64 ./cmd/dops # FreeBSD - 64 bit GOARCH=arm64 GOOS=freebsd go build -o _out/dops_freebsd-arm64 ./cmd/dops # FreeBSD - ARM _data/package-data/aur-git.sh _data/package-data/aur-bin.sh _data/package-data/homebrew.sh echo "" echo "[TODO]: call 'make package-push-aur-git' " echo "[TODO]: call 'make package-push-aur-bin' " echo "[TODO]: call 'make package-push-homebrew' " echo "[TODO]: create github release" echo "" package-push-aur-git: cd _out/dops-git && git push package-push-aur-bin: cd _out/dops-bin && git push package-push-homebrew: cd _out/homebrew-tap && git push ================================================ FILE: README.md ================================================ # ./dops - better `docker ps` A replacement for the default docker-ps that tries really hard to fit within your terminal width. ![](readme.d/main.png) ## Rationale By default, my `docker ps` output is really wide and every line wraps around into three. This (obviously) breaks the tabular display and makes everything chaotic. *(This gets especially bad if one container has multiple port mappings, and they are all displayed in a single row)* It doesn't look like we'll get improved output in the foreseeable future (see [moby#7477](https://github.com/moby/moby/issues/7477)), so I decided to make my own drop-in replacement. ## Features - All normal commandline flags/options from docker-ps work *(almost)* the same. - Write multi-value data (like multiple port mappings, multiple networks, etc.) into multiple lines instead of concatenating them. - Add color to the STATE and STATUS column (green / yellow / red). - Automatically remove columns in the output until it fits in the current terminal width. - sort the output with the `--sort` argument - Enter watch mode with the `--watch` argument More Changes from default docker-ps: - Show (by default) the container-cmd without arguments. - Show the ImageName (by default) without the registry prefix, and split ImageName and ImageTag into two columns. - Added the columns IP and NETWORK to the default column set (if they fit) - Added support for a few new columns (via --format): `{{.ImageName}`, `{{.ImageTag}`, `{{.Tag}`, `{{.ImageRegistry}`, `{{.Registry}`, `{{.ShortCommand}`, `{{.LabelKeys}`, `{{.IP}` - Added options to control the color-output, the used socket, the time-zone and time-format, etc (see `./dops --help`) ## Getting started ### Generic Linux (e.g. Debian/Fedora/...) - Download the latest binary from the [releases page](https://github.com/Mikescher/better-docker-ps/releases) and put it into your PATH (eg /usr/local/bin) - You can also use the following one-liner (afterwards you can use the `dops` command everywhere): ``` sudo wget "https://github.com/Mikescher/better-docker-ps/releases/latest/download/dops_linux-amd64-static" -O "/usr/local/bin/dops" && sudo chmod +x "/usr/local/bin/dops" ``` ### ArchLinux - Alternatively you can use one of the AUR packages (under Arch Linux): * https://aur.archlinux.org/packages/dops-bin (installs `dops` into your PATH) * https://aur.archlinux.org/packages/dops-git (installs `dops` into your PATH) - or the homebrew package: * `brew tap mikescher/tap && brew install dops` ### Optional steps - Alias the docker ps command to `dops` (see [section below](#usage-as-drop-in-replacement)) ### Building from source If you want to build `dops` from source, you need to have Go installed. ```sh git clone https://github.com/Mikescher/better-docker-ps.git cd better-docker-ps make build mv _out/dops "$HOME/.local/bin/" ``` ## Screenshots ![](readme.d/fullsize.png) All (default) columns visible   ![](readme.d/default.png) Output on a medium sized terminal   ![](readme.d/small.png) Output on a small terminal   ## Usage as drop-in replacement You can fully replace docker ps by creating a shell function in your `.bashrc` / `.zshrc`... ~~~sh docker() { case $1 in ps) shift command dops "$@" ;; *) command docker "$@";; esac } ~~~ This will alias every call to `docker ps ...` with `dops ...` (be sure to have the dops binary in your PATH). If you are using the fish-shell you have to create a (similar) function: ~~~fish function docker if test -n "$argv[1]" switch $argv[1] case ps dops $argv[2..-1] case '*' command docker $argv[1..-1] end end end ~~~ ## Changing the output format By default dops tries to be "intelligent" and find the best output format for your terminal width. The current output formats (= table columns) are defined in the [options.go](https://github.com/Mikescher/better-docker-ps/blob/master/cli/options.go). The first format that fits in your terminal width is used. But you can also override it by supplying a `--format` parameter. If you supply more than one `--format` parameter the first one that fits your terminal is used (same logic as with the default ones...) Normally only simple columns aka `{{.Status}}` are supported. But you can also use the full golang template syntax (e.g. `{{ printf "%.15s" .Command }}`). In this case it can be useful to specify the column header by prefixing it with a colon (`SHORTENED NAME:{{ printf "%.10s" (join .Names ";") }}`) The following functions are defined in these templates (plus the [default go functions](https://pkg.go.dev/text/template)): - `join`: strings.Join - `array_last`: v\[-1\] - `array_slice`: v\[a..b\] - `in_array`: v1.contains(v2) - `json`: json.Marshal(v) - `json_indent`: json.MarshalIndent(v, "", " ") - `json_pretty`: json.Indent(v, "", " ") - `coalesce`: v1 ?? v2 - `to_string`: fmt.Sprintf("%v", v) - `deref`: *v - `now`: time.Now() - `uniqid`: UUID Examples: ~~~~ $ ./dops --format "table {{.ID}}" $ ./dops --format "table {{.ID}}\\t{{.Names}}\\t{{.State}}" $ ./dops --format "idlist" $ ./dops --format "table {{.ID}}\\t{{.Names}}\\t{{.State}}" --format "table {{.ID}}\\t{{.Names}}" --format "table {{.ID}}" $ ./dops --format "ID: {{.ID}}; Name: {{.Names}}" $ ./dops -aq $ ./dops --sort "IP" --sort-direction "ASC" $ ./dops --format "table {{.ID}}\\tCMD:{{ printf \"%.15s\" .Command }}" $ ./dops --format "table {{.ID}}\\tNAME:{{ printf \"%.10s\" (join .Names \";\") }}" ~~~~ ## Persistant configuration You can also configure some/most of the options via a configuration file. Place a TOML formatted file in `$HOME/.config/dops.conf` / `$XDG_CONFIG_HOME/dops.conf`. ( `~/Library/Application Support/dops.conf` under macOS ) The following keys are supported: - verbose - silent - timezone - timeformat - timeformat-header - color - socket - all - size - filter (= string array) - search - format (= string array) - last - latest - truncate - header (= true / false / simple) - sort (= string array) - sort-direction (= string array) Example: ```toml verbose = 0 timezone = "Europe/Berlin" format = [ "table {{.ID}}\t{{.Names}}\t{{.State}}\t{{.Status}}", "table {{.ID}}\t{{.Names}}\t{{.State}}", "table {{.ID}}\t{{.Names}}", "table {{.ID}}", ] header = "simple" ``` ## Manual Output of `./dops --help`: ~~~~~~ better-docker-ps Usage: dops [OPTIONS] List docker container Options (default): -h, --help Show this screen. --version Show version. --all , -a Show all containers (default shows just running) --filter , -f Filter output based on conditions provided --search , -g Filter output by substring match across all visible columns (case-insensitive) --format Pretty-print containers using a Go template --last , -n Show n last created containers (includes all states) --latest , -l Show the latest created container (includes all states) --no-trunc Don't truncate output (eg ContainerIDs, Sha256 Image references, commandline) --quiet , -q Only display container IDs --size , -s Display total file sizes Options (extra | do not exist in `docker ps`): --silent Do not print any output --timezone Specify the timezone for date outputs --color Enable/Disable terminal color output --no-color Disable terminal color output --socket Specify the docker socket location (Default: `auto` - which calls the docker cli to determine the socket) --timeformat Specify the datetime output format (golang syntax) --no-header Do not print the table header --simple-header Do not print the lines under the header --format You can specify multiple formats and the first one that fits your terminal widt will be used --sort Sort output by a specific column, use the same identifier as in --format, only useful together with table formats --sort-direction The sort direction, only useful in combination with --sort --watch , -w Automatically refresh output periodically (interval is optional, default: 2s) Available --format keys (default): {{.ID}} Container ID {{.Image}} Image ID {{.Command}} Quoted command {{.CreatedAt}} Time when the container was created. {{.RunningFor}} Elapsed time since the container was started. {{.Ports}} Published ports. ([!] differs from docker CLI, these are only the published ports) {{.State}} Container status {{.Status}} Container status with details {{.Size}} Container disk size. {{.Names}} Container names. {{.Labels}} All labels assigned to the container. {{.Label}} [!] Unsupported {{.Mounts}} Names of the volumes mounted in this container. {{.Networks}} Names of the networks attached to this container. Available --format keys (extra | do not exist in `docker ps`): {{.ImageName}} Image ID (without tag and registry) {{.ImageTag}}, {{.Tag}} Image Tag {{.ImageRegistry}}, {{.Registry}} Image Registry {{.ShortCommand}} Command without arguments {{.LabelKeys}} All labels assigned to the container (keys only) {{.ShortPublishedPorts}} Published ports, shorter output than {{.Ports}} {{.LongPublishedPorts}} Published ports, full output with IP {{.ExposedPorts}} Exposed ports {{.PublishedPorts}} Published ports {{.NotPublishedPorts}} Exposed but not published ports {{.PublicPorts}} Only the public part of published ports {{.IP}} Internal IP Address ~~~~~~ ================================================ FILE: _data/package-data/aur-bin/.gitignore ================================================ pkg/ src/ dops/ dops-bin/ dops-git/ .SRCINFO *.tar.zst ================================================ FILE: _data/package-data/aur-bin/PKGBUILD ================================================ # Maintainer: Mikescher # Repo: https://github.com/Mikescher/better-docker-ps pkgname=dops-bin pkgver=1.13 pkgrel=1 pkgdesc="A replacement for the default docker-ps that tries really hard to fit into the width of your terminal." url="https://github.com/Mikescher/better-docker-ps" license=('Apache') arch=('x86_64') _binary="dops_linux-amd64" source=( "https://github.com/Mikescher/better-docker-ps/releases/download/v${pkgver}/${_binary}" ) _bin_sha='48addb268291151b0dd6752675829dc3ba81523d1515d6094ccf18269e26dfd6' sha256sums=( "$_bin_sha" ) package() { install -D -m755 "$srcdir/${_binary}" "${pkgdir}/usr/bin/dops" } ================================================ FILE: _data/package-data/aur-bin.sh ================================================ #!/bin/bash set -o nounset # disallow usage of unset vars ( set -u ) set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e ) set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E ) set -o pipefail # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status IFS=$'\n\t' # Set $IFS to only newline and tab. cd "$(dirname "$0")/aur-bin" git clean -ffdX version="$(cd ../../../ && git tag --sort=-v:refname | grep -P 'v[0-9\.]' | head -1 | cut -c2-)" cs0="$(cd ../../../ && sha256sum _out/dops_linux-amd64 | cut -d ' ' -f 1)" echo "Version: ${version} (${cs0})" sed --regexp-extended -i "s/pkgver=[0-9\.]+/pkgver=${version}/g" PKGBUILD sed --regexp-extended -i "s/_bin_sha='[A-Za-z0-9]+'/_bin_sha='${cs0}'/g" PKGBUILD namcap PKGBUILD makepkg --printsrcinfo > .SRCINFO # makepkg #(do not makepkg, release is probably not live) cd ../../../ git clone ssh://aur@aur.archlinux.org/dops-bin.git _out/dops-bin cp -v _data/package-data/aur-bin/PKGBUILD _out/dops-bin/PKGBUILD cp -v _data/package-data/aur-bin/.SRCINFO _out/dops-bin/.SRCINFO cd _out/dops-bin git add PKGBUILD git add .SRCINFO if [ -z "$(git status --porcelain)" ]; then echo "(!) Nothing changed -- nothing to commit" else git commit -m "v${version}" fi cd "../../_data/package-data/aur-bin" git clean -ffdX # git push manually (!) ================================================ FILE: _data/package-data/aur-git/.gitignore ================================================ pkg/ src/ dops/ dops-bin/ dops-git/ .SRCINFO *.tar.zst ================================================ FILE: _data/package-data/aur-git/PKGBUILD ================================================ # Maintainer: Mikescher # Repo: https://github.com/Mikescher/better-docker-ps pkgname=dops-git pkgver=1.13 pkgrel=1 pkgdesc="A replacement for the default docker-ps that tries really hard to fit into the width of your terminal." url="https://github.com/Mikescher/better-docker-ps" license=('Apache') makedepends=('go' 'git') arch=('any') source=("$pkgname::git+https://github.com/Mikescher/better-docker-ps") sha256sums=('SKIP') build() { cd "$pkgname" go build -o dops cmd/dops/main.go } package() { install -D -m755 "$pkgname/dops" "${pkgdir}/usr/bin/dops" } ================================================ FILE: _data/package-data/aur-git.sh ================================================ #!/bin/bash set -o nounset # disallow usage of unset vars ( set -u ) set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e ) set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E ) set -o pipefail # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status IFS=$'\n\t' # Set $IFS to only newline and tab. cd "$(dirname "$0")/aur-git" git clean -ffdX version=$(cd ../../../ && git tag --sort=-v:refname | grep -P 'v[0-9\.]' | head -1 | cut -c2-) echo "Version: ${version}" sed --regexp-extended -i "s/pkgver=[0-9\.]+/pkgver=${version}/g" PKGBUILD namcap PKGBUILD makepkg --printsrcinfo > .SRCINFO makepkg cd ../../../ pwd git clone ssh://aur@aur.archlinux.org/dops-git.git _out/dops-git cp _data/package-data/aur-git/PKGBUILD _out/dops-git/PKGBUILD cp _data/package-data/aur-git/.SRCINFO _out/dops-git/.SRCINFO cd _out/dops-git git add PKGBUILD git add .SRCINFO if [ -z "$(git status --porcelain)" ]; then echo "(!) Nothing changed -- nothing to commit" else git commit -m "v${version}" fi cd "../../_data/package-data/aur-git" git clean -ffdX # git push manually (!) ================================================ FILE: _data/package-data/homebrew/dops.rb ================================================ class Dops < Formula desc " A replacement for the default docker-ps that tries really hard to fit into the width of your terminal. " homepage "https://github.com/Mikescher/better-docker-ps" url "https://github.com/Mikescher/better-docker-ps/releases/download/v<>/dops_macos-arm64" sha256 "<>" def install bin.install "dops_macos-arm64" => "dops" end test do assert true end end ================================================ FILE: _data/package-data/homebrew.sh ================================================ #!/bin/bash set -o nounset # disallow usage of unset vars ( set -u ) set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e ) set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E ) set -o pipefail # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status IFS=$'\n\t' # Set $IFS to only newline and tab. set -o functrace cd "$(dirname "$0")/homebrew" cp dops.rb dops_patch.rb version="$(cd ../../../ && git tag --sort=-v:refname | grep -P 'v[0-9\.]' | head -1 | cut -c2-)" cs0="$(cd ../../../ && sha256sum _out/dops_macos-arm64 | cut -d ' ' -f 1)" echo "Version: ${version} (${cs0})" sed --regexp-extended -i "s/<>/${version}/g" dops_patch.rb sed --regexp-extended -i "s/<>/${cs0}/g" dops_patch.rb cd ../../../ git clone https://github.com/Mikescher/homebrew-tap.git _out/homebrew-tap cp "_data/package-data/homebrew/dops_patch.rb" _out/homebrew-tap/dops.rb rm "_data/package-data/homebrew/dops_patch.rb" cd _out/homebrew-tap/ git add dops.rb if [ -z "$(git status --porcelain)" ]; then echo "(!) Nothing changed -- nothing to commit" else git commit -m "dops v${version}" fi # git push manually (!) ================================================ FILE: _data/package-data/sanitycheck.sh ================================================ #!/bin/bash cd "$(dirname "$0")" version_tag="$(cd ../../ && git tag --sort=-v:refname | grep -P 'v[0-9\.]' | head -1 | cut -c2-)" version_code="$(cd ../../ && cat consts/version.go | grep -oP 'BETTER_DOCKER_PS_VERSION = .*' | grep -oP '"[0-9\.]+"' | grep -oP '[0-9\.]+' )" if [ "$version_tag" != "$version_code" ]; then echo "Git-Tag version and Code-const version do not match!" echo "[GIT-TAG] := $version_tag" echo "[GO-CODE] := $version_code" exit 1 else echo "Version ('$version_tag') ok" fi ================================================ FILE: cli/argTuple.go ================================================ package cli type ArgumentTuple struct { Key string Value *string } ================================================ FILE: cli/context.go ================================================ package cli import ( "better-docker-ps/pserr" "context" "encoding/hex" "fmt" "os" "strings" "time" "git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/termext" ) type PSContext struct { context.Context Opt Options Cache map[string]any } func (c PSContext) PrintPrimaryOutput(msg string) { if c.Opt.Quiet { return } c.printPrimaryRaw(msg + "\n") } func (c PSContext) PrintFatalMessage(msg string) { if c.Opt.Quiet { return } c.printErrorRaw(msg + "\n") } func (c PSContext) PrintFatalError(e error) { if c.Opt.Quiet { return } c.printErrorRaw(pserr.FormatError(e, c.Opt.Verbose) + "\n") } func (c PSContext) PrintErrorMessage(msg string) { if c.Opt.Quiet { return } c.printErrorRaw(msg + "\n") } func (c PSContext) PrintVerbose(msg string) { if c.Opt.Quiet || !c.Opt.Verbose { return } c.printVerboseRaw(msg + "\n") } func (c PSContext) PrintVerboseHeader(msg string) { if c.Opt.Quiet || !c.Opt.Verbose { return } c.printVerboseRaw("\n") c.printVerboseRaw("========================================" + "\n") c.printVerboseRaw(msg + "\n") c.printVerboseRaw("========================================" + "\n") c.printVerboseRaw("\n") } func (c PSContext) PrintVerboseKV(key string, vval any) { if c.Opt.Quiet || !c.Opt.Verbose { return } termlen := 236 keylen := 28 var val = "" switch v := vval.(type) { case []byte: val = hex.EncodeToString(v) case string: val = v case time.Time: val = v.In(c.Opt.TimeZone).Format(time.RFC3339Nano) default: val = fmt.Sprintf("%v", v) } if len(val) > (termlen-keylen-4) || strings.Contains(val, "\n") { c.printVerboseRaw(key + " :=\n" + val + "\n") } else { padkey := langext.StrPadRight(key, " ", keylen) c.printVerboseRaw(padkey + " := " + val + "\n") } } func (c PSContext) ClearTerminal() { fmt.Print("\033[H\033[2J") } func (c PSContext) printPrimaryRaw(msg string) { if c.Opt.Quiet { return } writeStdout(msg) } func (c PSContext) printErrorRaw(msg string) { if c.Opt.Quiet { return } if c.Opt.OutputColor { writeStderr(termext.Red(msg)) } else { writeStderr(msg) } } func (c PSContext) printVerboseRaw(msg string) { if c.Opt.Quiet { return } if c.Opt.OutputColor { writeStdout(termext.Gray(msg)) } else { writeStdout(msg) } } func writeStdout(msg string) { _, err := os.Stdout.WriteString(msg) if err != nil { panic("failed to write to stdout: " + err.Error()) } } func writeStderr(msg string) { _, err := os.Stderr.WriteString(msg) if err != nil { panic("failed to write to stdout: " + err.Error()) } } func NewContext(opt Options) (*PSContext, error) { return &PSContext{ Context: context.Background(), Opt: opt, Cache: make(map[string]any), }, nil } func NewEarlyContext() *PSContext { return &PSContext{ Context: context.Background(), Opt: DefaultCLIOptions(), Cache: make(map[string]any), } } func (c PSContext) Finish() { // ... } func (c *PSContext) GetIntFromCache(key string, calc func() int) int { if v1, ok := c.Cache[key]; ok { if v2, ok := v1.(int); ok { return v2 } panic(fmt.Sprintf("Wrong type in cache type(%s) = %T (expected: int)", key, v1)) } val := calc() c.Cache[key] = val return val } ================================================ FILE: cli/options.go ================================================ package cli import ( "encoding/json" "os" "path/filepath" "regexp" "runtime" "strings" "time" "git.blackforestbytes.com/BlackForestBytes/goext/cmdext" "git.blackforestbytes.com/BlackForestBytes/goext/termext" ) type SortDirection string const ( SortASC SortDirection = "ASC" SortDESC SortDirection = "DESC" ) type Options struct { Version bool Help bool Socket string Quiet bool Verbose bool OutputColor bool TimeZone *time.Location TimeFormat string TimeFormatHeader string Input *string All bool WithSize bool Filter *map[string][]string Search *string Limit int DefaultFormat bool Format []string // if more than 1 value, we use the later values as fallback for too-small terminal PrintHeader bool PrintHeaderLines bool Truncate bool SortColumns []string SortDirection []SortDirection WatchInterval *time.Duration } func DefaultCLIOptions() Options { return Options{ Version: false, Help: false, Quiet: false, Verbose: false, OutputColor: termext.SupportsColors(), TimeZone: time.Local, TimeFormatHeader: "Z07:00 MST", TimeFormat: "2006-01-02 15:04:05", Socket: "auto", Input: nil, All: false, WithSize: false, Limit: -1, DefaultFormat: true, Format: []string{ "table {{.ID}}\\t{{.Names}}\\t{{.ImageName}}\\t{{.Tag}}\\t{{.ShortCommand}}\\t{{.CreatedAt}}\\t{{.State}}\\t{{.Status}}\\t{{.LongPublishedPorts}}\\t{{.Networks}}\\t{{.IP}}", "table {{.ID}}\\t{{.Names}}\\t{{.ImageName}}\\t{{.Tag}}\\t{{.ShortCommand}}\\t{{.CreatedAt}}\\t{{.State}}\\t{{.Status}}\\t{{.LongPublishedPorts}}\\t{{.IP}}", "table {{.ID}}\\t{{.Names}}\\t{{.ImageName}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.State}}\\t{{.Status}}\\t{{.LongPublishedPorts}}\\t{{.IP}}", "table {{.ID}}\\t{{.Names}}\\t{{.ImageName}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.State}}\\t{{.Status}}\\t{{.PublishedPorts}}\\t{{.IP}}", "table {{.ID}}\\t{{.Names}}\\t{{.ImageName}}\\t{{.Tag}}\\t{{.CreatedAt}}\\t{{.State}}\\t{{.Status}}\\t{{.PublishedPorts}}", "table {{.ID}}\\t{{.Names}}\\t{{.ImageName}}\\t{{.Tag}}\\t{{.State}}\\t{{.Status}}\\t{{.PublishedPorts}}", "table {{.ID}}\\t{{.Names}}\\t{{.Tag}}\\t{{.State}}\\t{{.Status}}\\t{{.PublishedPorts}}", "table {{.ID}}\\t{{.Names}}\\t{{.Tag}}\\t{{.State}}\\t{{.Status}}\\t{{.ShortPublishedPorts}}", "table {{.ID}}\\t{{.Names}}\\t{{.Tag}}\\t{{.State}}\\t{{.Status}}", "table {{.ID}}\\t{{.Names}}\\t{{.State}}\\t{{.Status}}", "table {{.ID}}\\t{{.Names}}\\t{{.State}}", "table {{.Names}}\\t{{.State}}", "table {{.Names}}", "table {{.ID}}", }, PrintHeader: true, PrintHeaderLines: true, Truncate: true, SortColumns: make([]string, 0), SortDirection: make([]SortDirection, 0), WatchInterval: nil, } } func getDefaultSocket() string { if runtime.GOOS == "darwin" { home, err := os.UserHomeDir() if err != nil { return "/var/run/docker.sock" } return filepath.Join(home, ".docker/run/docker.sock") } return "/var/run/docker.sock" } func (o Options) GetSocket() string { // [1] Manually specified socket if o.Socket != "auto" { return o.Socket } // [2] Auto-detect from current docker context res, err := cmdext.Runner("docker").Arg("context").Arg("list").Arg("--format").Arg("json").Timeout(10 * time.Second).FailOnTimeout().FailOnExitCode().Run() if err == nil { for _, line := range strings.Split(res.StdOut, "\n") { var context dockerContext err = json.Unmarshal([]byte(line), &context) if err != nil { continue } if context.Current { return context.socket() } } } // [3] MacOS homedir if runtime.GOOS == "darwin" { if home, err := os.UserHomeDir(); err == nil { fp := filepath.Join(home, ".docker/run/docker.sock") if _, err = os.Stat(fp); err == nil { return fp } } } // [4] Default return "/var/run/docker.sock" } type dockerContext struct { Name string Description string DockerEndpoint string Current bool Error string ContextType string } var unixSocketPrefixPat = regexp.MustCompile("^unix://") func (ctx dockerContext) socket() string { return unixSocketPrefixPat.ReplaceAllString(ctx.DockerEndpoint, "") } func p(v bool) *bool { return &v } ================================================ FILE: cli/parser.go ================================================ package cli import ( "better-docker-ps/pserr" "fmt" "github.com/BurntSushi/toml" "github.com/kirsle/configdir" "git.blackforestbytes.com/BlackForestBytes/goext/timeext" "os" "path/filepath" "strconv" "strings" "time" "github.com/joomcode/errorx" "git.blackforestbytes.com/BlackForestBytes/goext/langext" ) func ParseCommandline(columnKeys []string) (Options, error) { o, err := parseCommandlineInternal(columnKeys) if err != nil { return Options{}, errorx.Decorate(err, "failed to parse commandline") } return o, nil } func parseCommandlineInternal(columnKeys []string) (Options, error) { unprocessedArgs := os.Args[1:] allOptionArguments := make([]ArgumentTuple, 0) // Parse Commandline KeyValue pairs for len(unprocessedArgs) > 0 { arg := unprocessedArgs[0] unprocessedArgs = unprocessedArgs[1:] if strings.HasPrefix(arg, "--") { arg = arg[2:] if strings.Contains(arg, "=") { key := arg[0:strings.Index(arg, "=")] val := arg[strings.Index(arg, "=")+1:] if len(key) <= 1 { return Options{}, pserr.DirectOutput.New("Unknown/Misplaced argument: " + arg) } allOptionArguments = append(allOptionArguments, ArgumentTuple{Key: key, Value: langext.Ptr(val)}) continue } else { key := arg if len(key) <= 1 { return Options{}, pserr.DirectOutput.New("Unknown/Misplaced argument: " + arg) } if len(unprocessedArgs) == 0 || strings.HasPrefix(unprocessedArgs[0], "-") { allOptionArguments = append(allOptionArguments, ArgumentTuple{Key: key, Value: nil}) continue } else { val := unprocessedArgs[0] unprocessedArgs = unprocessedArgs[1:] allOptionArguments = append(allOptionArguments, ArgumentTuple{Key: key, Value: langext.Ptr(val)}) continue } } } else if strings.HasPrefix(arg, "-") { arg = arg[1:] if len(arg) > 1 { for i := 0; i < len(arg); i++ { allOptionArguments = append(allOptionArguments, ArgumentTuple{Key: arg[i : i+1], Value: nil}) } continue } key := arg if key == "" { return Options{}, pserr.DirectOutput.New("Unknown/Misplaced argument: " + arg) } if len(unprocessedArgs) == 0 || strings.HasPrefix(unprocessedArgs[0], "-") { allOptionArguments = append(allOptionArguments, ArgumentTuple{Key: key, Value: nil}) continue } else { val := unprocessedArgs[0] unprocessedArgs = unprocessedArgs[1:] allOptionArguments = append(allOptionArguments, ArgumentTuple{Key: key, Value: langext.Ptr(val)}) continue } } else { return Options{}, pserr.DirectOutput.New("Unknown/Misplaced argument: " + arg) } } // Process common options opt := DefaultCLIOptions() confPath := filepath.Join(configdir.LocalConfig(), "dops.conf") if v, err := os.ReadFile(confPath); err == nil { tomldata := make(map[string]any) _, err = toml.Decode(string(v), &tomldata) if err != nil { return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "': " + err.Error()) } for tk, tvany := range tomldata { tv := fmt.Sprintf("%v", tvany) var tvarr []string = nil if varr1, ok := tvany.([]string); ok { tvarr = varr1 } else if varr2, ok := tvany.([]any); ok && langext.ArrAll(varr2, func(v any) bool { _, ok := v.(string); return ok }) { tvarr = langext.ArrCastSafe[any, string](varr2) } else { tvarr = []string{tv} } if tk == "verbose" { opt.Verbose, err = strconv.ParseBool(tv) if err != nil { return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "' (invalid value for 'verbose'): " + err.Error()) } } else if tk == "silent" { opt.Quiet, err = strconv.ParseBool(tv) if err != nil { return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "' (invalid value for 'silent'): " + err.Error()) } } else if tk == "timezone" { loc, err := time.LoadLocation(tv) if err != nil { return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + ": Unknown timezone: " + tv) } opt.TimeZone = loc } else if tk == "timeformat" { opt.TimeFormat = tv } else if tk == "timeformat-header" { opt.TimeFormatHeader = tv } else if tk == "color" { opt.OutputColor, err = strconv.ParseBool(tv) if err != nil { return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "' (invalid value for 'color'): " + err.Error()) } } else if tk == "socket" { opt.Socket = tv } else if tk == "all" { opt.All, err = strconv.ParseBool(tv) if err != nil { return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "' (invalid value for 'all'): " + err.Error()) } } else if tk == "size" { opt.WithSize, err = strconv.ParseBool(tv) if err != nil { return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "' (invalid value for 'size'): " + err.Error()) } } else if tk == "filter" { for _, elem := range tvarr { spl := strings.SplitN(elem, "=", 2) if len(spl) != 2 { return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "': Filter value must have a key and a value (a=b): " + elem) } if opt.Filter == nil { _v := make(map[string][]string) opt.Filter = &_v } filter := *opt.Filter filter[spl[0]] = []string{spl[1]} opt.Filter = &filter } } else if tk == "search" { opt.Search = langext.Ptr(tv) } else if tk == "format" { for _, elem := range tvarr { if opt.DefaultFormat { opt.Format = make([]string, 0) } opt.Format = append(opt.Format, elem) opt.DefaultFormat = false } } else if tk == "last" { if vint, err := strconv.ParseInt(tv, 10, 32); err == nil { opt.Limit = int(vint) opt.All = true continue } else { return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "': Failed to parse number of field 'last': '" + tv + "'") } } else if tk == "latest" { vbool, err := strconv.ParseBool(tv) if err != nil { return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "': Failed to parse boolean value of 'latest': '" + tv + "'") } if vbool { opt.Limit = -1 opt.All = true } else { opt.Limit = 1 opt.All = false } } else if tk == "truncate" { opt.Truncate, err = strconv.ParseBool(tv) if err != nil { return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "' (invalid value for 'truncate'): " + err.Error()) } } else if tk == "header" { if strings.EqualFold(tv, "no") { opt.PrintHeader = false } else if strings.EqualFold(tv, "simple") { opt.PrintHeader = true opt.PrintHeaderLines = false } else { vbool, err := strconv.ParseBool(tv) if err != nil { return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "': Failed to parse boolean value of 'latest': '" + tv + "'") } if vbool { opt.PrintHeader = true opt.PrintHeaderLines = true } else { opt.PrintHeader = false opt.PrintHeaderLines = false } } } else if tk == "sort" { opt.SortColumns = tvarr } else if tk == "sort-direction" { opt.SortDirection = make([]SortDirection, 0) for _, sdv := range tvarr { if strings.ToUpper(sdv) == "ASC" { opt.SortDirection = append(opt.SortDirection, SortASC) continue } if strings.ToUpper(sdv) == "DESC" { opt.SortDirection = append(opt.SortDirection, SortDESC) continue } return Options{}, pserr.DirectOutput.New(fmt.Sprintf("Failed to parse config file '"+confPath+"': Failed to parse sort-direction argument '%s'", sdv)) } } else { return Options{}, pserr.DirectOutput.New("Failed to parse config file '" + confPath + "' (unknown key '" + tk + "')") } } } for _, arg := range allOptionArguments { if (arg.Key == "h" || arg.Key == "help") && arg.Value == nil { return Options{Help: true}, nil } if arg.Key == "version" && arg.Value == nil { return Options{Version: true}, nil } if (arg.Key == "v" || arg.Key == "verbose") && arg.Value == nil { opt.Verbose = true continue } if (arg.Key == "silent") && arg.Value == nil { opt.Quiet = true continue } if (arg.Key == "q" || arg.Key == "quiet") && arg.Value == nil { opt.Format = []string{"idlist"} opt.DefaultFormat = false continue } if arg.Key == "timezone" && arg.Value != nil { loc, err := time.LoadLocation(*arg.Value) if err != nil { return Options{}, pserr.DirectOutput.New("Unknown timezone: " + *arg.Value) } opt.TimeZone = loc continue } if arg.Key == "timeformat" && arg.Value != nil { opt.TimeFormat = *arg.Value opt.TimeFormatHeader = "" continue } if arg.Key == "timeformat-header" && arg.Value != nil { opt.TimeFormatHeader = *arg.Value continue } if arg.Key == "color" && arg.Value != nil && (strings.ToLower(*arg.Value) == "true" || *arg.Value == "1") { opt.OutputColor = true continue } if arg.Key == "color" && arg.Value != nil && (strings.ToLower(*arg.Value) == "false" || *arg.Value == "0") { opt.OutputColor = false continue } if arg.Key == "no-color" && arg.Value == nil { opt.OutputColor = false continue } if (arg.Key == "socket") && arg.Value != nil { opt.Socket = *arg.Value continue } if (arg.Key == "input") && arg.Value != nil { // used for testing opt.Input = langext.Ptr(*arg.Value) continue } if (arg.Key == "all" || arg.Key == "a") && arg.Value == nil { opt.All = true continue } if (arg.Key == "size") && arg.Value == nil { opt.WithSize = true continue } if (arg.Key == "filter" || arg.Key == "f") && arg.Value != nil { spl := strings.SplitN(*arg.Value, "=", 2) if len(spl) != 2 { return Options{}, pserr.DirectOutput.New("Filter argument must have a key and a value (a=b): " + arg.Key) } if opt.Filter == nil { _v := make(map[string][]string) opt.Filter = &_v } filter := *opt.Filter if spl[0] == "project" { spl[0] = "label" spl[1] = "com.docker.compose.project=" + spl[1] } filter[spl[0]] = []string{spl[1]} opt.Filter = &filter continue } if (arg.Key == "search" || arg.Key == "g") && arg.Value != nil { opt.Search = langext.Ptr(*arg.Value) continue } if (arg.Key == "format") && arg.Value != nil { if opt.DefaultFormat { opt.Format = make([]string, 0) } opt.Format = append(opt.Format, *arg.Value) opt.DefaultFormat = false continue } if (arg.Key == "last" || arg.Key == "n") && arg.Value != nil { if v, err := strconv.ParseInt(*arg.Value, 10, 32); err == nil { opt.Limit = int(v) opt.All = true continue } return Options{}, pserr.DirectOutput.New("Failed to parse number argument '--last': '" + *arg.Value + "'") } if (arg.Key == "latest" || arg.Key == "l") && arg.Value != nil { opt.Limit = 1 opt.All = true continue } if (arg.Key == "no-trunc" || arg.Key == "no-truncate") && arg.Value == nil { opt.Truncate = false continue } if (arg.Key == "no-header") && arg.Value == nil { opt.PrintHeader = false continue } if (arg.Key == "simple-header") && arg.Value == nil { opt.PrintHeaderLines = false continue } if arg.Key == "sort" && arg.Value != nil { opt.SortColumns = strings.Split(*arg.Value, ",") continue } if arg.Key == "sort-direction" && arg.Value != nil { opt.SortDirection = make([]SortDirection, 0) for _, sdv := range strings.Split(*arg.Value, ",") { if strings.ToUpper(sdv) == "ASC" { opt.SortDirection = append(opt.SortDirection, SortASC) continue } if strings.ToUpper(sdv) == "DESC" { opt.SortDirection = append(opt.SortDirection, SortDESC) continue } return Options{}, pserr.DirectOutput.New(fmt.Sprintf("Failed to parse sort-direction argument '%s'", sdv)) } continue } if arg.Key == "watch" || arg.Key == "w" { d, err := timeext.ParseDurationShortString(langext.Coalesce(arg.Value, "2s")) if err != nil { return Options{}, pserr.DirectOutput.New("Failed to parse duration argument of '--watch': '" + *arg.Value + "'") } opt.WatchInterval = &d continue } return Options{}, pserr.DirectOutput.New("Unknown argument: " + arg.Key) } // Post Processing if len(opt.SortDirection) == 0 && len(opt.SortColumns) > 0 { for i := 0; i < len(opt.SortColumns); i++ { opt.SortDirection = append(opt.SortDirection, SortASC) // default sort (if not specified) is ASC on all sort columns } } if len(opt.SortDirection) != len(opt.SortColumns) { return Options{}, pserr.DirectOutput.New(fmt.Sprintf("Must specify the same number of values in --sort and --sort-direction ( %d <> %d )", len(opt.SortDirection), len(opt.SortColumns))) } for _, colkey := range opt.SortColumns { if !langext.InArray(colkey, columnKeys) { return Options{}, pserr.DirectOutput.New(fmt.Sprintf("Unknown column : '%s' in --sort", colkey)) } } return opt, nil } ================================================ FILE: cmd/dops/main.go ================================================ package main import ( "better-docker-ps/cli" "better-docker-ps/consts" "better-docker-ps/impl" "better-docker-ps/pserr" "fmt" "os" "runtime/debug" "git.blackforestbytes.com/BlackForestBytes/goext/langext" ) // Inspiration: https://github.com/moby/moby/issues/7477 func main() { defer func() { if err := recover(); err != nil { _, _ = os.Stderr.WriteString(fmt.Sprintf("%v\n\n%s", err, string(debug.Stack()))) os.Exit(consts.ExitcodePanic) } }() opt, err := cli.ParseCommandline(langext.MapKeyArr(impl.ColumnMap)) if err != nil { ctx := cli.NewEarlyContext() ctx.PrintFatalError(err) os.Exit(pserr.GetExitCode(err, consts.ExitcodeCLIParse)) return } ctx, err := cli.NewContext(opt) if err != nil { ctx.PrintFatalError(err) os.Exit(pserr.GetExitCode(err, consts.ExitcodeError)) return } defer ctx.Finish() if opt.Version { ctx.PrintPrimaryOutput("better-docker-ps v" + consts.BETTER_DOCKER_PS_VERSION) os.Exit(0) return } if opt.Help { printHelp(ctx) os.Exit(0) return } if opt.WatchInterval == nil { err = impl.Execute(ctx) if err != nil { ctx.PrintFatalError(err) os.Exit(pserr.GetExitCode(err, consts.ExitcodeError)) return } os.Exit(0) } else { err = impl.Watch(ctx, *opt.WatchInterval) if err != nil { ctx.PrintFatalError(err) os.Exit(pserr.GetExitCode(err, consts.ExitcodeError)) return } os.Exit(0) } } func printHelp(ctx *cli.PSContext) { ctx.PrintPrimaryOutput("better-docker-ps") ctx.PrintPrimaryOutput("") ctx.PrintPrimaryOutput("Usage:") ctx.PrintPrimaryOutput(" dops [OPTIONS] List docker container") ctx.PrintPrimaryOutput("") ctx.PrintPrimaryOutput("Options (default):") ctx.PrintPrimaryOutput(" -h, --help Show this screen.") ctx.PrintPrimaryOutput(" --version Show version.") ctx.PrintPrimaryOutput(" --all , -a Show all containers (default shows just running)") ctx.PrintPrimaryOutput(" --filter , -f Filter output based on conditions provided") ctx.PrintPrimaryOutput(" --search , -g Filter output by substring match across all visible columns (case-insensitive)") ctx.PrintPrimaryOutput(" --format Pretty-print containers using a Go template") ctx.PrintPrimaryOutput(" --last , -n Show n last created containers (includes all states)") ctx.PrintPrimaryOutput(" --latest , -l Show the latest created container (includes all states)") ctx.PrintPrimaryOutput(" --no-trunc Don't truncate output (eg ContainerIDs, Sha256 Image references, commandline)") ctx.PrintPrimaryOutput(" --quiet , -q Only display container IDs") ctx.PrintPrimaryOutput(" --size , -s Display total file sizes") ctx.PrintPrimaryOutput("") ctx.PrintPrimaryOutput("Options (extra | do not exist in `docker ps`):") ctx.PrintPrimaryOutput(" --silent Do not print any output") ctx.PrintPrimaryOutput(" --timezone Specify the timezone for date outputs") ctx.PrintPrimaryOutput(" --color Enable/Disable terminal color output") ctx.PrintPrimaryOutput(" --no-color Disable terminal color output") ctx.PrintPrimaryOutput(" --socket Specify the docker socket location (Default: `auto` - which calls the docker cli to determine the socket)") ctx.PrintPrimaryOutput(" --timeformat Specify the datetime output format (golang syntax)") ctx.PrintPrimaryOutput(" --no-header Do not print the table header") ctx.PrintPrimaryOutput(" --simple-header Do not print the lines under the header") ctx.PrintPrimaryOutput(" --format You can specify multiple formats and the first one that fits your terminal widt will be used") ctx.PrintPrimaryOutput(" --sort Sort output by a specific column, use the same identifier as in --format, only useful together with table formats ") ctx.PrintPrimaryOutput(" --sort-direction The sort direction, only useful in combination with --sort") ctx.PrintPrimaryOutput(" --watch Automatically refresh output periodically (interval is optional, default: 2s)") ctx.PrintPrimaryOutput("") ctx.PrintPrimaryOutput("Available --format keys (default):") ctx.PrintPrimaryOutput(" {{.ID}} Container ID") ctx.PrintPrimaryOutput(" {{.Image}} Image ID") ctx.PrintPrimaryOutput(" {{.Command}} Quoted command") ctx.PrintPrimaryOutput(" {{.CreatedAt}} Time when the container was created.") ctx.PrintPrimaryOutput(" {{.RunningFor}} Elapsed time since the container was started.") ctx.PrintPrimaryOutput(" {{.Ports}} Published ports. ([!] differs from docker CLI, these are only the published ports)") ctx.PrintPrimaryOutput(" {{.State}} Container status") ctx.PrintPrimaryOutput(" {{.Status}} Container status with details") ctx.PrintPrimaryOutput(" {{.Size}} Container disk size.") ctx.PrintPrimaryOutput(" {{.Names}} Container names.") ctx.PrintPrimaryOutput(" {{.Labels}} All labels assigned to the container.") ctx.PrintPrimaryOutput(" {{.Label}} [!] Unsupported") ctx.PrintPrimaryOutput(" {{.Mounts}} Names of the volumes mounted in this container.") ctx.PrintPrimaryOutput(" {{.Networks}} Names of the networks attached to this container.") ctx.PrintPrimaryOutput("") ctx.PrintPrimaryOutput("Available --format keys (extra | do not exist in `docker ps`):") ctx.PrintPrimaryOutput(" {{.ImageName} Image ID (without tag and registry)") ctx.PrintPrimaryOutput(" {{.ImageTag}, {{.Tag} Image Tag") ctx.PrintPrimaryOutput(" {{.ImageRegistry}, {{.Registry} Image Registry") ctx.PrintPrimaryOutput(" {{.ShortCommand} Command without arguments") ctx.PrintPrimaryOutput(" {{.LabelKeys} All labels assigned to the container (keys only)") ctx.PrintPrimaryOutput(" {{.ShortPublishedPorts}} Published ports, shorter output than {{.Ports}}") ctx.PrintPrimaryOutput(" {{.LongPublishedPorts}} Published ports, full output with IP") ctx.PrintPrimaryOutput(" {{.ExposedPorts}} Exposed ports") ctx.PrintPrimaryOutput(" {{.NotPublishedPorts}} Exposed but not published ports") ctx.PrintPrimaryOutput(" {{.PublishedPorts}} Published ports") ctx.PrintPrimaryOutput(" {{.PublicPorts}} Only the public part of published ports") ctx.PrintPrimaryOutput(" {{.IP} Internal IP Address") ctx.PrintPrimaryOutput("") } ================================================ FILE: consts/api.go ================================================ package consts // DockerAPIContainerList -> see https://docs.docker.com/engine/api/v1.41/#tag/Container/operation/ContainerList const DockerAPIContainerList = "http://localhost/v1.44/containers/json" ================================================ FILE: consts/exitcode.go ================================================ package consts const ( ExitcodeError = 60 ExitcodePanic = 61 ExitcodeNoArguments = 62 ExitcodeCLIParse = 63 ExitcodeNoLogin = 64 ExitcodeUnsupportedOutputFormat = 65 ExitcodeRecordNotFound = 66 ) const ( ExitcodeInvalidSession = 81 ExitcodePasswordNotFound = 82 ExitcodeParentNotAFolder = 83 ExitcodeInvalidPosition = 84 ExitcodeBookmarkFieldNotSupported = 85 ) ================================================ FILE: consts/version.go ================================================ package consts //go:generate /bin/bash version.sh const BETTER_DOCKER_PS_VERSION = "1.17" ================================================ FILE: consts/version.sh ================================================ #!/bin/bash sed -i 's/const BETTER_DOCKER_PS_VERSION = ".*"/const BETTER_DOCKER_PS_VERSION = "'$(git describe --tags --abbrev=0 | sed "s/v//")'"/' "version.go" ================================================ FILE: docker/api.go ================================================ package docker import ( "better-docker-ps/cli" "better-docker-ps/consts" "better-docker-ps/pserr" "context" "encoding/json" "fmt" "io" "net" "net/http" "net/url" "os" "strconv" "strings" "github.com/joomcode/errorx" ) func ListContainer(ctx *cli.PSContext) ([]byte, error) { if ctx.Opt.Input != nil { data, err := os.ReadFile(*ctx.Opt.Input) if err != nil { return nil, pserr.DirectOutput.Wrap(err, "Failed to read --input file") } return data, nil } socket := ctx.Opt.GetSocket() client := http.Client{ Transport: &http.Transport{ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { return net.Dial("unix", socket) }, }, } uri := fmt.Sprintf("%s?1=1", consts.DockerAPIContainerList) if ctx.Opt.All { uri += "&all=true" } if ctx.Opt.WithSize { uri += "&size=true" } if ctx.Opt.Limit != -1 { uri += "&limit=" + strconv.Itoa(ctx.Opt.Limit) } if ctx.Opt.Filter != nil { bin, err := json.Marshal(*ctx.Opt.Filter) if err != nil { return nil, errorx.InternalError.Wrap(err, "Failed to marshal filter") } uri += "&filters=" + url.PathEscape(string(bin)) } response, err := client.Get(uri) if err != nil { if strings.Contains(err.Error(), "connect: permission denied") { return nil, pserr.DirectOutput.Wrap(err, "Call to unix socket failed (permission denied)") } else { return nil, pserr.DirectOutput.Wrap(err, "Call to unix socket failed") } } body, err := io.ReadAll(response.Body) if err != nil { return nil, errorx.InternalError.Wrap(err, "Failed to read unix socket response") } return body, nil } ================================================ FILE: docker/schema.go ================================================ package docker import ( "git.blackforestbytes.com/BlackForestBytes/goext/langext" "net" ) type ContainerSchema struct { ID string `json:"Id"` Names []string `json:"Names"` Image string `json:"Image"` ImageID string `json:"ImageID"` Command string `json:"Command"` Created int64 `json:"Created"` Ports []PortSchema `json:"Ports"` Labels map[string]string `json:"Labels"` State ContainerState `json:"State"` Status string `json:"Status"` HostConfig ContainerHostConfig `json:"HostConfig"` NetworkSettings ContainerNetworkSettings `json:"NetworkSettings"` Mounts []ContainerMount `json:"Mounts"` SizeRw int64 `json:"SizeRw"` SizeRootFs int64 `json:"SizeRootFs"` } func (s ContainerSchema) PortsSorted() []PortSchema { ports := langext.ArrCopy(s.Ports) langext.SortSliceStable(ports, func(p1, p2 PortSchema) bool { if p1.PublicPort != p2.PublicPort { return p1.PublicPort < p2.PublicPort } if p1.PrivatePort != p2.PrivatePort { return p1.PrivatePort < p2.PrivatePort } return false }) return ports } type ContainerHostConfig struct { NetworkMode string `json:"NetworkMode"` } type ContainerNetworkSettings struct { Networks map[string]ContainerSingleNetworkSettings `json:"Networks"` } type ContainerSingleNetworkSettings struct { NetworkMode string `json:"NetworkID"` EndpointID string `json:"EndpointID"` Gateway string `json:"Gateway"` IPAddress string `json:"IPAddress"` IPPrefixLen int `json:"IPPrefixLen"` IPv6Gateway string `json:"IPv6Gateway"` GlobalIPv6Address string `json:"GlobalIPv6Address"` GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen"` MacAddress string `json:"MacAddress"` } type PortSchema struct { IP string `json:"IP"` PrivatePort int `json:"PrivatePort"` PublicPort int `json:"PublicPort"` Type string `json:"Type"` } func (s PortSchema) IsLoopback() bool { ip := net.ParseIP(s.IP) return ip != nil && ip.IsLoopback() } type ContainerMount struct { Name string `json:"Name"` Source string `json:"Source"` Destination string `json:"Destination"` Driver string `json:"Driver"` Mode string `json:"Mode"` RW bool `json:"RW"` Propagation string `json:"Propagation"` } type ContainerState string const ( StateCreated ContainerState = "created" StateRunning ContainerState = "running" StateRestarting ContainerState = "restarting" StateExited ContainerState = "exited" StatePaused ContainerState = "paused" StateDead ContainerState = "dead" ) func (ct ContainerState) Num() int { switch ct { case StateCreated: return 0 case StateRunning: return 1 case StateRestarting: return 2 case StateExited: return 3 case StatePaused: return 4 case StateDead: return 5 default: return 999 } } ================================================ FILE: docker/util.go ================================================ package docker import ( "better-docker-ps/cli" "strings" ) var registryPrefixList = []string{ ".com", ".de", ".net", ".io", ".org", } func SplitDockerImage(ctx *cli.PSContext, img string) (string, string, string) { resultRegistry := "" resultImage := "" resultTag := "" if v := strings.Split(img, ":"); len(v) > 1 { last := v[len(v)-1] if !strings.Contains(last, "/") { resultTag = last img = img[0 : len(img)-len(last)-1] } } if v := strings.Split(img, "/"); len(v) > 1 { first := v[0] if len(v) == 3 { resultRegistry = first img = img[len(resultRegistry)+1:] } else { for _, rpl := range registryPrefixList { if strings.HasSuffix(first, rpl) { resultRegistry = first img = img[len(resultRegistry)+1:] } break } } } resultImage = img if resultImage == "sha256" && len(resultTag) == 64 && ctx.Opt.Truncate { resultImage = "(sha256)" resultTag = resultTag[0:12] + "..." } return resultRegistry, resultImage, resultTag } ================================================ FILE: go.mod ================================================ module better-docker-ps go 1.24.2 require ( git.blackforestbytes.com/BlackForestBytes/goext v0.0.572 github.com/BurntSushi/toml v1.4.0 github.com/joomcode/errorx v1.1.1 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f golang.org/x/term v0.31.0 ) require ( github.com/bytedance/sonic v1.13.2 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-gonic/gin v1.10.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/rs/xid v1.6.0 // indirect github.com/rs/zerolog v1.34.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect go.mongodb.org/mongo-driver v1.17.3 // indirect golang.org/x/arch v0.16.0 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/net v0.39.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ git.blackforestbytes.com/BlackForestBytes/goext v0.0.572 h1:NALJ4KKkrRZcNJNsmGrUsjFdOclHSA/KyB6f94QV43k= git.blackforestbytes.com/BlackForestBytes/goext v0.0.572/go.mod h1:C4mXq6MwC919jRHjN5IM++qGy6wmvzJZyz30nf285MU= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/joomcode/errorx v1.1.1 h1:/LFG/qSk1gUTuZjs+qlyOJEpcVjD9DXgBNFhdZkQrjY= github.com/joomcode/errorx v1.1.1/go.mod h1:eQzdtdlNyN7etw6YCS4W4+lu442waxZYw5yvz0ULrRo= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU= github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= ================================================ FILE: impl/columns.go ================================================ package impl import ( "better-docker-ps/cli" "better-docker-ps/docker" "better-docker-ps/printer" "bytes" "encoding/json" "fmt" "git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/mathext" "git.blackforestbytes.com/BlackForestBytes/goext/rext" "git.blackforestbytes.com/BlackForestBytes/goext/termext" "git.blackforestbytes.com/BlackForestBytes/goext/timeext" "reflect" "regexp" "strconv" "strings" "text/template" "time" ) var rexIP = rext.W(regexp.MustCompile("^(?P[0-9]{1,3})\\.(?P[0-9]{1,3})\\.(?P[0-9]{1,3})\\.(?P[0-9]{1,3})$")) type ColSortFun = func(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int type ColumnDef struct { Reader printer.ColFun Sorter ColSortFun } var ColumnMap = map[string]ColumnDef{ "ID": {ColContainerID, SortContainerID}, "Image": {ColFullImage, SortFullImage}, "ImageName": {ColImage, SortImage}, "ImageTag": {ColImageTag, SortImageTag}, "Registry": {ColRegistry, SortRegistry}, "ImageRegistry": {ColRegistry, SortRegistry}, "Tag": {ColImageTag, SortImageTag}, "Command": {ColCommand, SortCommand}, "ShortCommand": {ColShortCommand, SortShortCommand}, "CreatedAt": {ColCreatedAt, SortCreatedAt}, "RunningFor": {ColRunningFor, SortRunningFor}, "Ports": {ColPortsPublished, SortPortsPublished}, "PublishedPorts": {ColPortsPublished, SortPortsPublished}, "ShortPublishedPorts": {ColPortsPublishedShort, SortPortsPublishedShort}, "LongPublishedPorts": {ColPortsPublishedLong, SortPortsPublishedLong}, "ExposedPorts": {ColPortsExposed, SortPortsExposed}, "NotPublishedPorts": {ColPortsNotPublished, SortPortsNotPublished}, "PublicPorts": {ColPortsPublicPart, SortPortsPublicPart}, "State": {ColState, SortState}, "Status": {ColStatus, SortStatus}, "Size": {ColSize, SortSize}, "Names": {ColName, SortName}, "Labels": {ColLabels, SortLabels}, "LabelKeys": {ColLabelKeys, SortLabelKeys}, "Mounts": {ColMounts, SortMounts}, "Networks": {ColNetworks, SortNetworks}, "IP": {ColIP, SortIP}, } func ColContainerID(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"CONTAINER ID"} } if ctx.Opt.Truncate { return []string{cont.ID[0:12]} } else { return []string{cont.ID} } } func ColFullImage(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"IMAGE"} } return []string{cont.Image} } func ColRegistry(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"REGISTRY"} } v, _, _ := docker.SplitDockerImage(ctx, cont.Image) return []string{v} } func ColImage(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"IMAGE"} } _, v, _ := docker.SplitDockerImage(ctx, cont.Image) return []string{v} } func ColImageTag(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"TAG"} } _, _, v := docker.SplitDockerImage(ctx, cont.Image) return []string{v} } func ColCommand(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"COMMAND"} } cmd := cont.Command if ctx.Opt.Truncate && len(cmd) > 20 { cmd = cmd[:19] + "…" } cmd = "\"" + cmd + "\"" return []string{cmd} } func ColShortCommand(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"COMMAND"} } spl := strings.Split(cont.Command, " ") if len(spl) == 0 { return []string{""} } else { return []string{spl[0]} } } func ColRunningFor(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"CREATED"} } ts := time.Unix(cont.Created, 0) diff := time.Now().Sub(ts) return []string{timeext.FormatNaturalDurationEnglish(diff)} } func ColCreatedAt(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { if ctx.Opt.TimeFormatHeader != "" { hdr := time.Now().In(ctx.Opt.TimeZone).Format(ctx.Opt.TimeFormatHeader) if hdr == "Z UTC" { hdr = "UTC" } return []string{"CREATED AT (" + hdr + ")"} } else { return []string{"CREATED AT"} } } ts := time.Unix(cont.Created, 0) return []string{ts.In(ctx.Opt.TimeZone).Format(ctx.Opt.TimeFormat)} } func ColState(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"STATE"} } strstate := "[" + strings.ToUpper(string(cont.State)) + "]" if !ctx.Opt.OutputColor { return []string{strstate} } return []string{stateColor(cont.State, strstate)} } func ColStatus(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"STATUS"} } if !ctx.Opt.OutputColor { return []string{cont.Status} } return []string{statusColor(cont.Status, cont.Status)} } func ColPortsExposed(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"EXPOSED PORTS"} } m := make(map[string]bool) r := make([]string, 0) for _, port := range cont.PortsSorted() { p1 := langext.StrPadLeft(strconv.Itoa(port.PublicPort), " ", 5) p2 := langext.StrPadLeft(strconv.Itoa(port.PrivatePort), " ", 5) if port.PublicPort == 0 { str := fmt.Sprintf(" %s / %s", p2, port.Type) if _, ok := m[str]; !ok { m[str] = true r = append(r, str) } } else { str := fmt.Sprintf("%s -> %s / %s", p1, p2, port.Type) if _, ok := m[str]; !ok { m[str] = true r = append(r, str) } } } return r } func ColPortsPublicPart(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"EXPOSED PORTS"} } m := make(map[string]bool) r := make([]string, 0) for _, port := range cont.PortsSorted() { if port.PublicPort != 0 { str := fmt.Sprintf("%d", port.PublicPort) if _, ok := m[str]; !ok { m[str] = true r = append(r, strconv.Itoa(port.PublicPort)) } } } return r } func ColPortsPublished(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"PUBLISHED PORTS"} } pubPortLenMax := ctx.GetIntFromCache("Printer::Ports::port_pub_length", func() int { ml := 0 for _, v1 := range allData { for _, v2 := range v1.Ports { ml = mathext.Max(ml, len(strconv.Itoa(v2.PublicPort))) } } return ml }) privPortLenMax := ctx.GetIntFromCache("Printer::Ports::port_pub_length", func() int { ml := 0 for _, v1 := range allData { for _, v2 := range v1.Ports { ml = mathext.Max(ml, len(strconv.Itoa(v2.PrivatePort))) } } return ml }) m := make(map[string]bool) r := make([]string, 0) for _, port := range cont.PortsSorted() { p1 := langext.StrPadLeft(strconv.Itoa(port.PublicPort), " ", pubPortLenMax) p2 := langext.StrPadLeft(strconv.Itoa(port.PrivatePort), " ", privPortLenMax) if port.PublicPort != 0 { str := fmt.Sprintf("%s -> %s / %s", p1, p2, port.Type) if port.IsLoopback() { str += " (loc)" } if _, ok := m[str]; !ok { m[str] = true r = append(r, str) } } } return r } func ColPortsPublishedShort(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"PUBLISHED PORTS"} } pubPortLenMax := ctx.GetIntFromCache("Printer::Ports::port_pub_length", func() int { ml := 0 for _, v1 := range allData { for _, v2 := range v1.Ports { ml = mathext.Max(ml, len(strconv.Itoa(v2.PublicPort))) } } return ml }) privPortLenMax := ctx.GetIntFromCache("Printer::Ports::port_pub_length", func() int { ml := 0 for _, v1 := range allData { for _, v2 := range v1.Ports { ml = mathext.Max(ml, len(strconv.Itoa(v2.PrivatePort))) } } return ml }) m := make(map[string]bool) r := make([]string, 0) for _, port := range cont.PortsSorted() { p1 := langext.StrPadLeft(strconv.Itoa(port.PublicPort), " ", pubPortLenMax) p2 := langext.StrPadLeft(strconv.Itoa(port.PrivatePort), " ", privPortLenMax) if port.PublicPort != 0 { str := fmt.Sprintf("%s -> %s", p1, p2) if _, ok := m[str]; !ok { m[str] = true r = append(r, str) } } } return r } func ColPortsPublishedLong(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"PUBLISHED PORTS"} } iplenMax := ctx.GetIntFromCache("Printer::Ports::ip_length", func() int { ml := 0 for _, v1 := range allData { for _, v2 := range v1.Ports { ml = mathext.Max(ml, len(v2.IP)) } } return ml }) pubPortLenMax := ctx.GetIntFromCache("Printer::Ports::port_pub_length", func() int { ml := 0 for _, v1 := range allData { for _, v2 := range v1.Ports { ml = mathext.Max(ml, len(strconv.Itoa(v2.PublicPort))) } } return ml }) privPortLenMax := ctx.GetIntFromCache("Printer::Ports::port_pub_length", func() int { ml := 0 for _, v1 := range allData { for _, v2 := range v1.Ports { ml = mathext.Max(ml, len(strconv.Itoa(v2.PrivatePort))) } } return ml }) m := make(map[string]bool) r := make([]string, 0) for _, port := range cont.PortsSorted() { p0 := langext.StrPadLeft("["+port.IP+"]", " ", iplenMax+2) p1 := langext.StrPadLeft(strconv.Itoa(port.PublicPort), " ", pubPortLenMax) p2 := langext.StrPadLeft(strconv.Itoa(port.PrivatePort), " ", privPortLenMax) if port.PublicPort != 0 { str := fmt.Sprintf("%s:%s -> %s", p0, p1, p2) if _, ok := m[str]; !ok { m[str] = true r = append(r, str) } } } return r } func ColPortsNotPublished(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"NOT PUBLISHED PORTS"} } m := make(map[string]bool) r := make([]string, 0) for _, port := range cont.PortsSorted() { p2 := langext.StrPadLeft(strconv.Itoa(port.PrivatePort), " ", 5) if port.PublicPort == 0 { str := fmt.Sprintf("%s / %s", p2, port.Type) if _, ok := m[str]; !ok { m[str] = true r = append(r, str) } } } return r } func ColName(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"NAME"} } r := make([]string, 0, len(cont.Names)) for _, n := range cont.Names { if len(n) > 0 && n[0] == '/' { n = n[1:] } r = append(r, n) } return r } func ColSize(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"SIZE"} } if cont.SizeRw == 0 && cont.SizeRootFs == 0 { return []string{} } return []string{fmt.Sprintf("%v (virt %v)", langext.StrPadRight(langext.FormatBytes(cont.SizeRw), " ", 11), langext.FormatBytes(cont.SizeRootFs))} } func ColMounts(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"MOUNTS"} } r := make([]string, 0, len(cont.Mounts)) for _, mnt := range cont.Mounts { val := fmt.Sprintf("%s -> %s", mnt.Source, mnt.Destination) if !mnt.RW { val += " [ro]" } r = append(r, val) } return r } func ColIP(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"IP"} } r := make([]string, 0, len(cont.NetworkSettings.Networks)) for _, nw := range cont.NetworkSettings.Networks { if nw.IPAddress != "" { r = append(r, nw.IPAddress) } } return r } func ColLabels(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"LABELS"} } r := make([]string, 0, len(cont.Mounts)) for k, v := range cont.Labels { r = append(r, fmt.Sprintf("%s := %s", k, v)) } return r } func ColLabelKeys(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"LABELS"} } r := make([]string, 0, len(cont.Mounts)) for k, _ := range cont.Labels { r = append(r, k) } return r } func ColNetworks(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { if cont == nil { return []string{"NETWORKS"} } r := make([]string, 0, len(cont.Mounts)) for k := range cont.NetworkSettings.Networks { r = append(r, k) } return r } func ColPlaintext(str string) printer.ColFun { return func(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string { return []string{str} } } // ##################################################################################################################### func SortContainerID(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { if ctx.Opt.Truncate { return langext.Compare(v1.ID[0:12], v2.ID[0:12]) } else { return langext.Compare(v1.ID, v2.ID) } } func SortFullImage(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { return langext.Compare(v1.Image, v2.Image) } func SortRegistry(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { reg1, _, _ := docker.SplitDockerImage(ctx, v1.Image) reg2, _, _ := docker.SplitDockerImage(ctx, v2.Image) return langext.Compare(reg1, reg2) } func SortImage(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { _, img1, _ := docker.SplitDockerImage(ctx, v1.Image) _, img2, _ := docker.SplitDockerImage(ctx, v2.Image) return langext.Compare(img1, img2) } func SortImageTag(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { _, _, tag1 := docker.SplitDockerImage(ctx, v1.Image) _, _, tag2 := docker.SplitDockerImage(ctx, v2.Image) return langext.Compare(tag1, tag2) } func SortCommand(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { return langext.Compare(v1.Command, v2.Command) } func SortShortCommand(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { spl1 := strings.Split(v1.Command, " ") sc1 := "" if len(spl1) > 0 { sc1 = spl1[0] } spl2 := strings.Split(v2.Command, " ") sc2 := "" if len(spl2) > 0 { sc2 = spl2[0] } return langext.Compare(sc1, sc2) } func SortRunningFor(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { return langext.Compare(v1.Created, v2.Created) * -1 // runnign for is 'now - created', so we need to invert the sort order } func SortCreatedAt(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { return langext.Compare(v1.Created, v2.Created) } func SortState(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { return langext.Compare(v1.State.Num(), v2.State.Num()) } func SortStatus(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { return langext.Compare(v1.Status, v2.Status) } func SortPortsExposed(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { parr1 := langext.ArrCopy(v1.Ports) parr2 := langext.ArrCopy(v2.Ports) pl1 := langext.ArrMap(parr1, func(v docker.PortSchema) string { return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type) }) pl2 := langext.ArrMap(parr2, func(v docker.PortSchema) string { return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type) }) langext.SortStable(pl1) langext.SortStable(pl2) pstr1 := strings.Join(pl1, "\n") pstr2 := strings.Join(pl2, "\n") return langext.Compare(pstr1, pstr2) } func SortPortsPublished(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { parr1 := langext.ArrCopy(v1.Ports) parr2 := langext.ArrCopy(v2.Ports) parr1 = langext.ArrFilter(parr1, func(v docker.PortSchema) bool { return v.PublicPort != 0 }) parr2 = langext.ArrFilter(parr2, func(v docker.PortSchema) bool { return v.PublicPort != 0 }) pl1 := langext.ArrMap(parr1, func(v docker.PortSchema) string { return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type) }) pl2 := langext.ArrMap(parr2, func(v docker.PortSchema) string { return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type) }) langext.SortStable(pl1) langext.SortStable(pl2) pstr1 := strings.Join(pl1, "\n") pstr2 := strings.Join(pl2, "\n") return langext.Compare(pstr1, pstr2) } func SortPortsPublishedShort(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { parr1 := langext.ArrCopy(v1.Ports) parr2 := langext.ArrCopy(v2.Ports) parr1 = langext.ArrFilter(parr1, func(v docker.PortSchema) bool { return v.PublicPort != 0 }) parr2 = langext.ArrFilter(parr2, func(v docker.PortSchema) bool { return v.PublicPort != 0 }) pl1 := langext.ArrMap(parr1, func(v docker.PortSchema) string { return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type) }) pl2 := langext.ArrMap(parr2, func(v docker.PortSchema) string { return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type) }) langext.SortStable(pl1) langext.SortStable(pl2) pstr1 := strings.Join(pl1, "\n") pstr2 := strings.Join(pl2, "\n") return langext.Compare(pstr1, pstr2) } func SortPortsPublishedLong(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { parr1 := langext.ArrCopy(v1.Ports) parr2 := langext.ArrCopy(v2.Ports) parr1 = langext.ArrFilter(parr1, func(v docker.PortSchema) bool { return v.PublicPort != 0 }) parr2 = langext.ArrFilter(parr2, func(v docker.PortSchema) bool { return v.PublicPort != 0 }) pl1 := langext.ArrMap(parr1, func(v docker.PortSchema) string { return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type) }) pl2 := langext.ArrMap(parr2, func(v docker.PortSchema) string { return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type) }) langext.SortStable(pl1) langext.SortStable(pl2) pstr1 := strings.Join(pl1, "\n") pstr2 := strings.Join(pl2, "\n") return langext.Compare(pstr1, pstr2) } func SortPortsNotPublished(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { parr1 := langext.ArrCopy(v1.Ports) parr2 := langext.ArrCopy(v2.Ports) parr1 = langext.ArrFilter(parr1, func(v docker.PortSchema) bool { return v.PublicPort == 0 }) parr2 = langext.ArrFilter(parr2, func(v docker.PortSchema) bool { return v.PublicPort == 0 }) pl1 := langext.ArrMap(parr1, func(v docker.PortSchema) string { return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type) }) pl2 := langext.ArrMap(parr2, func(v docker.PortSchema) string { return fmt.Sprintf("%s;%08d;%08d;%s", v.IP, v.PrivatePort, v.PublicPort, v.Type) }) langext.SortStable(pl1) langext.SortStable(pl2) pstr1 := strings.Join(pl1, "\n") pstr2 := strings.Join(pl2, "\n") return langext.Compare(pstr1, pstr2) } func SortPortsPublicPart(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { parr1 := langext.ArrCopy(v1.Ports) parr2 := langext.ArrCopy(v2.Ports) parr1 = langext.ArrFilter(parr1, func(v docker.PortSchema) bool { return v.PublicPort != 0 }) parr2 = langext.ArrFilter(parr2, func(v docker.PortSchema) bool { return v.PublicPort != 0 }) pl1 := langext.ArrMap(parr1, func(v docker.PortSchema) string { return fmt.Sprintf("%08d", v.PublicPort) }) pl2 := langext.ArrMap(parr2, func(v docker.PortSchema) string { return fmt.Sprintf("%08d", v.PublicPort) }) langext.SortStable(pl1) langext.SortStable(pl2) pstr1 := strings.Join(pl1, ":") pstr2 := strings.Join(pl2, ":") return langext.Compare(pstr1, pstr2) } func SortName(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { names1 := langext.ArrCopy(v1.Names) names2 := langext.ArrCopy(v2.Names) langext.SortStable(names1) langext.SortStable(names2) return langext.CompareArr(names1, names2) } func SortSize(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { return langext.CompareArr([]int64{v1.SizeRw, v1.SizeRootFs}, []int64{v2.SizeRw, v2.SizeRootFs}) } func SortMounts(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { mounts1 := langext.ArrMap(v1.Mounts, func(v docker.ContainerMount) string { return fmt.Sprintf("%s\n%s", v.Source, v.Destination) }) mounts2 := langext.ArrMap(v2.Mounts, func(v docker.ContainerMount) string { return fmt.Sprintf("%s\n%s", v.Source, v.Destination) }) langext.SortStable(mounts1) langext.SortStable(mounts2) return langext.CompareArr(mounts1, mounts2) } func SortIP(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { ips1 := langext.ArrMap(langext.MapToArr(v1.NetworkSettings.Networks), func(v langext.MapEntry[string, docker.ContainerSingleNetworkSettings]) string { return v.Value.IPAddress }) ips2 := langext.ArrMap(langext.MapToArr(v2.NetworkSettings.Networks), func(v langext.MapEntry[string, docker.ContainerSingleNetworkSettings]) string { return v.Value.IPAddress }) ips1 = langext.ArrFilter(ips1, func(v string) bool { return v != "" }) ips2 = langext.ArrFilter(ips2, func(v string) bool { return v != "" }) ips1 = langext.ArrMap(ips1, func(v string) string { return ipExpand(v) }) ips2 = langext.ArrMap(ips2, func(v string) string { return ipExpand(v) }) langext.SortStable(ips1) langext.SortStable(ips2) return langext.CompareArr(ips1, ips2) } func SortLabels(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { lbls1 := langext.ArrMap(langext.MapToArr(v1.Labels), func(v langext.MapEntry[string, string]) string { return fmt.Sprintf("%s\n%s", v.Key, v.Value) }) lbls2 := langext.ArrMap(langext.MapToArr(v2.Labels), func(v langext.MapEntry[string, string]) string { return fmt.Sprintf("%s\t%s", v.Key, v.Value) }) langext.SortStable(lbls1) langext.SortStable(lbls2) return langext.CompareArr(lbls1, lbls2) } func SortLabelKeys(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { lbls1 := langext.ArrMap(langext.MapToArr(v1.Labels), func(v langext.MapEntry[string, string]) string { return v.Key }) lbls2 := langext.ArrMap(langext.MapToArr(v2.Labels), func(v langext.MapEntry[string, string]) string { return v.Key }) langext.SortStable(lbls1) langext.SortStable(lbls2) return langext.CompareArr(lbls1, lbls2) } func SortNetworks(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.ContainerSchema) int { ntwrk1 := langext.ArrMap(langext.MapToArr(v1.NetworkSettings.Networks), func(v langext.MapEntry[string, docker.ContainerSingleNetworkSettings]) string { return v.Key }) ntwrk2 := langext.ArrMap(langext.MapToArr(v2.NetworkSettings.Networks), func(v langext.MapEntry[string, docker.ContainerSingleNetworkSettings]) string { return v.Key }) langext.SortStable(ntwrk1) langext.SortStable(ntwrk2) return langext.CompareArr(ntwrk1, ntwrk2) } // ##################################################################################################################### func getColFun(colkey string) (printer.ColFun, bool) { // Fast branch, simple references to columns for k, v := range ColumnMap { if "{{."+k+"}}" == colkey { return v.Reader, true } } // Slow branch, fully-featured go templates if strings.HasPrefix(colkey, "{{") && strings.HasSuffix(colkey, "}}") { return templateColFun(colkey, ""), true } if splt := strings.SplitN(colkey, ":", 2); len(splt) == 2 && strings.HasPrefix(splt[1], "{{") && strings.HasSuffix(splt[1], "}}") { return templateColFun(splt[1], splt[0]), true } // Fallback, nothing return nil, false } func templateColFun(fmtstr string, header string) printer.ColFun { return func(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) (res []string) { defer func() { if r := recover(); r != nil { ctx.PrintErrorMessage(fmt.Sprintf("Panic in template evaluation of '%s':\n%v", fmtstr, r)) res = []string{"@ERROR"} } }() if cont == nil { return []string{header} } funcs := template.FuncMap{ "join": strings.Join, "array_last": func(v any) any { rval := reflect.ValueOf(v) alen := rval.Len() if alen == 0 { return nil } return rval.Index(alen - 1).Interface() }, "array_slice": func(v any, start int, end int) any { rval := reflect.ValueOf(v) alen := rval.Len() start = max(0, min(alen, start)) end = max(0, min(alen, end)) return rval.Slice(start, end).Interface() }, "in_array": func(compval any, arrval any) (resp bool) { defer func() { if rec := recover(); rec != nil { resp = false } }() v := reflect.ValueOf(arrval) for i := 0; i < v.Len(); i++ { if v.Index(i).Equal(reflect.ValueOf(compval)) { return true } } return false }, "json": func(obj any) string { v, err := json.Marshal(obj) if err != nil { panic(err) } return string(v) }, "json_indent": func(obj any) string { v, err := json.MarshalIndent(obj, "", " ") if err != nil { panic(err) } return string(v) }, "json_pretty": func(v string) string { buffer := &bytes.Buffer{} err := json.Indent(buffer, []byte(v), "", " ") if err != nil { return v } else { return buffer.String() } }, "coalesce": func(val any, def any) any { if langext.IsNil(val) { return def } else { return val } }, "to_string": func(v any) string { return fmt.Sprintf("%v", v) }, "deref": func(vInput any) any { val := reflect.ValueOf(vInput) if val.Kind() == reflect.Ptr { return val.Elem().Interface() } return "" }, "now": func() time.Time { return time.Now() }, "uniqid": func() string { return langext.MustRawHexUUID() }, } templ, err := template.New("col").Funcs(funcs).Parse(fmtstr) if err != nil { ctx.PrintErrorMessage(fmt.Sprintf("Error in template parsing of '%s':\n%v", fmtstr, err.Error())) res = []string{"@ERROR"} } bfr := &bytes.Buffer{} err = templ.Execute(bfr, *cont) if err != nil { ctx.PrintErrorMessage(fmt.Sprintf("Error in template evaluation of '%s':\n%v", fmtstr, err.Error())) res = []string{"@ERROR"} } return strings.Split(bfr.String(), "\n") } } func getSortFun(colkey string) (ColSortFun, bool) { if cdef, ok := ColumnMap[colkey]; ok { return cdef.Sorter, true } return nil, false } func replaceSingleLineColumnData(ctx *cli.PSContext, allData []docker.ContainerSchema, data docker.ContainerSchema, format string) string { r := format for k, v := range ColumnMap { r = strings.ReplaceAll(r, "{{."+k+"}}", strings.Join(v.Reader(ctx, allData, &data), " ")) } return r } func parseTableDef(fmt string) []printer.ColFun { split := regexp.MustCompile("(\\\\t|\\t)").Split(fmt[6:], -1) columns1 := make([]printer.ColFun, 0) for _, v := range split { if cf, ok := getColFun(v); ok { columns1 = append(columns1, cf) } else { columns1 = append(columns1, ColPlaintext(v)) } } return columns1 } func stateColor(state docker.ContainerState, value string) string { switch state { case docker.StateCreated: return termext.Yellow(value) case docker.StateRunning: return termext.Green(value) case docker.StateRestarting: return termext.Yellow(value) case docker.StateExited: return termext.Red(value) case docker.StatePaused: return termext.Yellow(value) case docker.StateDead: return termext.Red(value) } return value } func statusColor(status string, value string) string { if status == "Created" { return termext.Yellow(value) } if strings.HasPrefix(status, "Exited") { return termext.Red(value) } if strings.HasPrefix(status, "Up") { if strings.HasSuffix(status, "(unhealthy)") { return termext.Red(value) } if strings.HasSuffix(status, "(health: starting)") { return termext.Yellow(value) } return termext.Green(value) } return value } func ipExpand(ip string) string { if match, ok := rexIP.MatchFirst(ip); ok { return fmt.Sprintf("%03s.%03s.%03s.%03s", match.GroupByName("b0").Value(), match.GroupByName("b1").Value(), match.GroupByName("b2").Value(), match.GroupByName("b3").Value()) } return ip } ================================================ FILE: impl/impl.go ================================================ package impl import ( "better-docker-ps/cli" "better-docker-ps/docker" "better-docker-ps/printer" "better-docker-ps/pserr" "encoding/json" "git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/mathext" "git.blackforestbytes.com/BlackForestBytes/goext/syncext" "golang.org/x/term" "os" "os/signal" "strings" "syscall" "time" ) func Execute(ctx *cli.PSContext) error { return executeSingle(ctx, false) } func Watch(ctx *cli.PSContext, d time.Duration) error { sigTermChannel := make(chan os.Signal, 8) signal.Notify(sigTermChannel, os.Interrupt, syscall.SIGTERM) for { err := executeSingle(ctx, true) if err != nil { return err } _, isSig := syncext.ReadChannelWithTimeout(sigTermChannel, d) if isSig { ctx.PrintPrimaryOutput("") ctx.PrintPrimaryOutput("Watch canceled with Ctrl+C") return nil } } } func executeSingle(ctx *cli.PSContext, clear bool) error { for _, fmt := range ctx.Opt.Format { if strings.Contains(fmt, "{{.Size}}") { ctx.Opt.WithSize = true } } jsonraw, err := docker.ListContainer(ctx) if err != nil { return err } ctx.PrintVerboseKV("API response", langext.TryPrettyPrintJson(string(jsonraw))) var data []docker.ContainerSchema err = json.Unmarshal(jsonraw, &data) if err != nil { return pserr.DirectOutput.Wrap(err, "Failed to decode Docker API response") } if len(ctx.Opt.SortColumns) > 0 { data = doSort(ctx, data, ctx.Opt.SortColumns, ctx.Opt.SortDirection) } if ctx.Opt.Search != nil { data = doSearch(ctx, data, *ctx.Opt.Search) } for i, v := range ctx.Opt.Format { if clear { ctx.ClearTerminal() } ok, err := doOutput(ctx, data, v, i == len(ctx.Opt.Format)-1) if err != nil { return err } if ok { return nil } } return pserr.DirectOutput.New("Missing format specification for output") } func doSearch(ctx *cli.PSContext, data []docker.ContainerSchema, needle string) []docker.ContainerSchema { needle = strings.ToLower(needle) haystackFormat := "" for _, f := range ctx.Opt.Format { if strings.HasPrefix(f, "table ") { haystackFormat = f break } } result := make([]docker.ContainerSchema, 0, len(data)) for _, cont := range data { hay := cont.ID + " " + strings.Join(cont.Names, " ") + " " + cont.Image + " " + cont.Command if haystackFormat != "" { for _, fn := range parseTableDef(haystackFormat) { hay += " " + strings.Join(fn(ctx, data, &cont), " ") } } else if len(ctx.Opt.Format) > 0 { hay += " " + replaceSingleLineColumnData(ctx, data, cont, ctx.Opt.Format[0]) } if strings.Contains(strings.ToLower(hay), needle) { result = append(result, cont) } } return result } func doSort(ctx *cli.PSContext, data []docker.ContainerSchema, skeys []string, sdirs []cli.SortDirection) []docker.ContainerSchema { langext.SortSliceStable(data, func(v1, v2 docker.ContainerSchema) bool { // return true if v1 < v2 for i := 0; i < len(skeys); i++ { sfn, ok := getSortFun(skeys[i]) if !ok { continue } cmp := sfn(ctx, &v1, &v2) if sdirs[i] == "DESC" { cmp = cmp * -1 } if cmp < 0 { return true } else if cmp > 0 { return false } } return false // equals }) return data } func doOutput(ctx *cli.PSContext, data []docker.ContainerSchema, format string, force bool) (bool, error) { if format == "idlist" { for _, v := range data { if ctx.Opt.Truncate { ctx.PrintPrimaryOutput(v.ID[0:12]) } else { ctx.PrintPrimaryOutput(v.ID) } } return true, nil } else if strings.HasPrefix(format, "table ") { columns := parseTableDef(format) outWidth := printer.Width(ctx, data, columns) if !force { termWidth, _, err := term.GetSize(int(os.Stdout.Fd())) if err == nil && 0 < termWidth && termWidth < outWidth { return false, nil } } printer.Print(ctx, data, columns) return true, nil } else { lines := make([]string, 0) outWidth := 0 for _, v := range data { str := replaceSingleLineColumnData(ctx, data, v, format) lines = append(lines, str) outWidth = mathext.Max(outWidth, printer.RealStrLen(str)) } if !force { termWidth, _, err := term.GetSize(int(os.Stdout.Fd())) if err == nil && 0 < termWidth && termWidth < outWidth { return false, nil } } for _, v := range lines { ctx.PrintPrimaryOutput(v) } return true, nil } } ================================================ FILE: install.sh ================================================ #!/usr/bin/env bash set -euo pipefail # This script handles the installation of 'dops' by detecting the OS # and architecture, downloading the appropriate binary, and configuring the shell PATH. # Reset Color_Off='' # Regular Colors Red='' Green='' Dim='' # White # Bold Bold_Green='' Bold_White='' if [[ -t 1 ]]; then # Reset Color_Off='\033[0m' # Text Reset # Regular Colors Red='\033[0;31m' # Red Green='\033[0;32m' # Green Dim='\033[0;2m' # White # Bold Bold_Green='\033[1;32m' # Bold Green Bold_White='\033[1m' # Bold White fi error() { echo -e "${Red}error${Color_Off}:" "$@" >&2 exit 1 } info() { echo -e "${Dim}$@ ${Color_Off}" } success() { echo -e "${Green}$@ ${Color_Off}" } info_bold() { echo -e "${Bold_White}$@ ${Color_Off}" } # Check if we're on Arch Linux is_arch_linux() { [[ -f /etc/arch-release ]] || command -v pacman >/dev/null 2>&1 } # Detect available AUR helpers in order of preference detect_aur_helper() { local aur_helpers=("yay" "paru" "pikaur" "pamac" "trizen" "yaourt") for helper in "${aur_helpers[@]}"; do if command -v "$helper" >/dev/null 2>&1; then echo "$helper" return 0 fi done return 1 } # Install via AUR install_via_aur() { local aur_helper="$1" info "Installing 'dops' via AUR using ${aur_helper}..." case "$aur_helper" in yay|paru|pikaur) "$aur_helper" -S --noconfirm dops-bin || error "Failed to install dops-bin via $aur_helper" ;; pamac) pamac install --no-confirm dops-bin || error "Failed to install dops-bin via pamac" ;; trizen|yaourt) "$aur_helper" -S --noconfirm dops-bin || error "Failed to install dops-bin via $aur_helper" ;; *) error "Unsupported AUR helper: $aur_helper" ;; esac success "dops was installed successfully via AUR using $aur_helper" echo "Run 'dops --help' to get started" echo info "To use 'dops' as a drop-in replacement for 'docker ps'," info "add the following function to your shell configuration file (e.g., ~/.zshrc, ~/.bashrc):" echo info_bold 'docker() {' info_bold ' case $1 in' info_bold ' ps)' info_bold ' shift' info_bold ' command dops "$@"' info_bold ' ;;' info_bold ' *)' info_bold ' command docker "$@";;' info_bold ' esac' info_bold '}' exit 0 } # Try AUR installation first on Arch Linux if is_arch_linux; then info "Detected Arch Linux, checking for AUR helpers..." if aur_helper=$(detect_aur_helper); then install_via_aur "$aur_helper" else info "No AUR helper found (yay, paru, pikaur, pamac, trizen, yaourt)" info "Falling back to binary installation..." echo fi fi # Check for curl command -v curl >/dev/null || error 'curl is required to install dops' REPO="Mikescher/better-docker-ps" BINARY_NAME="" # Platform detection OS=$(uname -s) ARCH=$(uname -m) info "Detecting platform: ${OS}/${ARCH}..." case "$OS" in Linux) if [ "$ARCH" = "x86_64" ]; then BINARY_NAME="dops_linux-amd64-static" elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then BINARY_NAME="dops_linux-arm64-static" fi ;; Darwin) if [ "$ARCH" = "arm64" ]; then BINARY_NAME="dops_macos-arm64" elif [ "$ARCH" = "x86_64" ]; then error "Intel-based Macs are not supported." fi ;; esac if [ -z "$BINARY_NAME" ]; then error "Unsupported OS or Architecture: ${OS}/${ARCH}" fi DOWNLOAD_URL="https://github.com/${REPO}/releases/latest/download/${BINARY_NAME}" install_env=DOPS_INSTALL bin_env=\$$install_env/bin install_dir=${!install_env:-$HOME/.dops} bin_dir=$install_dir/bin exe=$bin_dir/dops if [[ ! -d $bin_dir ]]; then mkdir -p "$bin_dir" || error "Failed to create install directory \"$bin_dir\"" fi info "Downloading 'dops' from ${DOWNLOAD_URL}..." curl --fail --location --progress-bar --output "$exe" "$DOWNLOAD_URL" || error "Failed to download dops from \"$DOWNLOAD_URL\"" chmod +x "$exe" || error 'Failed to set permissions on dops executable' tildify() { if [[ $1 = $HOME/* ]]; then local replacement=\~/ echo "${1/$HOME\//$replacement}" else echo "$1" fi } success "dops was installed successfully to $Bold_Green$(tildify "$exe")" if command -v dops >/dev/null; then echo "Run 'dops --help' to get started" exit fi refresh_command='' tilde_bin_dir=$(tildify "$bin_dir") quoted_install_dir=\"${install_dir//\"/\\\"}\" if [[ $quoted_install_dir = \"$HOME/* ]]; then quoted_install_dir=${quoted_install_dir/$HOME\//\$HOME/} fi echo case $(basename "$SHELL") in fish) commands=( "set --export $install_env $quoted_install_dir" "set --export PATH $bin_env \$PATH" ) fish_config=$HOME/.config/fish/config.fish tilde_fish_config=$(tildify "$fish_config") if [[ -w $fish_config ]]; then { echo -e '\n# dops' for command in "${commands[@]}"; do echo "$command" done } >>"$fish_config" info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_fish_config\"" refresh_command="source $tilde_fish_config" else echo "Manually add the directory to $tilde_fish_config (or similar):" for command in "${commands[@]}"; do info_bold " $command" done fi ;; zsh) commands=( "export $install_env=$quoted_install_dir" "export PATH=\"$bin_env:\$PATH\"" ) zsh_config=$HOME/.zshrc tilde_zsh_config=$(tildify "$zsh_config") if [[ -w $zsh_config ]]; then { echo -e '\n# dops' for command in "${commands[@]}"; do echo "$command" done } >>"$zsh_config" info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_zsh_config\"" refresh_command="exec $SHELL" else echo "Manually add the directory to $tilde_zsh_config (or similar):" for command in "${commands[@]}"; do info_bold " $command" done fi ;; bash) commands=( "export $install_env=$quoted_install_dir" "export PATH=\"$bin_env:\$PATH\"" ) bash_configs=( "$HOME/.bashrc" "$HOME/.bash_profile" ) if [[ ${XDG_CONFIG_HOME:-} ]]; then bash_configs+=( "$XDG_CONFIG_HOME/.bash_profile" "$XDG_CONFIG_HOME/.bashrc" "$XDG_CONFIG_HOME/bash_profile" "$XDG_CONFIG_HOME/bashrc" ) fi set_manually=true for bash_config in "${bash_configs[@]}"; do tilde_bash_config=$(tildify "$bash_config") if [[ -w $bash_config ]]; then { echo -e '\n# dops' for command in "${commands[@]}"; do echo "$command" done } >>"$bash_config" info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_bash_config\"" refresh_command="source $bash_config" set_manually=false break fi done if [[ $set_manually = true ]]; then echo "Manually add the directory to your shell configuration file (or similar):" for command in "${commands[@]}"; do info_bold " $command" done fi ;; *) echo 'Manually add the directory to your shell configuration file (or similar):' info_bold " export $install_env=$quoted_install_dir" info_bold " export PATH=\"$bin_env:\$PATH\"" ;; esac echo info "To get started, run:" echo if [[ $refresh_command ]]; then info_bold " $refresh_command" fi info_bold " dops --help" echo info "To use 'dops' as a drop-in replacement for 'docker ps'," info "add the following function to your shell configuration file (e.g., ~/.zshrc, ~/.bashrc):" echo info_bold 'docker() {' info_bold ' case $1 in' info_bold ' ps)' info_bold ' shift' info_bold ' command dops "$@"' info_bold ' ;;' info_bold ' *)' info_bold ' command docker "$@";;' info_bold ' esac' info_bold '}' ================================================ FILE: printer/printer.go ================================================ package printer import ( "better-docker-ps/cli" "better-docker-ps/docker" "git.blackforestbytes.com/BlackForestBytes/goext/mathext" "git.blackforestbytes.com/BlackForestBytes/goext/termext" "strings" ) type ColFun = func(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *docker.ContainerSchema) []string func Width(ctx *cli.PSContext, data []docker.ContainerSchema, cols []ColFun) int { var cells = make([][]string, 0) if ctx.Opt.PrintHeader { row := make([]string, 0) for _, fn := range cols { h := fn(ctx, data, nil) row = append(row, h[0]) } cells = append(cells, row) } for _, dat := range data { extrow := make([][]string, 0) maxheight := 1 for _, fn := range cols { h := fn(ctx, data, &dat) extrow = append(extrow, h) maxheight = mathext.Max(maxheight, len(h)) } for yy := 0; yy < maxheight; yy++ { row := make([]string, len(cols)) for xx := 0; xx < len(cols); xx++ { if yy < len(extrow[xx]) { row[xx] = extrow[xx][yy] } } cells = append(cells, row) } } if len(cells) == 0 { return 0 } lens := make([]int, len(cells[0])) for _, row := range cells { for i, cell := range row { lens[i] = mathext.Max(lens[i], RealStrLen(cell)) } } w := 0 for _, v := range lens { w += v } return w + 4*(len(cols)-1) } func Print(ctx *cli.PSContext, data []docker.ContainerSchema, cols []ColFun) { var cells = make([][]string, 0) if ctx.Opt.PrintHeader { row := make([]string, 0) for _, fn := range cols { h := fn(ctx, data, nil) row = append(row, h[0]) } cells = append(cells, row) } for _, dat := range data { extrow := make([][]string, 0) maxheight := 1 for _, fn := range cols { h := fn(ctx, data, &dat) extrow = append(extrow, h) maxheight = mathext.Max(maxheight, len(h)) } for yy := 0; yy < maxheight; yy++ { row := make([]string, len(cols)) for xx := 0; xx < len(cols); xx++ { if yy < len(extrow[xx]) { row[xx] = extrow[xx][yy] } } cells = append(cells, row) } } if len(cells) == 0 { return } lens := make([]int, len(cells[0])) for _, row := range cells { for i, cell := range row { lens[i] = mathext.Max(lens[i], RealStrLen(cell)) } } for rowidx, row := range cells { { rowstr := "" for colidx, cell := range row { if colidx > 0 { rowstr += " " } if colidx == len(row)-1 { rowstr += cell // do not pad last } else { rowstr += TermStrPadRight(cell, " ", lens[colidx]) } } ctx.PrintPrimaryOutput(rowstr) } if ctx.Opt.PrintHeader && ctx.Opt.PrintHeaderLines && rowidx == 0 { rowstr := "" for colidx := range row { if colidx > 0 { rowstr += " " } rowstr += TermStrPadRight("", "-", lens[colidx]) } ctx.PrintPrimaryOutput(rowstr) } } } func RealStrLen(cell string) int { return len([]rune(termext.CleanString(cell))) } func TermStrPadRight(str string, pad string, padlen int) string { if pad == "" { pad = " " } if RealStrLen(str) >= padlen { return str } return str + strings.Repeat(pad, padlen-RealStrLen(str))[0:(padlen-RealStrLen(str))] } ================================================ FILE: pserr/err.go ================================================ package pserr import "github.com/joomcode/errorx" var ( DopsErrors = errorx.NewNamespace("dops") ) var ( DirectOutput = DopsErrors.NewType("direct_out") ) var ( Exitcode = errorx.RegisterProperty("dops.exitcode") ) ================================================ FILE: pserr/util.go ================================================ package pserr import ( "fmt" "github.com/joomcode/errorx" ) func GetDirectOutput(err error) *errorx.Error { sub := err for sub != nil { errx := errorx.Cast(sub) if errx == nil { break } if uw := errx.Unwrap(); uw != nil { sub = uw continue } if errx.Type() == DirectOutput { return errx } sub = errx.Cause() } return nil } func FormatError(err error, verbose bool) string { if errx := GetDirectOutput(err); errx != nil { if verbose { return fmt.Sprintf("%s\n\n%+v", errx.Message(), err) } else { return errx.Message() } } if verbose { return fmt.Sprintf("%+v", err) } else { return err.Error() } } func GetExitCode(err error, fallback int) int { if errx := GetDirectOutput(err); errx != nil { if ec, ok := errx.Property(Exitcode); ok { if eci, ok := ec.(int); ok { return eci } } } return fallback }