commit bdc8ae889dd6dc0539e479baa72373fffdc6ae5c
Author: Peder Bergebakken Sundt <pbsds@hotmail.com>
Date:   Sun Feb 23 00:04:10 2025 +0100

    Initial commit

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/.gitignore b/.gitignore
new file mode 100644
index 0000000..9b42106
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.direnv/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3453fd5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,54 @@
+# nixq
+
+Like `jq`, `yq` and `htmlq`, but for nix :snowflake::snowflake::snowflake:
+Basically just a shell interface for [nix-select](https://git.clan.lol/clan/nix-select/).
+
+## Examples
+
+```
+$ nixq . 'packages.{x86_64-linux,aarch64-linux}.*' --yaml
+aarch64-linux:
+  default: /nix/store/6nrvhk2viafsy84sg29jrzkf4xdap529-nixq
+  nixq: /nix/store/6nrvhk2viafsy84sg29jrzkf4xdap529-nixq
+  nixq-lix: /nix/store/2m7z3kjf76mqaldh40r3zfg39vfbrg93-nixq
+  nixq-nix: /nix/store/q8jqlz75hs6hlpzsbjhr994w2ffzyply-nixq
+x86_64-linux:
+  default: /nix/store/fx8n5krq20y6z1p97iv7ra77zzq0lvf6-nixq
+  nixq: /nix/store/fx8n5krq20y6z1p97iv7ra77zzq0lvf6-nixq
+  nixq-lix: /nix/store/a2znjf47hhfd2749ff0lwy7ljz3nsz43-nixq
+  nixq-nix: /nix/store/046dkmci916zgy1690pqvcb0wijrij98-nixq
+```
+
+```
+$ nixq -f '<nixpkgs>' 'pkgs.{spade,sus-compiler,pagefind}.meta.maintainers.*.github'
+{
+  "pagefind": [
+    "pbsds"
+  ],
+  "spade": [
+    "pbsds"
+  ],
+  "sus-compiler": [
+    "pbsds"
+  ]
+}
+```
+
+```
+$ nixq flake:nixpkgs 'legacyPackages.x86_64-linux.{disko,pagefind}.meta.available'
+{
+  "disko": true,
+  "pagefind": true
+}
+```
+
+```
+$ nixq github:NixOS/nixpkgs/nixos-unstable 'legacyPackages.x86_64-linux.python313Packages' --keys | jq .[:5]
+[
+  "acompressor",
+  "autocrop",
+  "autodeint",
+  "autoload",
+  "autosub",
+]
+```
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..30ef198
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,59 @@
+{
+  "nodes": {
+    "nix-select": {
+      "locked": {
+        "lastModified": 1739677887,
+        "narHash": "sha256-VIGgoWnzXzlwRHfJt3YRNQsRyX1I1dPdVm61hnyKJZo=",
+        "ref": "refs/heads/main",
+        "rev": "d2aa1f72b6e1ac318f4125d6eebee256bbeb05a3",
+        "revCount": 11,
+        "type": "git",
+        "url": "https://git.clan.lol/clan/nix-select.git"
+      },
+      "original": {
+        "type": "git",
+        "url": "https://git.clan.lol/clan/nix-select.git"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1739866667,
+        "narHash": "sha256-EO1ygNKZlsAC9avfcwHkKGMsmipUk1Uc0TbrEZpkn64=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "73cf49b8ad837ade2de76f87eb53fc85ed5d4680",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "nixpkgs-lib": {
+      "locked": {
+        "lastModified": 1739667890,
+        "narHash": "sha256-7QtSNdCEbYG1v+ZVrFWhBkhlo2GWehPffWC0BP1VZSo=",
+        "owner": "nix-community",
+        "repo": "nixpkgs.lib",
+        "rev": "9b883b6d4d3bd580734ddb4b5bfde8ebffd26559",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-community",
+        "repo": "nixpkgs.lib",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "nix-select": "nix-select",
+        "nixpkgs": "nixpkgs",
+        "nixpkgs-lib": "nixpkgs-lib"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..9e87bbd
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,44 @@
+{
+  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+  inputs.nix-select.url = "git+https://git.clan.lol/clan/nix-select.git";
+  inputs.nixpkgs-lib.url = "github:nix-community/nixpkgs.lib";
+
+  outputs = inputs:
+  let
+    forAllSystems = f: inputs.nixpkgs.lib.genAttrs inputs.nixpkgs.lib.systems.flakeExposed (system: f rec {
+      inherit system;
+      pkgs = inputs.nixpkgs.legacyPackages.${system};
+      inherit (pkgs) lib;
+    });
+  in {
+    inherit inputs;
+
+    overlays.default = prev: {
+      nixq = prev.callPackage ./package.nix { inherit (inputs) nix-select nixpkgs-lib; };
+    };
+
+    packages = forAllSystems ({ pkgs, ... }: rec {
+      default  = nixq;
+      nixq     = pkgs.callPackage ./package.nix { inherit (inputs) nix-select nixpkgs-lib; };
+      nixq-nix = pkgs.callPackage ./package.nix { inherit (inputs) nix-select nixpkgs-lib; extraRuntimeDeps = [ pkgs.nix ]; };
+      nixq-lix = pkgs.callPackage ./package.nix { inherit (inputs) nix-select nixpkgs-lib; extraRuntimeDeps = [ pkgs.lix ]; };
+    });
+
+    devShells = forAllSystems ({ lib, pkgs, ... }: rec {
+      default = pkgs.mkShellNoCC {
+        packages = builtins.attrValues {
+          inherit (pkgs)
+            lix
+            jq
+            yq
+            bat
+            ;
+          };
+        env.NIXQ_NIX_SELECT_PATH = inputs.nix-select.outPath;
+        env.NIXQ_NIXPKGS_LIB_PATH = inputs.nixpkgs.outPath;
+        env.UV_PYTHON_DOWNLOADS = "never";
+      };
+    });
+
+  };
+}
diff --git a/nixq.sh b/nixq.sh
new file mode 100755
index 0000000..a5ad4c7
--- /dev/null
+++ b/nixq.sh
@@ -0,0 +1,219 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [ -z "${NIXQ_NIX_SELECT_PATH:-}" ]; then
+	>&2 echo "nix-select not found! Please set NIXQ_NIX_SELECT_PATH!"
+fi
+if [ -z "${NIXQ_NIXPKGS_LIB_PATH:-}" ]; then
+	>&2 echo "nixpkgs-lib not found! Please set NIXQ_NIXPKGS_LIB_PATH!"
+fi
+
+die() {
+	>&2 echo "ERROR:" "$@"
+	exit 1
+}
+
+print_help() {
+	echo "nixq: A nix tool to query elements/lists/attrs from nested list/attrs,"
+	echo "      using the nix-select library: https://git.clan.lol/clan/nix-select/"
+	echo ""
+	echo "Usage:"
+	echo "  $0 [-f] <file> [-q] <query> [-k] [-v] [-D] [-y] [--apply <expr>] ..."
+	echo ""
+	echo "  -f, --file   <file>   Load a file, typically named default.nix"
+	echo "  -f, --file   <file>   Load a file, typically named default.nix"
+	echo "     [--flake] <file>   Load a flake, does not support attributes after '#'"
+	echo " [-q, --query] <query>  The select query to perform"
+	echo "  -k, --keys, --names   Will '--apply builtins.attrName'"
+	echo "  -v, --values          Will '--apply builtins.attrValue'"
+	echo "  -t, --try-values      Will tryEval the attribute set values"
+	# echo "  -D, --rename-outPath  Rename \"outPath\" keys to \"outPath'\""
+	echo "  -y, --yaml            Output as yaml"
+	echo "  --raw                 Output raw"
+	echo "  --apply <expr>        Apply custom expression with nixpkgs 'lib' in scope, can be used multiple times"
+	echo "  ...                   Passed on to 'nix eval'"
+	# PAGER="" nix --extra-experimental-features "nix-command flakes" eval --help
+}
+
+nix_str() {
+	local out="$1"
+	out="${out/\"/\\\"}"
+	out="${out/\$/\\\$}"
+	printf '"%s"\n' "$out"
+}
+
+lib_expr="let lib = import $NIXQ_NIXPKGS_LIB_PATH/lib; in"
+
+if [[ $# -eq 0 ]]; then
+	print_help
+	exit 0
+fi
+
+file_expr=""
+query=""
+output_raw=false
+output_yaml=false
+
+set_file_expr_normal() {
+	if [[ -z "${1:-}" ]]; then
+		die "No file provided"
+	fi
+	local path
+	if [[ $1 =~ \<[a-zA-Z0-9_-]+\> ]]; then
+		path="$1"
+	else
+		path="$(nix_str "$(realpath "$1")")"
+	fi
+	file_expr="(let f = import $path; in if builtins.isFunction f then f {} else f)"
+}
+set_file_expr_flake() {
+	if [[ -z "${1:-}" ]]; then
+		die "No flake provided"
+	fi
+	file_expr="(builtins.getFlake $(nix_str "$1"))"
+}
+
+posargs=()
+filtered_args=()
+
+while [[ "$#" -gt 0 ]]; do
+	arg="$1"
+	case "$1" in
+	-[^-][^-]*)
+		shift
+		# shellcheck disable=SC2046
+		set -- $(echo "${arg:1:99999}" | sed -e 's/\(.\)/ -\1/g') "$@"
+		;;
+	--help)
+		print_help
+		exit 0
+		;;
+	-f | --file)
+		shift
+		set_file_expr_normal "$1"
+		shift
+		;;
+	--flake)
+		shift
+		set_file_expr_flake "$1"
+		shift
+		;;
+	-q | --query)
+		shift
+		query="$1"
+		shift
+		;;
+	-y | --yaml)
+		output_yaml=true
+		shift
+		;;
+	--raw)
+		output_raw=true
+		filtered_args+=("$1")
+		shift
+		;;
+	-k | --keys | --names)
+		filtered_args+=("--apply" "builtins.attrNames")
+		shift
+		;;
+	-v | --values)
+		filtered_args+=("--apply" "builtins.attrValues")
+		shift
+		;;
+	#-D | --rename-out-path)
+	#    filtered_args+=("--apply" "$lib_expr lib.mapAttrs' (k: v: { name= { outPath=\"outPath'\"; }.\${k} or k; value = v; })");
+	#    shift
+	#    ;;
+	-t | --try-values)
+		# TODO: figure out how to force the full value
+		# filtered_args+=("--apply" "builtins.mapAttrs (k: v: if (builtins.tryEval (builtins.toJSON v == \"\")).success then v else null)");
+		filtered_args+=("--apply" "builtins.mapAttrs (k: v: if (builtins.tryEval (v == null)).success then v else null)")
+		shift
+		;;
+	#-d | --max-depth)
+	#    filtered_args+=("--apply" "");
+	#    shift
+	#    ;;
+	--apply)
+		filtered_args+=("$1")
+		shift
+		filtered_args+=("$lib_expr $1")
+		shift
+		;;
+	[^-]*)
+		posargs+=("$1")
+		shift
+		;;
+	*)
+		filtered_args+=("$1")
+		shift
+		;;
+	esac
+done
+
+for arg in "${posargs[@]}"; do
+	if [[ -z "$file_expr" ]]; then
+		set_file_expr_flake "$arg"
+	elif [[ -z "$query" ]]; then
+		query="$arg"
+	else
+		die "Too many positional arguments!"
+	fi
+done
+
+if [[ -z "$file_expr" ]]; then
+	die "No file provided"
+fi
+if [[ -z "$query" ]]; then
+	die "No query provided"
+fi
+
+if ! $output_raw; then
+	# TODO: figure out why this is slow
+	#filtered_args+=(
+	#    "--apply"
+	#    '
+	#        let
+	#            mapTree = x:
+	#                if !(builtins.tryEval (x==null)).success then
+	#                    null
+	#                else if builtins.isAttrs x then
+	#                    builtins.mapAttrs (k: v: mapTree v) x
+	#                else if builtins.isList x then
+	#                    builtins.map mapTree x
+	#                else if builtins.isFunction x then
+	#                    "<lambda>"
+	#                else
+	#                    x;
+	#        in mapTree
+	#    '
+	#);
+	filtered_args+=(--json)
+fi
+
+nix_cmd=(
+	nix
+	--extra-experimental-features "nix-command flakes"
+	eval --impure
+	--expr "(import $NIXQ_NIX_SELECT_PATH/select.nix).select $(nix_str "$query") $file_expr"
+	"${filtered_args[@]}"
+)
+
+output="$(
+	# set -x
+	"${nix_cmd[@]}"
+)"
+
+if $output_raw; then
+	cat <<<"$output"
+elif $output_yaml; then
+	if [[ -t 1 ]] && >/dev/null command -v bat; then
+		yq . -y <<<"$output" | bat --language yaml --style plain --paging never
+	else
+		yq . -y <<<"$output"
+	fi
+elif [[ -t 1 ]] && >/dev/null command -v jq; then
+	jq . <<<"$output"
+else
+	cat <<<"$output"
+fi
diff --git a/package.nix b/package.nix
new file mode 100644
index 0000000..7f31bd1
--- /dev/null
+++ b/package.nix
@@ -0,0 +1,31 @@
+{
+  writeShellApplication,
+  coreutils,
+  gnused,
+  jq,
+  yq,
+  bat,
+
+  nix-select ? "",
+  nixpkgs-lib ? "",
+  extraRuntimeDeps ? [ ],
+}:
+writeShellApplication {
+  name = "nixq";
+
+  text = builtins.readFile ./nixq.sh;
+  extraShellCheckFlags = [ "--severity" "info" ];
+
+  runtimeInputs = [
+    coreutils
+    gnused
+    jq
+    yq
+    bat
+  ] ++ extraRuntimeDeps;
+
+  runtimeEnv = {
+    NIXQ_NIX_SELECT_PATH = nix-select;
+    NIXQ_NIXPKGS_LIB_PATH = nixpkgs-lib;
+  };
+}