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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
================================================
FILE: .idea/inspectionProfiles/Project_Default.xml
================================================
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>
================================================
FILE: .idea/modules.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/better-docker-ps.iml" filepath="$PROJECT_DIR$/.idea/better-docker-ps.iml" />
</modules>
</component>
</project>
================================================
FILE: .idea/vcs.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
================================================
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.

## 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

All (default) columns visible

Output on a medium sized terminal

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 <ftr>, -f <ftr> Filter output based on conditions provided
--search <str>, -g <str> Filter output by substring match across all visible columns (case-insensitive)
--format <fmt> 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 <true|false> Enable/Disable terminal color output
--no-color Disable terminal color output
--socket <filepath> Specify the docker socket location (Default: `auto` - which calls the docker cli to determine the socket)
--timeformat <go-time-fmt> 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 <fmt> You can specify multiple formats and the first one that fits your terminal widt will be used
--sort <col> Sort output by a specific column, use the same identifier as in --format, only useful together with table formats
--sort-direction <ASC|DESC> The sort direction, only useful in combination with --sort
--watch <interval>, -w <interval> 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 <aur@mikescher.com>
# 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 <aur@mikescher.com>
# 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<<version>>/dops_macos-arm64"
sha256 "<<shahash>>"
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>>/${version}/g" dops_patch.rb
sed --regexp-extended -i "s/<<shahash>>/${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 <ftr>, -f <ftr> Filter output based on conditions provided")
ctx.PrintPrimaryOutput(" --search <str>, -g <str> Filter output by substring match across all visible columns (case-insensitive)")
ctx.PrintPrimaryOutput(" --format <fmt> 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 <true|false> Enable/Disable terminal color output")
ctx.PrintPrimaryOutput(" --no-color Disable terminal color output")
ctx.PrintPrimaryOutput(" --socket <filepath> Specify the docker socket location (Default: `auto` - which calls the docker cli to determine the socket)")
ctx.PrintPrimaryOutput(" --timeformat <go-time-fmt> 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 <fmt> You can specify multiple formats and the first one that fits your terminal widt will be used")
ctx.PrintPrimaryOutput(" --sort <col> Sort output by a specific column, use the same identifier as in --format, only useful together with table formats ")
ctx.PrintPrimaryOutput(" --sort-direction <ASC|DESC> The sort direction, only useful in combination with --sort")
ctx.PrintPrimaryOutput(" --watch <interval> 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<b0>[0-9]{1,3})\\.(?P<b1>[0-9]{1,3})\\.(?P<b2>[0-9]{1,3})\\.(?P<b3>[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
}
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
SYMBOL INDEX (138 symbols across 16 files)
FILE: _data/package-data/homebrew/dops.rb
class Dops (line 1) | class Dops < Formula
method install (line 8) | def install
FILE: cli/argTuple.go
type ArgumentTuple (line 3) | type ArgumentTuple struct
FILE: cli/context.go
type PSContext (line 16) | type PSContext struct
method PrintPrimaryOutput (line 22) | func (c PSContext) PrintPrimaryOutput(msg string) {
method PrintFatalMessage (line 30) | func (c PSContext) PrintFatalMessage(msg string) {
method PrintFatalError (line 38) | func (c PSContext) PrintFatalError(e error) {
method PrintErrorMessage (line 46) | func (c PSContext) PrintErrorMessage(msg string) {
method PrintVerbose (line 54) | func (c PSContext) PrintVerbose(msg string) {
method PrintVerboseHeader (line 62) | func (c PSContext) PrintVerboseHeader(msg string) {
method PrintVerboseKV (line 74) | func (c PSContext) PrintVerboseKV(key string, vval any) {
method ClearTerminal (line 106) | func (c PSContext) ClearTerminal() {
method printPrimaryRaw (line 110) | func (c PSContext) printPrimaryRaw(msg string) {
method printErrorRaw (line 118) | func (c PSContext) printErrorRaw(msg string) {
method printVerboseRaw (line 130) | func (c PSContext) printVerboseRaw(msg string) {
method Finish (line 172) | func (c PSContext) Finish() {
method GetIntFromCache (line 176) | func (c *PSContext) GetIntFromCache(key string, calc func() int) int {
function writeStdout (line 142) | func writeStdout(msg string) {
function writeStderr (line 149) | func writeStderr(msg string) {
function NewContext (line 156) | func NewContext(opt Options) (*PSContext, error) {
function NewEarlyContext (line 164) | func NewEarlyContext() *PSContext {
FILE: cli/options.go
type SortDirection (line 16) | type SortDirection
constant SortASC (line 19) | SortASC SortDirection = "ASC"
constant SortDESC (line 20) | SortDESC SortDirection = "DESC"
type Options (line 23) | type Options struct
method GetSocket (line 101) | func (o Options) GetSocket() string {
function DefaultCLIOptions (line 49) | func DefaultCLIOptions() Options {
function getDefaultSocket (line 90) | func getDefaultSocket() string {
type dockerContext (line 141) | type dockerContext struct
method socket (line 152) | func (ctx dockerContext) socket() string {
function p (line 156) | func p(v bool) *bool {
FILE: cli/parser.go
function ParseCommandline (line 19) | func ParseCommandline(columnKeys []string) (Options, error) {
function parseCommandlineInternal (line 27) | func parseCommandlineInternal(columnKeys []string) (Options, error) {
FILE: cmd/dops/main.go
function main (line 17) | func main() {
function printHelp (line 80) | func printHelp(ctx *cli.PSContext) {
FILE: consts/api.go
constant DockerAPIContainerList (line 4) | DockerAPIContainerList = "http://localhost/v1.44/containers/json"
FILE: consts/exitcode.go
constant ExitcodeError (line 4) | ExitcodeError = 60
constant ExitcodePanic (line 5) | ExitcodePanic = 61
constant ExitcodeNoArguments (line 6) | ExitcodeNoArguments = 62
constant ExitcodeCLIParse (line 7) | ExitcodeCLIParse = 63
constant ExitcodeNoLogin (line 8) | ExitcodeNoLogin = 64
constant ExitcodeUnsupportedOutputFormat (line 9) | ExitcodeUnsupportedOutputFormat = 65
constant ExitcodeRecordNotFound (line 10) | ExitcodeRecordNotFound = 66
constant ExitcodeInvalidSession (line 14) | ExitcodeInvalidSession = 81
constant ExitcodePasswordNotFound (line 15) | ExitcodePasswordNotFound = 82
constant ExitcodeParentNotAFolder (line 16) | ExitcodeParentNotAFolder = 83
constant ExitcodeInvalidPosition (line 17) | ExitcodeInvalidPosition = 84
constant ExitcodeBookmarkFieldNotSupported (line 18) | ExitcodeBookmarkFieldNotSupported = 85
FILE: consts/version.go
constant BETTER_DOCKER_PS_VERSION (line 5) | BETTER_DOCKER_PS_VERSION = "1.17"
FILE: docker/api.go
function ListContainer (line 21) | func ListContainer(ctx *cli.PSContext) ([]byte, error) {
FILE: docker/schema.go
type ContainerSchema (line 8) | type ContainerSchema struct
method PortsSorted (line 26) | func (s ContainerSchema) PortsSorted() []PortSchema {
type ContainerHostConfig (line 42) | type ContainerHostConfig struct
type ContainerNetworkSettings (line 46) | type ContainerNetworkSettings struct
type ContainerSingleNetworkSettings (line 49) | type ContainerSingleNetworkSettings struct
type PortSchema (line 61) | type PortSchema struct
method IsLoopback (line 68) | func (s PortSchema) IsLoopback() bool {
type ContainerMount (line 73) | type ContainerMount struct
type ContainerState (line 83) | type ContainerState
method Num (line 94) | func (ct ContainerState) Num() int {
constant StateCreated (line 86) | StateCreated ContainerState = "created"
constant StateRunning (line 87) | StateRunning ContainerState = "running"
constant StateRestarting (line 88) | StateRestarting ContainerState = "restarting"
constant StateExited (line 89) | StateExited ContainerState = "exited"
constant StatePaused (line 90) | StatePaused ContainerState = "paused"
constant StateDead (line 91) | StateDead ContainerState = "dead"
FILE: docker/util.go
function SplitDockerImage (line 16) | func SplitDockerImage(ctx *cli.PSContext, img string) (string, string, s...
FILE: impl/columns.go
type ColumnDef (line 27) | type ColumnDef struct
function ColContainerID (line 62) | func ColContainerID(ctx *cli.PSContext, allData []docker.ContainerSchema...
function ColFullImage (line 74) | func ColFullImage(ctx *cli.PSContext, allData []docker.ContainerSchema, ...
function ColRegistry (line 82) | func ColRegistry(ctx *cli.PSContext, allData []docker.ContainerSchema, c...
function ColImage (line 92) | func ColImage(ctx *cli.PSContext, allData []docker.ContainerSchema, cont...
function ColImageTag (line 102) | func ColImageTag(ctx *cli.PSContext, allData []docker.ContainerSchema, c...
function ColCommand (line 112) | func ColCommand(ctx *cli.PSContext, allData []docker.ContainerSchema, co...
function ColShortCommand (line 127) | func ColShortCommand(ctx *cli.PSContext, allData []docker.ContainerSchem...
function ColRunningFor (line 141) | func ColRunningFor(ctx *cli.PSContext, allData []docker.ContainerSchema,...
function ColCreatedAt (line 152) | func ColCreatedAt(ctx *cli.PSContext, allData []docker.ContainerSchema, ...
function ColState (line 170) | func ColState(ctx *cli.PSContext, allData []docker.ContainerSchema, cont...
function ColStatus (line 184) | func ColStatus(ctx *cli.PSContext, allData []docker.ContainerSchema, con...
function ColPortsExposed (line 196) | func ColPortsExposed(ctx *cli.PSContext, allData []docker.ContainerSchem...
function ColPortsPublicPart (line 225) | func ColPortsPublicPart(ctx *cli.PSContext, allData []docker.ContainerSc...
function ColPortsPublished (line 245) | func ColPortsPublished(ctx *cli.PSContext, allData []docker.ContainerSch...
function ColPortsPublishedShort (line 291) | func ColPortsPublishedShort(ctx *cli.PSContext, allData []docker.Contain...
function ColPortsPublishedLong (line 334) | func ColPortsPublishedLong(ctx *cli.PSContext, allData []docker.Containe...
function ColPortsNotPublished (line 388) | func ColPortsNotPublished(ctx *cli.PSContext, allData []docker.Container...
function ColName (line 410) | func ColName(ctx *cli.PSContext, allData []docker.ContainerSchema, cont ...
function ColSize (line 426) | func ColSize(ctx *cli.PSContext, allData []docker.ContainerSchema, cont ...
function ColMounts (line 438) | func ColMounts(ctx *cli.PSContext, allData []docker.ContainerSchema, con...
function ColIP (line 455) | func ColIP(ctx *cli.PSContext, allData []docker.ContainerSchema, cont *d...
function ColLabels (line 470) | func ColLabels(ctx *cli.PSContext, allData []docker.ContainerSchema, con...
function ColLabelKeys (line 483) | func ColLabelKeys(ctx *cli.PSContext, allData []docker.ContainerSchema, ...
function ColNetworks (line 496) | func ColNetworks(ctx *cli.PSContext, allData []docker.ContainerSchema, c...
function ColPlaintext (line 509) | func ColPlaintext(str string) printer.ColFun {
function SortContainerID (line 517) | func SortContainerID(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 ...
function SortFullImage (line 525) | func SortFullImage(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *d...
function SortRegistry (line 529) | func SortRegistry(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *do...
function SortImage (line 536) | func SortImage(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docke...
function SortImageTag (line 543) | func SortImageTag(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *do...
function SortCommand (line 550) | func SortCommand(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *doc...
function SortShortCommand (line 554) | func SortShortCommand(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2...
function SortRunningFor (line 570) | func SortRunningFor(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *...
function SortCreatedAt (line 574) | func SortCreatedAt(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *d...
function SortState (line 578) | func SortState(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docke...
function SortStatus (line 582) | func SortStatus(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *dock...
function SortPortsExposed (line 586) | func SortPortsExposed(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2...
function SortPortsPublished (line 606) | func SortPortsPublished(ctx *cli.PSContext, v1 *docker.ContainerSchema, ...
function SortPortsPublishedShort (line 629) | func SortPortsPublishedShort(ctx *cli.PSContext, v1 *docker.ContainerSch...
function SortPortsPublishedLong (line 652) | func SortPortsPublishedLong(ctx *cli.PSContext, v1 *docker.ContainerSche...
function SortPortsNotPublished (line 675) | func SortPortsNotPublished(ctx *cli.PSContext, v1 *docker.ContainerSchem...
function SortPortsPublicPart (line 698) | func SortPortsPublicPart(ctx *cli.PSContext, v1 *docker.ContainerSchema,...
function SortName (line 721) | func SortName(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker...
function SortSize (line 731) | func SortSize(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker...
function SortMounts (line 735) | func SortMounts(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *dock...
function SortIP (line 749) | func SortIP(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *docker.C...
function SortLabels (line 777) | func SortLabels(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *dock...
function SortLabelKeys (line 791) | func SortLabelKeys(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *d...
function SortNetworks (line 805) | func SortNetworks(ctx *cli.PSContext, v1 *docker.ContainerSchema, v2 *do...
function getColFun (line 821) | func getColFun(colkey string) (printer.ColFun, bool) {
function templateColFun (line 846) | func templateColFun(fmtstr string, header string) printer.ColFun {
function getSortFun (line 957) | func getSortFun(colkey string) (ColSortFun, bool) {
function replaceSingleLineColumnData (line 964) | func replaceSingleLineColumnData(ctx *cli.PSContext, allData []docker.Co...
function parseTableDef (line 976) | func parseTableDef(fmt string) []printer.ColFun {
function stateColor (line 989) | func stateColor(state docker.ContainerState, value string) string {
function statusColor (line 1007) | func statusColor(status string, value string) string {
function ipExpand (line 1030) | func ipExpand(ip string) string {
FILE: impl/impl.go
function Execute (line 20) | func Execute(ctx *cli.PSContext) error {
function Watch (line 24) | func Watch(ctx *cli.PSContext, d time.Duration) error {
function executeSingle (line 46) | func executeSingle(ctx *cli.PSContext, clear bool) error {
function doSearch (line 93) | func doSearch(ctx *cli.PSContext, data []docker.ContainerSchema, needle ...
function doSort (line 121) | func doSort(ctx *cli.PSContext, data []docker.ContainerSchema, skeys []s...
function doOutput (line 152) | func doOutput(ctx *cli.PSContext, data []docker.ContainerSchema, format ...
FILE: printer/printer.go
function Width (line 13) | func Width(ctx *cli.PSContext, data []docker.ContainerSchema, cols []Col...
function Print (line 68) | func Print(ctx *cli.PSContext, data []docker.ContainerSchema, cols []Col...
function RealStrLen (line 147) | func RealStrLen(cell string) int {
function TermStrPadRight (line 151) | func TermStrPadRight(str string, pad string, padlen int) string {
FILE: pserr/util.go
function GetDirectOutput (line 8) | func GetDirectOutput(err error) *errorx.Error {
function FormatError (line 33) | func FormatError(err error, verbose bool) string {
function GetExitCode (line 49) | func GetExitCode(err error, fallback int) int {
Condensed preview — 38 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (129K chars).
[
{
"path": ".gitignore",
"chars": 779,
"preview": "\n########## GOLAND ##########\n\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictiona"
},
{
"path": ".idea/.gitignore",
"chars": 176,
"preview": "# Default ignored files\n/shelf/\n/workspace.xml\n# Editor-based HTTP Client requests\n/httpRequests/\n# Datasource local sto"
},
{
"path": ".idea/better-docker-ps.iml",
"chars": 322,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"WEB_MODULE\" version=\"4\">\n <component name=\"Go\" enabled=\"true\" />\n "
},
{
"path": ".idea/inspectionProfiles/Project_Default.xml",
"chars": 562,
"preview": "<component name=\"InspectionProjectProfileManager\">\n <profile version=\"1.0\">\n <option name=\"myName\" value=\"Project De"
},
{
"path": ".idea/modules.xml",
"chars": 284,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"ProjectModuleManager\">\n <modules>\n "
},
{
"path": ".idea/vcs.xml",
"chars": 180,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"VcsDirectoryMappings\">\n <mapping dire"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "MIT License\n\nCopyright (c) 2022 Mike Schwörer\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
},
{
"path": "Makefile",
"chars": 2790,
"preview": "build:\n\tgo generate ./...\n\tCGO_ENABLED=0 go build -o _out/dops ./cmd/dops\n\nrun: build\n\t./_out/dops\n\nclean:\n\tgo clean\n\trm"
},
{
"path": "README.md",
"chars": 10576,
"preview": "# ./dops - better `docker ps` \nA replacement for the default docker-ps that tries really hard to fit within your termina"
},
{
"path": "_data/package-data/aur-bin/.gitignore",
"chars": 54,
"preview": "pkg/\nsrc/\ndops/\ndops-bin/\ndops-git/\n.SRCINFO\n*.tar.zst"
},
{
"path": "_data/package-data/aur-bin/PKGBUILD",
"chars": 664,
"preview": "# Maintainer: Mikescher <aur@mikescher.com>\n# Repo: https://github.com/Mikescher/better-docker-ps\n\npkgname=dops-bi"
},
{
"path": "_data/package-data/aur-bin.sh",
"chars": 1438,
"preview": "#!/bin/bash\n\nset -o nounset # disallow usage of unset vars ( set -u )\nset -o errexit # Exit immediately if a pipeli"
},
{
"path": "_data/package-data/aur-git/.gitignore",
"chars": 54,
"preview": "pkg/\nsrc/\ndops/\ndops-bin/\ndops-git/\n.SRCINFO\n*.tar.zst"
},
{
"path": "_data/package-data/aur-git/PKGBUILD",
"chars": 604,
"preview": "# Maintainer: Mikescher <aur@mikescher.com>\n# Repo: https://github.com/Mikescher/better-docker-ps\n\npkgname=dops-gi"
},
{
"path": "_data/package-data/aur-git.sh",
"chars": 1210,
"preview": "#!/bin/bash\n\nset -o nounset # disallow usage of unset vars ( set -u )\nset -o errexit # Exit immediately if a pipeli"
},
{
"path": "_data/package-data/homebrew/dops.rb",
"chars": 435,
"preview": "class Dops < Formula\n\n desc \" A replacement for the default docker-ps that tries really hard to fit into the width "
},
{
"path": "_data/package-data/homebrew.sh",
"chars": 1269,
"preview": "#!/bin/bash\n\nset -o nounset # disallow usage of unset vars ( set -u )\nset -o errexit # Exit immediately if a pipeli"
},
{
"path": "_data/package-data/sanitycheck.sh",
"chars": 517,
"preview": "#!/bin/bash\n\ncd \"$(dirname \"$0\")\"\n\nversion_tag=\"$(cd ../../ && git tag --sort=-v:refname | grep -P 'v[0-9\\.]' | head -1 "
},
{
"path": "cli/argTuple.go",
"chars": 72,
"preview": "package cli\n\ntype ArgumentTuple struct {\n\tKey string\n\tValue *string\n}\n"
},
{
"path": "cli/context.go",
"chars": 3299,
"preview": "package cli\n\nimport (\n\t\"better-docker-ps/pserr\"\n\t\"context\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"git.blackf"
},
{
"path": "cli/options.go",
"chars": 4499,
"preview": "package cli\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"git.blackforest"
},
{
"path": "cli/parser.go",
"chars": 13285,
"preview": "package cli\n\nimport (\n\t\"better-docker-ps/pserr\"\n\t\"fmt\"\n\t\"github.com/BurntSushi/toml\"\n\t\"github.com/kirsle/configdir\"\n\t\"gi"
},
{
"path": "cmd/dops/main.go",
"chars": 7169,
"preview": "package main\n\nimport (\n\t\"better-docker-ps/cli\"\n\t\"better-docker-ps/consts\"\n\t\"better-docker-ps/impl\"\n\t\"better-docker-ps/ps"
},
{
"path": "consts/api.go",
"chars": 201,
"preview": "package consts\n\n// DockerAPIContainerList -> see https://docs.docker.com/engine/api/v1.41/#tag/Container/operation/Conta"
},
{
"path": "consts/exitcode.go",
"chars": 503,
"preview": "package consts\n\nconst (\n\tExitcodeError = 60\n\tExitcodePanic = 61\n\tExitcodeNoArguments"
},
{
"path": "consts/version.go",
"chars": 92,
"preview": "package consts\n\n//go:generate /bin/bash version.sh\n\nconst BETTER_DOCKER_PS_VERSION = \"1.17\"\n"
},
{
"path": "consts/version.sh",
"chars": 161,
"preview": "#!/bin/bash\n\nsed -i 's/const BETTER_DOCKER_PS_VERSION = \".*\"/const BETTER_DOCKER_PS_VERSION = \"'$(git describe --tags --"
},
{
"path": "docker/api.go",
"chars": 1619,
"preview": "package docker\n\nimport (\n\t\"better-docker-ps/cli\"\n\t\"better-docker-ps/consts\"\n\t\"better-docker-ps/pserr\"\n\t\"context\"\n\t\"encod"
},
{
"path": "docker/schema.go",
"chars": 3178,
"preview": "package docker\n\nimport (\n\t\"git.blackforestbytes.com/BlackForestBytes/goext/langext\"\n\t\"net\"\n)\n\ntype ContainerSchema struc"
},
{
"path": "docker/util.go",
"chars": 1005,
"preview": "package docker\n\nimport (\n\t\"better-docker-ps/cli\"\n\t\"strings\"\n)\n\nvar registryPrefixList = []string{\n\t\".com\",\n\t\".de\",\n\t\".ne"
},
{
"path": "go.mod",
"chars": 1790,
"preview": "module better-docker-ps\n\ngo 1.24.2\n\nrequire (\n\tgit.blackforestbytes.com/BlackForestBytes/goext v0.0.572\n\tgithub.com/Burn"
},
{
"path": "go.sum",
"chars": 11156,
"preview": "git.blackforestbytes.com/BlackForestBytes/goext v0.0.572 h1:NALJ4KKkrRZcNJNsmGrUsjFdOclHSA/KyB6f94QV43k=\ngit.blackforest"
},
{
"path": "impl/columns.go",
"chars": 29142,
"preview": "package impl\n\nimport (\n\t\"better-docker-ps/cli\"\n\t\"better-docker-ps/docker\"\n\t\"better-docker-ps/printer\"\n\t\"bytes\"\n\t\"encodin"
},
{
"path": "impl/impl.go",
"chars": 4412,
"preview": "package impl\n\nimport (\n\t\"better-docker-ps/cli\"\n\t\"better-docker-ps/docker\"\n\t\"better-docker-ps/printer\"\n\t\"better-docker-ps"
},
{
"path": "install.sh",
"chars": 8390,
"preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# This script handles the installation of 'dops' by detecting the OS\n# and archit"
},
{
"path": "printer/printer.go",
"chars": 3147,
"preview": "package printer\n\nimport (\n\t\"better-docker-ps/cli\"\n\t\"better-docker-ps/docker\"\n\t\"git.blackforestbytes.com/BlackForestBytes"
},
{
"path": "pserr/err.go",
"chars": 222,
"preview": "package pserr\n\nimport \"github.com/joomcode/errorx\"\n\nvar (\n\tDopsErrors = errorx.NewNamespace(\"dops\")\n)\n\nvar (\n\tDirectOutp"
},
{
"path": "pserr/util.go",
"chars": 884,
"preview": "package pserr\n\nimport (\n\t\"fmt\"\n\t\"github.com/joomcode/errorx\"\n)\n\nfunc GetDirectOutput(err error) *errorx.Error {\n\n\tsub :="
}
]
About this extraction
This page contains the full source code of the Mikescher/better-docker-ps GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 38 files (114.5 KB), approximately 37.3k tokens, and a symbol index with 138 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.