From 304fcfea4c202a99fbb997740b3f7771bd2878ec Mon Sep 17 00:00:00 2001 From: jacobi petrucciani Date: Fri, 25 Oct 2024 22:50:10 -0400 Subject: [PATCH] initial commit --- .envrc | 1 + .github/dependabot.yml | 6 + .github/workflows/check.yml | 21 + .gitignore | 5 + README.md | 234 ++++++++++ builtin/default.nix | 2 + default.nix | 6 + flake.lock | 61 +++ flake.nix | 36 ++ pog/bashbible.nix | 365 +++++++++++++++ pog/default.nix | 857 ++++++++++++++++++++++++++++++++++++ 11 files changed, 1594 insertions(+) create mode 100644 .envrc create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/check.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 builtin/default.nix create mode 100644 default.nix create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 pog/bashbible.nix create mode 100644 pog/default.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6778b04 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..27461ba --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,21 @@ +name: check +on: {workflow_dispatch, push: {branches: [main]}, pull_request} +jobs: + nixpkgs-fmt: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4.2.2 + - uses: cachix/install-nix-action@v30 + - run: nix develop -c nixpkgs-fmt --check . + statix: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4.2.2 + - uses: cachix/install-nix-action@v30 + - run: nix develop -c statix check + deadnix: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4.2.2 + - uses: cachix/install-nix-action@v30 + - run: nix develop -c deadnix -f -_ -l . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc59666 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +result* +.direnv + +# bun/node +node_modules/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..42b64d7 --- /dev/null +++ b/README.md @@ -0,0 +1,234 @@ +# pog + +[![uses nix](https://img.shields.io/badge/uses-nix-%237EBAE4)](https://nixos.org/) + +`pog` is a [nix](https://nixos.org/) library that enables you to create comprehensive CLI tools with rich features like flag parsing, auto-documentation, tab completion, and interactive prompts - all purely in Nix leveraging the vast ecosystem of [nixpkgs](https://github.com/NixOS/nixpkgs). + +## Features + +- 🚀 Create full-featured CLI tools in pure Nix (and bash) +- 📖 Automatic help text generation and documentation +- 🔄 Tab completion out of the box +- 🎯 Interactive prompting capabilities +- 🎨 Rich terminal colors and styling +- 🛠 Comprehensive flag system with: + - Short and long flag options + - Environment variable overrides + - Default values + - Required flags with prompts + - Boolean flags + - Custom completion functions +- ⚡ Runtime input management +- 🔍 Verbose mode support +- 🎭 Color toggle support +- 🧰 Helper functions for common operations + - `debug` for included verbose flag + - `die` for exiting with a message and custom exit code + - much more! + +## Quick Start + +regular import: + +```nix +let + pog = import (fetchTarball { + name = "pog-2024-10-25"; + url = "https://github.com/jpetrucciani/pog/main.tar.gz"; # note, you'll probably want to grab a commit sha for this instead of `main`! + sha256 = ""; # this is necessary, but you can find it by letting nix try to evaluate this! + }) {}; +in +pog.pog { + name = "meme"; + description = "A helpful CLI tool"; + flags = [ + { + name = "config"; + description = "path to config file"; + argument = "FILE"; + } + ]; + script = '' + echo "Config file: $config" + debug "Verbose mode enabled" + echo "this is a cool tool!" + ''; +} +``` + +or if you want to add it as an overlay to nixpkgs, you can add `pog.overlay` in your overlays for nixpkgs! + +using flakes: + +```nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + pog.url = "github:jpetrucciani/pog"; + }; + outputs = { self, nixpkgs, pog, ... }: + let + system = "x86_64-linux"; + in + { + packages = nixpkgs { inherit system; overlays = [ pog.overlay ]; }; + }; +} +``` + +## API Reference + +### Main Function (`pog {}`) + +The main function accepts the following arguments: + +```nix +pog { + # Required + name = "tool-name"; # Name of your CLI tool + script = '' + echo "hello, world!" + ''; # Bash script or function that uses helpers + + # Optional + version = "0.0.0"; # Version of your tool + description = "..."; # Tool description + flags = [ ]; # List of flag definitions + arguments = [ ]; # Positional arguments + argumentCompletion = "files"; # Completion for positional args + runtimeInputs = [ ]; # Runtime dependencies + bashBible = false; # Include bash-bible helpers + beforeExit = ""; # Code to run before exit + strict = false; # Enable strict bash mode + flagPadding = 20; # Padding for help text + showDefaultFlags = false; # Show built-in flags in usage + shortDefaultFlags = true; # Enable short versions of default flags +} +``` + +### Flag Definition + +Flags are defined using the following schema: + +```nix +{ + # Required + name = "flag-name"; # Name of the flag + + # Optional + short = "f"; # Single-char short version + description = "..."; # Flag description + default = ""; # Default value + bool = false; # Is this a boolean flag? + argument = "VAR"; # Argument name in help text + envVar = "POG_FLAG_NAME"; # Override env variable + required = false; # Is this flag required? + prompt = ""; # Interactive prompt command + promptError = "..."; # Error message for failed prompt + completion = ""; # Tab completion command + flagPadding = 20; # Help text padding +} +``` + +### Built-in Flag Features + +- Environment variable overrides: Each flag can be set via environment variable +- Default values: Flags can have default values +- Required flags: Mark flags as required with custom error messages +- Boolean flags: Simple on/off flags +- Custom completion: Define custom tab completion for each flag +- Interactive prompts: Add interactive selection for flag values + +### Helper Functions + +pog provides various helper functions for common operations: + +```nix +helpers = { + fn = { + add = "..."; # Addition helper + sub = "..."; # Subtraction helper + ts_to_seconds = "..."; # Timestamp conversion + }; + var = { + empty = name: "..."; # Check if variable is empty + notEmpty = name: "..."; # Check if variable is not empty + }; + file = { + exists = name: "..."; # Check if file exists + notExists = name: "..."; # Check if file doesn't exist + empty = name: "..."; # Check if file is empty + notEmpty = name: "..."; # Check if file is not empty + }; + # ... and more +}; +``` + +You can use these helpers by making `script` a function that takes an arg: + +```nix +script = helpers: '' + ${helpers.flag "force"} && debug "executed with --force flag!" +''; +``` + +## Example + +Here's a bit more complete example showing various features: + +```nix +pog { + name = "deploy"; + description = "Deploy application to cloud"; + flags = [ + pog._.flags.aws.region # this is a predefined flag from this repo, with tab completion! + { + name = "environment"; + short = "e"; + description = "deployment environment"; + required = true; + completion = ''echo "dev staging prod"''; + } + { + name = "force"; + bool = true; + description = "skip confirmation prompts"; + } + ]; + runtimeInputs = with pkgs; [ + awscli2 + kubectl + ]; + script = helpers: with helpers; '' + if ${flag "force"}; then + debug "forcing deployment!" + ${confirm { prompt = "Ready to deploy?"; }} + fi + + ${spinner { + command = "kubectl apply -f ./manifests/"; + title = "Deploying..."; + }} + + green "Deployment complete!" + ''; +} +``` + +## More (useful) examples + +for more comprehensive examples, check out [this directory in my main nix repo!](https://github.com/jpetrucciani/nix/tree/main/mods/pog) + +## Terminal Colors and Styling + +pog includes comprehensive terminal styling capabilities: + +- Text colors: black, red, green, yellow, blue, purple, cyan, grey +- Background colors: red_bg, green_bg, yellow_bg, blue_bg, purple_bg, cyan_bg, grey_bg +- Styles: bold, dim, italic, underlined, blink, invert, hidden + +Colors can be disabled globally using `--no-color` or the `NO_COLOR` environment variable. + +## Contributing + +Feel free to open issues and pull requests! We welcome contributions to make pog even more powerful/useful. diff --git a/builtin/default.nix b/builtin/default.nix new file mode 100644 index 0000000..72c3ea7 --- /dev/null +++ b/builtin/default.nix @@ -0,0 +1,2 @@ +# TODO: add some stuff here! +_: { } diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..642cead --- /dev/null +++ b/default.nix @@ -0,0 +1,6 @@ +{ pkgs ? import { }, system ? pkgs.system }: +let + params = { inherit pkgs system; }; + pog = import ./pog params; +in +pog diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..29505fc --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1723362943, + "narHash": "sha256-dFZRVSgmJkyM0bkPpaYRtG/kRMRTorUIDj8BxoOt1T4=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "a58bc8ad779655e790115244571758e8de055e3d", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a70c970 --- /dev/null +++ b/flake.nix @@ -0,0 +1,36 @@ +{ + description = "pog"; + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { nixpkgs, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + pog = import ./. { + inherit pkgs system; + }; + params = { + inherit pkgs; + inherit (pog) _ pog; + }; + builtin = import ./builtin params; + in + rec { + packages = { inherit pog builtin; }; + defaultPackage = packages.pog; + + devShells = { + default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + bun + deadnix + nixpkgs-fmt + statix + ]; + }; + }; + }); +} diff --git a/pog/bashbible.nix b/pog/bashbible.nix new file mode 100644 index 0000000..ec6883e --- /dev/null +++ b/pog/bashbible.nix @@ -0,0 +1,365 @@ +# bash bible functions implemented as a set of attribute sets in nix +# https://github.com/dylanaraps/pure-bash-bible +{ pkgs, ... }: +let + inherit (pkgs.lib) attrValues concatStrings; +in +rec { + functions = { + strings = { + trim_string = '' + trim_string() { + # Usage: trim_string " example string " + : "''${1#"''${1%%[![:space:]]*}"}" + : "''${_%"''${_##*[![:space:]]}"}" + printf '%s\n' "$_" + } + ''; + trim_all = '' + # shellcheck disable=SC2086,SC2048 + trim_all() { + # Usage: trim_all " example string " + set -f + set -- $* + printf '%s\n' "$*" + set +f + } + ''; + regex = '' + regex() { + # Usage: regex "string" "regex" + [[ $1 =~ $2 ]] && printf '%s\n' "''${BASH_REMATCH[1]}" + } + ''; + split = '' + split() { + # Usage: split "string" "delimiter" + IFS=$'\n' read -d "" -ra arr <<< "''${1//$2/$'\n'}" + printf '%s\n' "''${arr[@]}" + } + ''; + lower = '' + lower() { + # Usage: lower "string" + printf '%s\n' "''${1,,}" + } + ''; + upper = '' + upper() { + # Usage: upper "string" + printf '%s\n' "''${1^^}" + } + ''; + reverse_case = '' + reverse_case() { + # Usage: reverse_case "string" + printf '%s\n' "''${1~~}" + } + ''; + trim_quotes = '' + trim_quotes() { + # Usage: trim_quotes "string" + : "''${1//\'}" + printf '%s\n' "''${_//\"}" + } + ''; + strip_all = '' + strip_all() { + # Usage: strip_all "string" "pattern" + printf '%s\n' "''${1//$2}" + } + ''; + strip = '' + strip() { + # Usage: strip "string" "pattern" + printf '%s\n' "''${1/$2}" + } + ''; + lstrip = '' + # shellcheck disable=SC2295 + lstrip() { + # Usage: lstrip "string" "pattern" + printf '%s\n' "''${1##$2}" + } + ''; + rstrip = '' + # shellcheck disable=SC2295 + rstrip() { + # Usage: rstrip "string" "pattern" + printf '%s\n' "''${1%%$2}" + } + ''; + urlencode = '' + urlencode() { + # Usage: urlencode "string" + local LC_ALL=C + for (( i = 0; i < ''${#1}; i++ )); do + : "''${1:i:1}" + case "$_" in + [a-zA-Z0-9.~_-]) + printf '%s' "$_" + ;; + + *) + printf '%%%02X' "'$_" + ;; + esac + done + printf '\n' + } + ''; + urldecode = '' + urldecode() { + # Usage: urldecode "string" + : "''${1//+/ }" + printf '%b\n' "''${_//%/\\x}" + } + ''; + }; + arrays = { + reverse_array = '' + reverse_array() { + # Usage: reverse_array "array" + shopt -s extdebug + f()(printf '%s\n' "''${BASH_ARGV[@]}"); f "$@" + shopt -u extdebug + } + ''; + remove_array_dups = '' + remove_array_dups() { + # Usage: remove_array_dups "array" + declare -A tmp_array + + for i in "$@"; do + [[ $i ]] && IFS=" " tmp_array["''${i:- }"]=1 + done + + printf '%s\n' "''${!tmp_array[@]}" + } + ''; + random_array_element = '' + random_array_element() { + # Usage: random_array_element "array" + local arr=("$@") + printf '%s\n' "''${arr[RANDOM % $#]}" + } + ''; + cycle = '' + cycle() { + printf '%s ' "''${arr[''${i:=0}]}" + ((i=i>=''${#arr[@]}-1?0:++i)) + } + ''; + }; + files = { + head = '' + head() { + # Usage: head "n" "file" + mapfile -tn "$1" line < "$2" + printf '%s\n' "''${line[@]}" + } + ''; + tail = '' + tail() { + # Usage: tail "n" "file" + mapfile -tn 0 line < "$2" + printf '%s\n' "''${line[@]: -$1}" + } + ''; + lines = '' + lines() { + # Usage: lines "file" + mapfile -tn 0 lines < "$1" + printf '%s\n' "''${#lines[@]}" + } + ''; + count = '' + count() { + # Usage: count /path/to/dir/* + # count /path/to/dir/*/ + printf '%s\n' "$#" + } + ''; + extract = '' + extract() { + # Usage: extract file "opening marker" "closing marker" + while IFS=$'\n' read -r line; do + [[ $extract && $line != "$3" ]] && + printf '%s\n' "$line" + + [[ $line == "$2" ]] && extract=1 + [[ $line == "$3" ]] && extract= + done < "$1" + } + ''; + }; + paths = { + dirname = '' + dirname() { + # Usage: dirname "path" + local tmp=''${1:-.} + + [[ $tmp != *[!/]* ]] && { + printf '/\n' + return + } + + tmp=''${tmp%%"''${tmp##*[!/]}"} + + [[ $tmp != */* ]] && { + printf '.\n' + return + } + + tmp=''${tmp%/*} + tmp=''${tmp%%"''${tmp##*[!/]}"} + + printf '%s\n' "''${tmp:-/}" + } + ''; + basename = '' + basename() { + # Usage: basename "path" ["suffix"] + local tmp + + tmp=''${1%"''${1##*[!/]}"} + tmp=''${tmp##*/} + tmp=''${tmp%"''${2/"$tmp"}"} + + printf '%s\n' "''${tmp:-/}" + } + ''; + }; + terminal = { + get_term_size = '' + get_term_size() { + # Usage: get_term_size + + # (:;:) is a micro sleep to ensure the variables are + # exported immediately. + shopt -s checkwinsize; (:;:) + printf '%s\n' "$LINES $COLUMNS" + } + ''; + get_window_size = '' + # shellcheck disable=SC2141 + get_window_size() { + # Usage: get_window_size + printf '%b' "''${TMUX:+\\ePtmux;\\e}\\e[14t''${TMUX:+\\e\\\\}" + IFS=';t' read -d t -t 0.05 -sra term_size + printf '%s\n' "''${term_size[1]}x''${term_size[2]}" + } + ''; + get_cursor_pos = '' + get_cursor_pos() { + # Usage: get_cursor_pos + IFS='[;' read -p $'\e[6n' -d R -rs _ y x _ + printf '%s\n' "$x $y" + } + ''; + }; + conversion = { + hex_to_rgb = '' + hex_to_rgb() { + # Usage: hex_to_rgb "#FFFFFF" + # hex_to_rgb "000000" + : "''${1/\#}" + ((r=16#''${_:0:2},g=16#''${_:2:2},b=16#''${_:4:2})) + printf '%s\n' "$r $g $b" + } + ''; + rgb_to_hex = '' + rgb_to_hex() { + # Usage: rgb_to_hex "r" "g" "b" + printf '#%02x%02x%02x\n' "$1" "$2" "$3" + } + ''; + }; + other = { + read_sleep = '' + read_sleep() { + # Usage: read_sleep 1 + # read_sleep 0.2 + read -rt "$1" <> <(:) || : + } + ''; + date = '' + date() { + # Usage: date "format" + # See: 'man strftime' for format. + printf "%($1)T\\n" "-1" + } + ''; + uuid = '' + uuid() { + # Usage: uuid + C="89ab" + + for ((N=0;N<16;++N)); do + B="$((RANDOM%256))" + + case "$N" in + 6) printf '4%x' "$((B%16))" ;; + 8) printf '%c%x' "''${C:$RANDOM%''${#C}:1}" "$((B%16))" ;; + + 3|5|7|9) + printf '%02x-' "$B" + ;; + + *) + printf '%02x' "$B" + ;; + esac + done + + printf '\n' + } + ''; + bar = '' + bar() { + # Usage: bar 1 10 + # ^----- Elapsed Percentage (0-100). + # ^-- Total length in chars. + ((elapsed=$1*$2/100)) + + # Create the bar with spaces. + printf -v prog "%''${elapsed}s" + printf -v total "%$(($2-elapsed))s" + + printf '%s\r' "[''${prog// /-}''${total}]" + } + ''; + get_functions = '' + get_functions() { + # Usage: get_functions + IFS=$'\n' read -d "" -ra functions < <(declare -F) + printf '%s\n' "''${functions[@]//declare -f }" + } + ''; + bkr = '' + bkr() { + (nohup "$@" &>/dev/null &) + } + ''; + }; + }; + + bible = '' + ## bash bible + ### strings + ${concatStrings (attrValues functions.strings)} + ### arrays + ${concatStrings (attrValues functions.arrays)} + ### files + ${concatStrings (attrValues functions.files)} + ### paths + ${concatStrings (attrValues functions.paths)} + ### terminal + ${concatStrings (attrValues functions.terminal)} + ### conversion + ${concatStrings (attrValues functions.conversion)} + ### other + ${concatStrings (attrValues functions.other)} + ## end bash bible + ''; +} diff --git a/pog/default.nix b/pog/default.nix new file mode 100644 index 0000000..fcc739c --- /dev/null +++ b/pog/default.nix @@ -0,0 +1,857 @@ +{ pkgs, ... }: +let + inherit (builtins) concatStringsSep filter isString replaceStrings split stringLength substring; + inherit (pkgs.lib) stringToCharacters; + inherit (pkgs.lib.lists) reverseList; + inherit (pkgs.lib.strings) fixedWidthString toUpper; + reverse = x: concatStringsSep "" (reverseList (stringToCharacters x)); + rightPad = num: text: reverse (fixedWidthString num " " (reverse text)); + ind = text: concatStringsSep "\n" (map (x: " ${x}") (filter isString (split "\n" text))); + + bashbible = import ./bashbible.nix { inherit pkgs; }; +in +rec { + overlay = final: prev: { inherit pog; }; + _ = with pkgs; let + core = "${pkgs.coreutils}/bin"; + in + rec { + # binaries + ## text + awk = "${pkgs.gawk}/bin/awk"; + bat = "${pkgs.bat}/bin/bat"; + curl = "${pkgs.curl}/bin/curl"; + figlet = "${pkgs.figlet}/bin/figlet"; + git = "${pkgs.git}/bin/git"; + gum = "${pkgs.gum}/bin/gum"; + gron = "${pkgs.gron}/bin/gron"; + jq = "${pkgs.jq}/bin/jq"; + rg = "${pkgs.ripgrep}/bin/rg"; + sed = "${pkgs.gnused}/bin/sed"; + grep = "${pkgs.gnugrep}/bin/grep"; + shfmt = "${pkgs.shfmt}/bin/shfmt"; + cut = "${core}/cut"; + head = "${core}/head"; + mktemp = "${core}/mktemp"; + realpath = "${core}/realpath"; + sort = "${core}/sort"; + tail = "${core}/tail"; + tr = "${core}/tr"; + uniq = "${core}/uniq"; + uuid = "${pkgs.libossp_uuid}/bin/uuid"; + yq = "${pkgs.yq-go}/bin/yq"; + y2j = "${pkgs.remarshal}/bin/yaml2json"; + + ## nix + _nix = pkgs.nixVersions.nix_2_22; + cachix = "${pkgs.cachix}/bin/cachix"; + nixpkgs-fmt = "${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt"; + nixfmt = "${pkgs.nixfmt-rfc-style}/bin/nixfmt"; + + ## common + ls = "${core}/ls"; + date = "${core}/date"; + find = "${pkgs.findutils}/bin/find"; + xargs = "${pkgs.findutils}/bin/xargs"; + getopt = "${pkgs.getopt}/bin/getopt"; + fzf = "${pkgs.fzf}/bin/fzf"; + sox = "${pkgs.sox}/bin/play"; + ffmpeg = "${pkgs.ffmpeg-full}/bin/ffmpeg"; + ssh = "${pkgs.openssh}/bin/ssh"; + which = "${pkgs.which}/bin/which"; + + ## containers + d = "${pkgs.docker-client}/bin/docker"; + k = "${pkgs.kubectl}/bin/kubectl"; + + ## clouds + aws = "${pkgs.awscli2}/bin/aws"; + gcloud = "${pkgs.google-cloud-sdk}/bin/gcloud"; + + # fzf partials + fzfq = ''${fzf} -q "$1" --no-sort --header-first --reverse''; + fzfqm = ''${fzfq} -m''; + + # ssh partials + _ssh = { + hosts = ''${_.grep} '^Host' ~/.ssh/config ~/.ssh/config.d/* 2>/dev/null | ${_.grep} -v '[?*]' | ${_.cut} -d ' ' -f 2- | ${_.sort} -u''; + }; + + # docker partials + docker = { + di = "${d} images"; + da = "${d} ps -a"; + get_image = "${awk} '{ print $3 }'"; + get_container = "${awk} '{ print $1 }'"; + }; + + # k8s partials + k8s = { + ka = "${k} get pods | ${sed} '1d'"; + get_id = "${awk} '{ print $1 }'"; + fmt = rec { + _fmt = + let + parseCol = col: "${col.k}:${col.v}"; + in + columns: "-o custom-columns='${concatStringsSep "," (map parseCol columns)}'"; + _cols = { + name = { k = "NAME"; v = ".metadata.name"; }; + namespace = { k = "NAMESPACE"; v = ".metadata.namespace"; }; + ready = { k = "READY"; v = ''status.conditions[?(@.type=="Ready")].status''; }; + status = { k = "STATUS"; v = ".status.phase"; }; + ip = { k = "IP"; v = ".status.podIP"; }; + node = { k = "NODE"; v = ".spec.nodeName"; }; + image = { k = "IMAGE"; v = ".spec.containers[*].image"; }; + host_ip = { k = "HOST_IP"; v = ".status.hostIP"; }; + start_time = { k = "START_TIME"; v = ".status.startTime"; }; + }; + pod = _fmt (with _cols; [ + name + namespace + ready + status + ip + node + image + ]); + }; + }; + + # json partials + refresh_patch = '' + echo "spec.template.metadata.labels.date = \"$(${_.date} +'%s')\";" | + ${_.gron} -u | + ${_.tr} -d '\n' | + ${_.sed} -E 's#\s+##g' + ''; + + # flags to reuse + flags = { + aws = { + region = { + name = "region"; + default = "us-east-1"; + description = "the AWS region in which to do this operation"; + argument = "REGION"; + envVar = "AWS_REGION"; + completion = ''echo -e '${concatStringsSep "\\n" _.globals.aws.regions}' ''; + }; + }; + gcp = { + project = { + name = "project"; + description = "the GCP project in which to do this operation"; + argument = "PROJECT_ID"; + completion = ''${_.gcloud} projects list | ${_.sed} '1d' | ${_.awk} '{print $1}' ''; + }; + }; + k8s = { + all_namespaces = { + name = "all_namespaces"; + short = "A"; + description = "operate across all namespaces"; + bool = true; + }; + namespace = { + name = "namespace"; + default = "default"; + description = "the namespace in which to do this operation"; + argument = "NAMESPACE"; + completion = ''${_.k} get ns | ${_.sed} '1d' | ${_.awk} '{print $1}' ''; + }; + nodes = { + name = "nodes"; + description = "the node(s) on which to perform this operation"; + argument = "NODES"; + completion = ''${_.k} get nodes -o wide | ${_.sed} '1d' | ${_.awk} '{print $1}' ''; + prompt = '' + ${_.k} get nodes -o wide | + ${_.fzfqm} --header-lines=1 | + ${_.k8s.get_id} + ''; + promptError = "you must specify one or more nodes!"; + }; + serviceaccount = { + name = "serviceaccount"; + description = "the service account to use for the workload"; + argument = "SERVICE_ACCOUNT"; + default = "default"; + completion = ''${_.k} get sa -o=jsonpath='{range .items[*].metadata.name}{@}{"\n"}{end}' ''; + }; + }; + docker = { + image = { + name = "image"; + description = "the docker image to use"; + argument = "IMAGE"; + prompt = '' + echo -e "${globals.hacks.docker.default_images}\n$(${globals.hacks.docker.get_local_images})" | + ${_.sort} -u | + ${_.fzfq} --header "IMAGE"''; + promptError = "you must specify a docker image!"; + completion = '' + echo -e "${globals.hacks.docker.default_images}\n$(${globals.hacks.docker.get_local_images})" | + ${_.sort} -u + ''; + }; + }; + common = { + force = { + name = "force"; + bool = true; + description = "forcefully do this thing"; + }; + color = { + name = "color"; + description = "the bash color/style to use [${bashColorsList}]"; + argument = "COLOR"; + default = "green"; + completion = ''echo "${bashColorsList} ${toUpper bashColorsList}"''; + }; + }; + github = { + owner = { + name = "owner"; + description = "the github user or organization that owns the repo"; + required = true; + }; + repo = { + name = "repo"; + description = "the github repo to pull tags from"; + required = true; + }; + }; + nix = { + overmind = { + name = "overmind"; + short = "o"; + bool = true; + description = "include an overmind config"; + }; + }; + python = { + package = { + name = "package"; + }; + version = { + name = "version"; + short = "r"; + }; + }; + ssh = { + host = { + name = "host"; + short = "H"; + description = "the ssh host to use"; + completion = _._ssh.hosts; + prompt = ''${_._ssh.hosts} | ${_.fzfq} --header "HOST"''; + promptError = "you must specify a ssh host!"; + }; + }; + }; + globals = { + hacks = { + bash_or_sh = "if command -v bash >/dev/null 2>/dev/null; then exec bash; else exec sh; fi"; + docker = { + default_images = concatStringsSep "\\n" _.globals.images; + get_local_images = '' + docker image ls --format "{{.Repository}}:{{.Tag}}" 2>/dev/null | + ${_.grep} -v '' | + ${_.sort} -u + ''; + }; + }; + + # docker images to use in various spots + images = [ + "alpine:3.20" + "ubuntu:24.04" + "almalinux:8.10" + "ghcr.io/jpetrucciani/foundry-nix:latest" + "ghcr.io/jpetrucciani/python-3.11:latest" + "ghcr.io/jpetrucciani/python-3.12:latest" + "ghcr.io/jpetrucciani/k8s-aws:latest" + "ghcr.io/jpetrucciani/k8s-gcp:latest" + "node:20" + "node:22" + "python:3.11" + "python:3.12" + "nicolaka/netshoot:latest" + ]; + aws = { + regions = [ + "us-east-1" + "us-east-2" + "us-west-1" + "us-west-2" + "us-gov-west-1" + "ca-central-1" + "eu-west-1" + "eu-west-2" + "eu-central-1" + "ap-southeast-1" + "ap-southeast-2" + "ap-south-1" + "ap-northeast-1" + "ap-northeast-2" + "sa-east-1" + "cn-north-1" + ]; + }; + gcp = { + regions = [ + "asia-east1" + "asia-east2" + "asia-northeast1" + "asia-northeast2" + "asia-northeast3" + "asia-south1" + "asia-south2" + "asia-southeast1" + "asia-southeast2" + "australia-southeast1" + "australia-southeast2" + "europe-central2" + "europe-north1" + "europe-west1" + "europe-west2" + "europe-west3" + "europe-west4" + "europe-west6" + "northamerica-northeast1" + "northamerica-northeast2" + "southamerica-east1" + "southamerica-west1" + "us-central1" + "us-east1" + "us-east4" + "us-west1" + "us-west2" + "us-west3" + "us-west4" + ]; + }; + tencent = { + regions = [ + "ap-guangzhou" + "ap-shanghai" + "ap-nanjing" + "ap-beijing" + "ap-chengdu" + "ap-chongqing" + "ap-hongkong" + "ap-singapore" + "ap-jakarta" + "ap-seoul" + "ap-tokyo" + "ap-mumbai" + "ap-bangkok" + "na-toronto" + "sa-saopaulo" + "na-siliconvalley" + "na-ashburn" + "eu-frankfurt" + "eu-moscow" + ]; + }; + }; + }; + + writeBashBinChecked = name: text: + pkgs.stdenv.mkDerivation { + inherit name text; + dontUnpack = true; + passAsFile = "text"; + nativeBuildInputs = [ pkgs.pkgs.shellcheck ]; + installPhase = '' + mkdir -p $out/bin + echo '#!/bin/bash' > $out/bin/${name} + cat $textPath >> $out/bin/${name} + chmod +x $out/bin/${name} + shellcheck $out/bin/${name} + ''; + }; + + bashEsc = ''\033''; + bashColors = [ + { + name = "reset"; + code = ''${bashEsc}[0m''; + } + # styles + { + name = "bold"; + code = ''${bashEsc}[1m''; + } + { + name = "dim"; + code = ''${bashEsc}[2m''; + } + { + name = "italic"; + code = ''${bashEsc}[3m''; + } + { + name = "underlined"; + code = ''${bashEsc}[4m''; + } + { + # note: this probably doesn't work in the majority of terminal emulators + name = "blink"; + code = ''${bashEsc}[5m''; + } + { + name = "invert"; + code = ''${bashEsc}[7m''; + } + { + name = "hidden"; + code = ''${bashEsc}[8m''; + } + # foregrounds + { + name = "black"; + code = ''${bashEsc}[1;30m''; + } + { + name = "red"; + code = ''${bashEsc}[1;31m''; + } + { + name = "green"; + code = ''${bashEsc}[1;32m''; + } + { + name = "yellow"; + code = ''${bashEsc}[1;33m''; + } + { + name = "blue"; + code = ''${bashEsc}[1;34m''; + } + { + name = "purple"; + code = ''${bashEsc}[1;35m''; + } + { + name = "cyan"; + code = ''${bashEsc}[1;36m''; + } + { + name = "grey"; + code = ''${bashEsc}[1;90m''; + } + # backgrounds + { + name = "red_bg"; + code = ''${bashEsc}[41m''; + } + { + name = "green_bg"; + code = ''${bashEsc}[42m''; + } + { + name = "yellow_bg"; + code = ''${bashEsc}[43m''; + } + { + name = "blue_bg"; + code = ''${bashEsc}[44m''; + } + { + name = "purple_bg"; + code = ''${bashEsc}[45m''; + } + { + name = "cyan_bg"; + code = ''${bashEsc}[46m''; + } + { + name = "grey_bg"; + code = ''${bashEsc}[100m''; + } + ]; + bashColorsList = concatStringsSep " " (map (x: x.name) (filter (x: x.name != "reset") bashColors)); + + writeBashBinCheckedWithFlags = pog; + pog = { + helpers = rec { + fn = { + add = "${_.awk} '{print $1 + $2}'"; + sub = "${_.awk} '{print $1 - $2}'"; + ts_to_seconds = "${_.awk} -F\: '{ for(k=NF;k>0;k--) sum+=($k*(60^(NF-k))); print sum }'"; + }; + var = { + empty = name: ''[ -z "''${${name}}" ]''; + notEmpty = name: ''[ -n "''${${name}}" ]''; + }; + file = { + exists = name: ''[ -f "''${${name}}" ]''; + notExists = name: ''[ ! -f "''${${name}}" ]''; + empty = name: ''[ ! -s "''${${name}}" ]''; + notEmpty = name: ''[ -s "''${${name}}" ]''; + }; + dir = { + exists = name: ''[ -d "${name}" ]''; + notExists = name: ''[ ! -d "${name}" ]''; + empty = name: ''[ -z "$(ls -A '${name}')" ]''; + notEmpty = name: ''[ ! -z "$(ls -A '${name}')" ]''; + }; + timer = { + start = name: ''_pog_start_${name}="$(${_.date} +%s.%N)"''; + stop = name: ''"$(echo "$(${_.date} +%s.%N) - $_pog_start_${name}" | ${pkgs.pkgs.bc}/bin/bc -l)"''; + round = places: ''${pkgs.pkgs.coreutils}/bin/printf '%.*f\n' ${toString places}''; + }; + confirm = yesno; + yesno = { prompt ? "Would you like to continue?", exit_code ? 0 }: '' + ${_.gum} confirm "${prompt}" || exit ${toString exit_code} + ''; + + spinner = { command, spinner ? "dot", align ? "left", title ? "processing..." }: '' + ${_.gum} spin --spinner="${spinner}" --align="${align}" --title="${title}" ${command} + ''; + spinners = [ + "line" + "dot" + "minidot" + "jump" + "pulse" + "points" + "globe" + "moon" + "monkey" + "meter" + "hamburger" + ]; + + # shorthands + flag = var.notEmpty; + notFlag = var.empty; + + # tmp stuff + tmp = + let + mktmp = "${pkgs.coreutils}/bin/mktemp"; + ext = extension: ''"$(${mktmp} --suffix=.${extension})"''; + in + { + _mktemp = mktmp; + json = ext "json"; + yaml = ext "yaml"; + csv = ext "csv"; + txt = ext "txt"; + }; + }; + __functor = _: pogFn; + }; + pogFn = + { name + , version ? "0.0.0" + , script + , description ? "a helpful bash script with flags, created through nix + pog!" + , flags ? [ ] + , parsedFlags ? map flag flags + , arguments ? [ ] + , argumentCompletion ? "files" + , runtimeInputs ? [ ] + , bashBible ? false + , beforeExit ? "" + , strict ? false + , flagPadding ? 20 + , showDefaultFlags ? false + , shortDefaultFlags ? true + }: + let + inherit (pog) helpers; + filterBlank = filter (x: x != ""); + shortHelp = if shortDefaultFlags then "-h|" else ""; + shortVerbose = if shortDefaultFlags then "-v|" else ""; + shortHelpDoc = if shortDefaultFlags then "-h, " else ""; + shortVerboseDoc = if shortDefaultFlags then "-v, " else ""; + defaultFlagHelp = if showDefaultFlags then "[${shortHelp}--help] [${shortVerbose}--verbose] [--no-color] " else ""; + in + pkgs.stdenv.mkDerivation { + inherit version; + pname = name; + dontUnpack = true; + nativeBuildInputs = [ pkgs.installShellFiles pkgs.shellcheck ]; + passAsFile = [ + "text" + "completion" + ]; + text = '' + # shellcheck disable=SC2317 + ${if strict then "set -o errexit -o pipefail -o noclobber" else ""} + VERBOSE="''${POG_VERBOSE-}" + NO_COLOR="''${POG_NO_COLOR-}" + export PATH="${pkgs.lib.makeBinPath runtimeInputs}:$PATH" + + help() { + cat <&2 + fi + } + cleanup() { + trap - SIGINT SIGTERM ERR EXIT + ${ind beforeExit} + } + trap cleanup SIGINT SIGTERM ERR EXIT + + ${concatStringsSep "\n" (map (x: '' + ${x.name}(){ + echo -e "''${${toUpper x.name}}$1''${RESET}" + } + '') bashColors)} + + die() { + local msg=$1 + local code=''${2-1} + echo >&2 -e "''${RED}$msg''${RESET}" + exit "$code" + } + setup_colors + ${if bashBible then bashbible.bible else ""} + ${concatStringsSep "\n" (filterBlank (map (x: x.flagPrompt) parsedFlags))} + # script + ${if builtins.isFunction script then script helpers else script} + ''; + completion = + let + argCompletion = + if argumentCompletion == "files" then '' + compopt -o default + COMPREPLY=() + '' else '' + completions=$(${argumentCompletion} "$current") + # shellcheck disable=SC2207 + COMPREPLY=( $(compgen -W "$completions" -- "$current") ) + ''; + in + '' + #!/bin/bash + # shellcheck disable=SC2317 + _${name}() + { + local current previous completions + compopt +o default + + flags(){ + echo "\ + ${if shortDefaultFlags then "-h -v " else ""}${concatStringsSep " " (map (x: "-${x.short}") (filter (x: x.short != "") parsedFlags))} \ + --help --verbose --no-color ${concatStringsSep " " (map (x: "--${x.name}") parsedFlags)}" + } + executables(){ + echo -n "$PATH" | + ${_.xargs} -d: -I{} -r -- find -L {} -maxdepth 1 -mindepth 1 -type f -executable -printf '%P\n' 2>/dev/null | + ${_.sort} -u + } + + COMPREPLY=() + current="''${COMP_WORDS[COMP_CWORD]}" + previous="''${COMP_WORDS[COMP_CWORD-1]}" + + if [[ $current = -* ]]; then + completions=$(flags) + # shellcheck disable=SC2207 + COMPREPLY=( $(compgen -W "$completions" -- "$current") ) + ${concatStringsSep "\n" (map (x: x.completionBlock) parsedFlags)} + elif [[ $COMP_CWORD = 1 ]] || [[ $previous = -* && $COMP_CWORD = 2 ]]; then + ${argCompletion} + else + compopt -o default + COMPREPLY=() + fi + return 0 + } + complete -F _${name} ${name} + ''; + installPhase = '' + mkdir -p $out/bin + echo '#!/bin/bash' >$out/bin/${name} + cat $textPath >>$out/bin/${name} + chmod +x $out/bin/${name} + shellcheck $out/bin/${name} + shellcheck $completionPath + installShellCompletion --bash --name ${name} $completionPath + ''; + meta = { + inherit description; + mainProgram = name; + }; + }; + + flag = + { name + , _name ? (replaceStrings [ "-" ] [ "_" ] name) + , short ? substring 0 1 name + , shortDef ? if short != "" then "-${short}|" else "" + , default ? "" + , hasDefault ? (stringLength default) > 0 + , bool ? false + , marker ? if bool then "" else ":" + , description ? "a flag" + , argument ? "VAR" + , envVar ? "POG_" + (replaceStrings [ "-" ] [ "_" ] (toUpper name)) + , required ? false + , prompt ? if required then "true" else "" + , promptError ? "you must specify a value for '--${name}'!" + , promptErrorExitCode ? 3 + , hasPrompt ? (stringLength prompt) > 0 + , completion ? "" + , hasCompletion ? (stringLength completion) > 0 + , flagPadding ? 20 + }: { + inherit short default bool marker description; + name = _name; + shortOpt = "${short}${marker}"; + longOpt = "${_name}${marker}"; + flagDefault = ''${_name}="''${${envVar}:-${default}}"''; + flagPrompt = + if hasPrompt then '' + [ -z "''${${_name}}" ] && ${_name}="$(${prompt})" + [ -z "''${${_name}}" ] && die "${promptError}" ${toString promptErrorExitCode} + '' else ""; + ex = "[${shortDef}--${_name}${if bool then "" else " ${argument}"}]"; + helpDoc = + let + base = (if short != "" then "-${short}, " else "") + "--${_name}"; + in + (rightPad flagPadding base) + + "\t${description}" + + "${if hasDefault then " [default: '${default}']" else ""}" + + "${if hasPrompt then " [will prompt if not passed in]" else ""}" + + "${if bool then " [bool]" else ""}" + ; + definition = '' + ${shortDef}--${_name}) + ${_name}=${if bool then "1" else "$2"} + shift ${if bool then "" else "2"} + ;;''; + completionBlock = + if hasCompletion then '' + elif [[ $previous = -${short} ]] || [[ $previous = --${_name} ]]; then + # shellcheck disable=SC2116 + completions=$(${completion}) + # shellcheck disable=SC2207 + COMPREPLY=( $(compgen -W "$completions" -- "$current") ) + '' else ""; + }; + + foo = pog { + name = "foo"; + description = "a tester script for pog, my classic bash bin + flag + bashbible meme"; + bashBible = true; + beforeExit = '' + green "this is beforeExit - foo test complete!" + ''; + flags = [ + _.flags.common.color + { + name = "functions"; + short = ""; + description = "list all functions! (this is a lot of text)"; + bool = true; + } + ]; + script = h: with h; '' + color="''${color^^}" + trim_string " foo " + upper 'foo' + lower 'FOO' + lstrip "The Quick Brown Fox" "The " + urlencode "https://github.com/dylanaraps/pure-bash-bible" + remove_array_dups 1 1 2 2 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 5 + hex_to_rgb "#FFFFFF" + rgb_to_hex "255" "255" "255" + date "%a %d %b - %l:%M %p" + uuid + bar 1 10 + ''${functions:+get_functions} + debug "''${GREEN}this is a debug message, only visible when passing -v (or setting POG_VERBOSE)!" + black "this text is 'black'" + red "this text is 'red'" + green "this text is 'green'" + yellow "this text is 'yellow'" + blue "this text is 'blue'" + purple "this text is 'purple'" + cyan "this text is 'cyan'" + grey "this text is 'grey'" + green_bg "this text has a green background" + purple_bg "this text has a purple background" + yellow_bg "this text has a yellow background" + bold "this text should be bold!" + dim "this text should be dim!" + italic "this text should be italic!" + underlined "this text should be underlined!" + blink "this text might blink on certain terminal emulators!" + invert "this text should be inverted!" + hidden "this text should be hidden!" + echo -e "''${GREEN_BG}''${RED}this text is red on a green background and looks awful''${RESET}" + echo -e "''${!color}this text has its color set by a flag '--color' or env var 'POG_COLOR' (default green)''${RESET}" + ${spinner {command="sleep 3";}} + ${confirm {exit_code=69;}} + die "this is a die" 0 + ''; + }; +}