Add custom script packages with Nix dependency management
This commit introduces two new custom script packages: tb-scripts for all systems and tb-dev-scripts for development machines. These packages provide a maintainable way to distribute utility scripts across the infrastructure with proper dependency management. Created pkgs/ directory with two script collections: 1. **tb-scripts** - General utilities available on all systems: - ,jq_reformat: Reformat JSON files in-place with atomic file operations - ,rename_lower: Convert filenames to lowercase with validation 2. **tb-dev-scripts** - Development-specific tools: - ,cmake_update_fetchcontent: Update CMake FetchContent dependencies All scripts have been significantly enhanced from their original versions: - Proper quoting to handle filenames with spaces - Secure temporary file creation using mktemp - Atomic file replacement to prevent data loss - Input validation and comprehensive error handling - Usage help with -h/--help flag - Extensive inline comments explaining each section - Cleanup traps on error - Complete rewrite in Python for consistency - Validates files exist before attempting rename - Checks if target lowercase filename already exists - Skips files already lowercase (no-op) - Descriptive error messages for each failure case - Usage documentation with examples - Proper exit codes - Interactive CMake FetchContent dependency updater - Recursively finds all CMakeLists.txt files via add_subdirectory() - Queries GitHub API for latest releases/tags - Compares semantic versions and commit hashes - Shows available updates in formatted table - Prompts for confirmation before applying updates - Atomic file updates with validation Scripts are packaged using writeShellApplication with proper dependency injection via runtimeInputs: - tb-scripts requires: jq, python3 - tb-dev-scripts requires: python3, git Dependencies are automatically available in PATH when scripts run, eliminating manual dependency checks. Created system module files to import the script packages: - system/default/scripts.nix: Adds tb-scripts to nixosModules.default - system/develop/scripts.nix: Adds tb-dev-scripts to nixosModules.develop Updated flake.nix to import these modules in the appropriate contexts. - Scripts have proper Nix-managed dependencies - No manual installation or PATH configuration needed - Easy to extend with additional scripts - Scripts are validated with shellcheck during build - Clear separation between all-systems and dev-only utilities - Comprehensive error handling and user documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
19
pkgs/tb-scripts/default.nix
Normal file
19
pkgs/tb-scripts/default.nix
Normal file
@@ -0,0 +1,19 @@
|
||||
{ pkgs }:
|
||||
|
||||
pkgs.symlinkJoin {
|
||||
name = "tb-scripts";
|
||||
meta = {
|
||||
description = "Custom utility scripts for TB - available on all systems";
|
||||
maintainers = [ ];
|
||||
};
|
||||
paths = [
|
||||
(pkgs.writeShellApplication {
|
||||
name = ",jq_reformat";
|
||||
runtimeInputs = [ pkgs.jq ];
|
||||
text = builtins.readFile ./jq_reformat.sh;
|
||||
})
|
||||
(pkgs.writers.writePython3Bin ",rename_lower" {
|
||||
libraries = [ ];
|
||||
} (builtins.readFile ./rename_lower.py))
|
||||
];
|
||||
}
|
||||
135
pkgs/tb-scripts/jq_reformat.sh
Normal file
135
pkgs/tb-scripts/jq_reformat.sh
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#
|
||||
# jq_reformat - Reformat JSON files in-place using jq
|
||||
#
|
||||
# Usage: jq_reformat FILE [FILE...]
|
||||
#
|
||||
# This script reformats JSON files in-place by running them through jq.
|
||||
# It preserves the original files by using atomic temporary file replacement,
|
||||
# ensuring data safety even if the process is interrupted.
|
||||
#
|
||||
# Examples:
|
||||
# jq_reformat config.json
|
||||
# jq_reformat *.json
|
||||
# jq_reformat data/*.json settings.json
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
#
|
||||
# Show usage information
|
||||
#
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: ,jq_reformat FILE [FILE...]
|
||||
|
||||
Reformat JSON files in-place using jq.
|
||||
|
||||
This script reformats JSON files by running them through jq's parser,
|
||||
which produces consistently formatted, valid JSON. Files are updated
|
||||
atomically using temporary files to prevent data loss.
|
||||
|
||||
Arguments:
|
||||
FILE One or more JSON files to reformat
|
||||
|
||||
Options:
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
,jq_reformat config.json
|
||||
,jq_reformat *.json
|
||||
,jq_reformat data/*.json settings.json
|
||||
|
||||
Exit codes:
|
||||
0 Success - all files reformatted
|
||||
1 Error - invalid arguments, missing files, or jq errors
|
||||
EOF
|
||||
}
|
||||
|
||||
#
|
||||
# Reformat a single JSON file in-place
|
||||
#
|
||||
# Arguments:
|
||||
# $1 - Path to the JSON file to reformat
|
||||
#
|
||||
# Returns:
|
||||
# 0 on success, 1 on error
|
||||
#
|
||||
reformat_file() {
|
||||
local file="$1"
|
||||
local temp_file
|
||||
|
||||
# Validate that the file exists and is readable
|
||||
if [[ ! -f "$file" ]]; then
|
||||
echo "Error: File not found: $file" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -r "$file" ]]; then
|
||||
echo "Error: File not readable: $file" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create a secure temporary file in the same directory as the target
|
||||
# This ensures we're on the same filesystem for atomic mv operation
|
||||
temp_file=$(mktemp "${file}.XXXXXX") || {
|
||||
echo "Error: Failed to create temporary file for: $file" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Set up cleanup trap to remove temp file on error
|
||||
# shellcheck disable=SC2064
|
||||
trap "rm -f '$temp_file'" EXIT ERR
|
||||
|
||||
# Run jq to reformat the JSON
|
||||
# - Read from the original file
|
||||
# - Write to temp file
|
||||
# - If successful, atomically replace the original
|
||||
if jq . <"$file" >"$temp_file" 2>/dev/null; then
|
||||
# Preserve original file permissions
|
||||
chmod --reference="$file" "$temp_file" 2>/dev/null || true
|
||||
|
||||
# Atomically replace the original file
|
||||
mv "$temp_file" "$file"
|
||||
|
||||
# Clear the trap since we succeeded
|
||||
trap - EXIT ERR
|
||||
return 0
|
||||
else
|
||||
# jq failed - the file is likely not valid JSON
|
||||
echo "Error: Failed to parse JSON in: $file" >&2
|
||||
rm -f "$temp_file"
|
||||
trap - EXIT ERR
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# Main script logic
|
||||
#
|
||||
main() {
|
||||
# Handle help flag
|
||||
if [[ $# -eq 0 ]] || [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
|
||||
usage
|
||||
[[ $# -eq 0 ]] && return 1
|
||||
return 0
|
||||
fi
|
||||
|
||||
local exit_code=0
|
||||
local file
|
||||
|
||||
# Process each file argument
|
||||
# Using "$@" properly handles filenames with spaces and special characters
|
||||
for file in "$@"; do
|
||||
if ! reformat_file "$file"; then
|
||||
exit_code=1
|
||||
# Continue processing other files even if one fails
|
||||
fi
|
||||
done
|
||||
|
||||
return "$exit_code"
|
||||
}
|
||||
|
||||
# Run main function with all arguments
|
||||
main "$@"
|
||||
115
pkgs/tb-scripts/rename_lower.py
Normal file
115
pkgs/tb-scripts/rename_lower.py
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
rename_lower - Rename files to lowercase
|
||||
|
||||
Usage: rename_lower FILE [FILE...]
|
||||
|
||||
This script renames files by converting their filenames to lowercase.
|
||||
It performs validation to ensure files exist and that the target lowercase
|
||||
filename doesn't already exist before renaming.
|
||||
|
||||
Examples:
|
||||
rename_lower MyFile.TXT
|
||||
rename_lower *.JPG
|
||||
rename_lower Document.PDF Image.PNG
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def usage():
|
||||
"""Show usage information"""
|
||||
print("""Usage: ,rename_lower FILE [FILE...]
|
||||
|
||||
Rename files to lowercase filenames.
|
||||
|
||||
This script converts filenames to lowercase. It validates that files
|
||||
exist and checks for conflicts before renaming.
|
||||
|
||||
Arguments:
|
||||
FILE One or more files to rename
|
||||
|
||||
Options:
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
,rename_lower MyFile.TXT
|
||||
,rename_lower *.JPG
|
||||
,rename_lower Document.PDF Image.PNG
|
||||
|
||||
Exit codes:
|
||||
0 Success - all files renamed (or already lowercase)
|
||||
1 Error - invalid arguments, missing files, or conflicts
|
||||
""")
|
||||
|
||||
|
||||
def rename_to_lowercase(filepath: str) -> bool:
|
||||
"""
|
||||
Rename a single file to lowercase.
|
||||
|
||||
Arguments:
|
||||
filepath: Path to the file to rename
|
||||
|
||||
Returns:
|
||||
True on success, False on error
|
||||
"""
|
||||
# Convert to Path object for easier manipulation
|
||||
original = Path(filepath)
|
||||
|
||||
# Validate that the file exists
|
||||
if not original.exists():
|
||||
print(f"Error: File not found: {filepath}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Get the directory and filename components
|
||||
directory = original.parent
|
||||
original_name = original.name
|
||||
lowercase_name = original_name.lower()
|
||||
|
||||
# Check if the filename is already lowercase
|
||||
if original_name == lowercase_name:
|
||||
# Already lowercase, nothing to do
|
||||
return True
|
||||
|
||||
# Construct the target path
|
||||
target = directory / lowercase_name
|
||||
|
||||
# Check if target already exists
|
||||
if target.exists():
|
||||
print(f"Error: Target file already exists: {target}", file=sys.stderr)
|
||||
print(f" Cannot rename: {filepath}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Perform the rename
|
||||
try:
|
||||
original.rename(target)
|
||||
print(f"Renamed: {original_name} -> {lowercase_name}")
|
||||
return True
|
||||
except OSError as e:
|
||||
print(f"Error: Failed to rename {filepath}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Main script logic"""
|
||||
# Handle help flag or no arguments
|
||||
if len(sys.argv) == 1 or sys.argv[1] in ("-h", "--help"):
|
||||
usage()
|
||||
return 0 if len(sys.argv) > 1 else 1
|
||||
|
||||
success = True
|
||||
|
||||
# Process each file argument
|
||||
for filepath in sys.argv[1:]:
|
||||
if not rename_to_lowercase(filepath):
|
||||
success = False
|
||||
# Continue processing other files even if one fails
|
||||
|
||||
return 0 if success else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user