1
0

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.

## New Package Structure

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

## Script Improvements

All scripts have been significantly enhanced from their original versions:

### ,jq_reformat (refactored from shell script)
- 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

### ,rename_lower (converted from Perl to Python)
- 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

### ,cmake_update_fetchcontent (new)
- 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

## Nix Integration

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.

## Module Organization

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.

## Benefits

- 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:
2025-11-19 05:15:37 -08:00
parent 36a2d10cb1
commit f641f85fd8
8 changed files with 826 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
{ 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.writeShellApplication {
name = ",rename_lower";
runtimeInputs = [ pkgs.python3 ];
text = ''
python3 - "$@" <<'PYTHON_SCRIPT'
${builtins.readFile ./rename_lower.py}
PYTHON_SCRIPT
'';
})
];
}

View File

@@ -0,0 +1,134 @@
#!/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
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 "$@"

View 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())