From 88efedf26b08aeb6595a656430017f2b145de4b5 Mon Sep 17 00:00:00 2001 From: Ismo Vuorinen Date: Wed, 12 Feb 2025 01:05:37 +0200 Subject: [PATCH] feat: updates, docs, license fixes, new helpers --- local/bin/README.md | 2 +- local/bin/git-dirty | 20 +- local/bin/t | 155 +++++++------ local/bin/x-clean-vendordirs | 54 +++++ local/bin/x-foreach | 2 + local/bin/x-gh-get-latest-release-targz | 2 +- local/bin/x-gh-get-latest-version | 188 +++++++++++++--- local/bin/x-localip | 5 +- local/bin/x-multi-ping | 183 +++++++++++++++ local/bin/x-open-ports | 136 ++++++++++-- local/bin/x-path | 281 ++++++++++++++++++++++++ local/bin/x-path-append | 64 +++--- local/bin/x-path-prepend | 61 +++-- local/bin/x-path-remove | 52 +++-- local/bin/x-quota-usage.php | 2 + local/bin/x-record | 2 +- local/bin/x-set-php-aliases | 158 +++++++++---- local/bin/x-sha256sum-matcher | 134 ++++++----- local/bin/x-thumbgen | 193 +++++++++++++--- local/bin/x-until-error | 94 +++++++- local/bin/x-until-success | 96 ++++++-- local/bin/x-welcome-banner | 4 +- local/bin/x-when-down | 11 +- local/bin/x-when-up | 11 +- scripts/create-aerospace-keymaps.php | 4 +- 25 files changed, 1559 insertions(+), 355 deletions(-) create mode 100755 local/bin/x-clean-vendordirs create mode 100755 local/bin/x-multi-ping create mode 100755 local/bin/x-path diff --git a/local/bin/README.md b/local/bin/README.md index a4f07a8..c8cc821 100644 --- a/local/bin/README.md +++ b/local/bin/README.md @@ -13,7 +13,7 @@ Some problematic code has been fixed per `shellcheck` suggestions. ## Sourced | Script | Source | -| ----------------------- | ----------------- | +|-------------------------|-------------------| | `x-dupes` | skx/sysadmin-util | | `x-foreach` | mvdan/dotfiles | | `x-multi-ping` | skx/sysadmin-util | diff --git a/local/bin/git-dirty b/local/bin/git-dirty index fbbf1be..0b37b9b 100755 --- a/local/bin/git-dirty +++ b/local/bin/git-dirty @@ -23,8 +23,8 @@ VERBOSE="${VERBOSE:-0}" # UTF-8 ftw -GITDIRTY="❌ " -GITCLEAN="✅ " +GIT_DIRTY="❌ " +GIT_CLEAN="✅ " # Function to print messages if VERBOSE is enabled # $1 - message (string) @@ -41,7 +41,7 @@ catch() # Function to check the git status of a directory # $1 - directory (string) -gitdirty() +git_dirty() { local d="$1" trap 'catch $? $LINENO' ERR @@ -58,15 +58,15 @@ gitdirty() # If we have `.git` folder, check it. if [[ -d ".git" ]]; then - ISDIRTY=$(git diff --shortstat 2> /dev/null | tail -n1) - ICON="$GITCLEAN" + GIT_IS_DIRTY=$(git diff --shortstat 2> /dev/null | tail -n1) + ICON="$GIT_CLEAN" - [[ $ISDIRTY != "" ]] && ICON="$GITDIRTY" + [[ $GIT_IS_DIRTY != "" ]] && ICON="$GIT_DIRTY" printf " %s %s\n" "$ICON" "$(pwd)" else # If it wasn't git repository, check subdirectories. - gitdirtyrepos ./* + git_dirty_repos ./* fi cd - > /dev/null || exit fi @@ -76,10 +76,10 @@ gitdirty() # Function to check git status for multiple directories # $@ - directories -gitdirtyrepos() +git_dirty_repos() { for x in "$@"; do - gitdirty "$x" + git_dirty "$x" done } @@ -96,7 +96,7 @@ main() 11) echo "segfault occurred";; esac' EXIT - gitdirtyrepos "$GIT_DIRTY_DIR" + git_dirty_repos "$GIT_DIRTY_DIR" } main "$@" diff --git a/local/bin/t b/local/bin/t index 5a06bb4..82a1738 100755 --- a/local/bin/t +++ b/local/bin/t @@ -1,7 +1,7 @@ #!/usr/bin/env bash # -# Credit to ThePrimeagen, jessarcher -# https://github.com/jessarcher/dotfiles/blob/master/scripts/t +# Credit to ThePrimeagen, Jess Archer +# See https://github.com/jessarcher/dotfiles/blob/master/scripts/t # # Tweaks by Ismo Vuorinen 2025 # vim: ft=bash ts=2 sw=2 et @@ -9,104 +9,135 @@ # Set environment variables for configuration with defaults T_ROOT="${T_ROOT:-$HOME/Code}" DOTFILES="${DOTFILES:-$HOME/.dotfiles}" +T_MAX_DEPTH="${T_MAX_DEPTH:-3}" # Function to print an error message and exit -error_exit() -{ +error_exit() { echo "Error: $1" >&2 exit 1 } -get_directories() -{ - local dirs='' - dirs+='# Directories\n' - dirs+=$( - find "$T_ROOT" \ - -maxdepth 3 \ - -mindepth 1 \ - -type d \ - -not -path '*/dist/*' \ - -not -path '*/dist' \ - -not -path '*/node_modules/*' \ - -not -path '*/node_modules' \ - -not -path '*/vendor/*' \ - -not -path '*/vendor' \ - -not -path '*/.idea/*' \ - -not -path '*/.idea' \ - -not -path '*/.vscode/*' \ - -not -path '*/.vscode' \ - -not -path '*/.git/*' \ - -not -path '*/.git' \ - -not -path '*/.svn/*' \ - -not -path '*/.svn' - ) - dirs+="$(printf "\n%s" "$DOTFILES")" +# Validate that T_ROOT exists +if [[ ! -d "$T_ROOT" ]]; then + error_exit "T_ROOT directory '$T_ROOT' does not exist." +fi - echo "$dirs" + +# Check for required dependencies +check_dependencies() { + local T_DEPS=(tmux fzf find) + for cmd in "${T_DEPS[@]}"; do + if ! command -v "$cmd" &> /dev/null; then + error_exit "$cmd is not installed." + fi + done } -check_tmux() -{ +check_dependencies + +# Generate an array of '-not -path' rules for each exclusion pattern +# without using namerefs. +generate_exclude_rules() { + local result_var="$1" + shift + local arr=() + for pattern in "$@"; do + # Exclude both the directory and any subdirectories under it. + arr+=( -not -path "*/${pattern}" -not -path "*/${pattern}/*" ) + done + # Use eval to assign the array to the variable whose name was passed. + eval "$result_var=(\"\${arr[@]}\")" +} + +get_directories() { + local exclude_patterns=( + ".bzr" ".git" ".hg" ".idea" ".obsidian" ".run" ".svn" ".vscode" + "build" "dist" "node_modules" "out" "target" "vendor" + ) + local exclude_rules=() + generate_exclude_rules exclude_rules "${exclude_patterns[@]}" + + local dirs + # Use $'string' to correctly process escape sequences. + dirs=$'# Directories\n' + dirs+=$(find "$T_ROOT" \ + -maxdepth "$T_MAX_DEPTH" \ + -mindepth 1 \ + -type d \ + "${exclude_rules[@]}" + ) + echo -e "$dirs" +} + +check_tmux() { if ! command -v tmux &> /dev/null; then error_exit "tmux is not installed." fi - # check to see that tmux server is running + # Ensure tmux server is running if ! tmux info &> /dev/null; then tmux start-server fi } -get_sessions() -{ +get_sessions() { check_tmux - local sessions='' - sessions+='# Sessions\n' - sessions+=$(tmux list-sessions -F "#{session_name}" 2> /dev/null) + T_TMUX_SESSIONS=$(tmux list-sessions -F "#{session_name}" 2> /dev/null) - echo "$sessions" + if [[ -z "$T_TMUX_SESSIONS" ]]; then + echo "" + return + fi + + echo -e "# Sessions\n$T_TMUX_SESSIONS" } -items='' - -# Select the directory +# Determine selection from command-line argument or interactive fzf menu if [[ $# -eq 1 ]]; then selected="$1" else - items+=$(get_sessions | sort) - items+='\n' - items+=$(get_directories | sort) + # Combine sessions and directories for selection + T_ITEMS="$(get_sessions | sort) +$(get_directories | sort)" - selected=$(echo -e "$items" | fzf) || exit 0 # Exit if no selection is made + # Use sort to order the entries and fzf for interactive selection + selected=$(echo "$T_ITEMS" | fzf) || exit 0 fi -# If user selected a header, exit -[[ ${selected:0:1} == "#" ]] && error_exit "You selected a header, why?" +# Reject selection if it is a header line +[[ ${selected:0:1} == "#" ]] && error_exit "Header selected. Please choose a valid session or directory." -# Exit if no directory was selected -[[ -z $selected ]] && error_exit "No directory selected." +[[ -z "$selected" ]] && error_exit "No directory or session selected." # Sanitize the session name session_name=$(basename "$selected") -# If we get nothing, we are dealing with a session -[[ $session_name == "" ]] && session_name="$selected" -# Remove dots from the session name as tmux doesn't like them +if [[ -z "$session_name" ]]; then + session_name="$selected" +fi +# Remove dots since tmux dislikes them session_name="${session_name//./}" -# Try to switch to the tmux session -tmux switch-client -t "=$session_name" +# Attempt to switch to an existing session +tmux switch-client -t "=$session_name" 2>/dev/null +active_session=$(tmux display-message -p -F '#{session_name}' 2>/dev/null) -active_session=$(tmux display-message -p -F '#{session_name}' 2> /dev/null) -# echo "active session: $active_session" -if [ -n "$active_session" ] && [ "$active_session" == "$session_name" ]; then +if [[ "$active_session" == "$session_name" ]]; then exit 0 fi -# Create a new tmux session or attach to an existing one -if tmux new-session -c "$selected" -d -s "$session_name" 2> /dev/null; then - tmux switch-client -t "$session_name" +# Create a new session (or attach to an existing one) based on the selection +if [ -z "$TMUX" ]; then + # Not inside tmux: create (or attach to) the session and attach. + tmux new-session -A -s "$session_name" -c "$selected" else - tmux new -c "$selected" -A -s "$session_name" + # Inside tmux: check if the target session exists. + if tmux has-session -t "$session_name" 2>/dev/null; then + # Session exists; switch to it. + tmux switch-client -t "$session_name" + else + # Session does not exist; create it in detached mode and then switch. + tmux new-session -d -s "$session_name" -c "$selected" + tmux switch-client -t "$session_name" + fi fi diff --git a/local/bin/x-clean-vendordirs b/local/bin/x-clean-vendordirs new file mode 100755 index 0000000..876730a --- /dev/null +++ b/local/bin/x-clean-vendordirs @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# vim: ft=bash sw=2 ts=2 et +# +# Removes vendor and node_modules directories from the +# current directory and all subdirectories. +# +# Author: Ismo Vuorinen 2025 +# License: MIT + +# Check if the user has provided a directory as an argument +if [ "$1" ]; then + # Check if the directory exists + if [ -d "$1" ]; then + CLEANDIR="$1" + else + msgr err "Directory $1 does not exist." + exit 1 + fi +else + CLEANDIR="." +fi + +# Function to remove node_modules and vendor folders +remove_node_modules_vendor() { + local dir=$1 + + # If the directory is a symlink, skip it + if [ -L "$dir" ]; then + msgr msg "Skipping symlink $dir" + return + fi + + # Check if the directory exists + if [ -d "$dir" ]; then + # If node_modules or vendor folder exists, remove it and all its contents + if [ -d "$dir/node_modules" ]; then + msgr run "Removing $dir/node_modules" + rm -rf "$dir/node_modules" + fi + + if [ -d "$dir/vendor" ]; then + msgr run "Removing $dir/vendor" + rm -rf "$dir/vendor" + fi + + # Recursively check subdirectories + for item in "$dir"/*; do + remove_node_modules_vendor "$item" + done + fi +} + +# Start removing node_modules and vendor folders from the current working directory +remove_node_modules_vendor "$CLEANDIR" diff --git a/local/bin/x-foreach b/local/bin/x-foreach index baf4719..a2a84e2 100755 --- a/local/bin/x-foreach +++ b/local/bin/x-foreach @@ -1,6 +1,7 @@ #!/usr/bin/env bash # # foreach +# foreach "ls -d */" "git status" # run git status in each folder # # Source: https://github.com/mvdan/dotfiles/blob/master/.bin/foreach @@ -11,6 +12,7 @@ for dir in $($cmd); do ( echo "$dir" cd "$dir" || exit 1 + # shellcheck disable=SC2294,SC2034 eval "$@" # allow multiple commands like "foo && bar" ) done diff --git a/local/bin/x-gh-get-latest-release-targz b/local/bin/x-gh-get-latest-release-targz index c374814..3870ada 100755 --- a/local/bin/x-gh-get-latest-release-targz +++ b/local/bin/x-gh-get-latest-release-targz @@ -1,7 +1,7 @@ #!/usr/bin/env bash # # Fetch the latest release version of a GitHub repository in tar.gz format (e.g. v1.0.0.tar.gz) -# Usage: x-gh-get-latest-release-targ [--get] +# Usage: x-gh-get-latest-release-targz [--get] # Author: Ismo Vuorinen 2024 set -euo pipefail diff --git a/local/bin/x-gh-get-latest-version b/local/bin/x-gh-get-latest-version index 77547b5..a12f834 100755 --- a/local/bin/x-gh-get-latest-version +++ b/local/bin/x-gh-get-latest-version @@ -1,66 +1,190 @@ #!/usr/bin/env bash # -# Get latest release version from GitHub +# Get latest release version, branch tag, or latest commit from GitHub # Usage: x-gh-get-latest-version # Author: Ismo Vuorinen 2024 set -euo pipefail -# Enable verbosity with VERBOSE=1 +# Environment variables, more under get_release_version() and get_latest_branch_tag() +# functions. These can be overridden by the user. +GITHUB_API_URL="${GITHUB_API_URL:-https://api.github.com/repos}" VERBOSE="${VERBOSE:-0}" -# Function to print usage information -usage() -{ - echo "Usage: $0 (e.g. ivuorinen/dotfiles)" +# Prints a message if VERBOSE=1 +msg() { + [[ "$VERBOSE" -eq 1 ]] && echo "$1" +} + +# Show usage information +usage() { + cat < (e.g. ivuorinen/dotfiles) + +Fetches the latest release version, latest branch tag, or latest commit SHA from GitHub. + +Options: + - INCLUDE_PRERELEASES=1 Include prerelease versions (default: only stable releases). + - OLDEST_RELEASE=1 Fetch the oldest release instead of the latest. + - BRANCH= Fetch the latest tag from a specific branch (default: main). + - LATEST_COMMIT=1 Fetch the latest commit SHA from the specified branch. + - OUTPUT=json Return output as JSON (default: plain text). + - GITHUB_API_URL= Override GitHub API URL (useful for GitHub Enterprise). + - GITHUB_TOKEN= Use GitHub API token to increase rate limits (default: unauthenticated). + +Requirements: + - curl + - jq (for JSON processing) + +Examples: + # Fetch the latest stable release + $0 ivuorinen/dotfiles + + # Fetch the latest release including prereleases + INCLUDE_PRERELEASES=1 $0 ivuorinen/dotfiles + + # Fetch the oldest release + OLDEST_RELEASE=1 $0 ivuorinen/dotfiles + + # Fetch the latest tag from the 'develop' branch + BRANCH=develop $0 ivuorinen/dotfiles + + # Fetch the latest commit SHA from 'main' branch + LATEST_COMMIT=1 $0 ivuorinen/dotfiles + + # Output result in JSON format + OUTPUT=json $0 ivuorinen/dotfiles + + # Use GitHub API token for higher rate limits + GITHUB_TOKEN="your_personal_access_token" $0 ivuorinen/dotfiles + + # Use GitHub Enterprise API + GITHUB_API_URL="https://github.example.com/api/v3/repos" $0 ivuorinen/dotfiles +EOF exit 1 } -# Function to print messages if VERBOSE is enabled -# $1 - message (string) -msg() -{ - [[ "$VERBOSE" -eq 1 ]] && echo "$1" - return 0 +# Check that required dependencies are installed +check_dependencies() { + for cmd in curl jq; do + if ! command -v "$cmd" &>/dev/null; then + echo "Error: '$cmd' is required but not installed." >&2 + exit 1 + fi + done } -# Function to fetch the latest release version from GitHub +# Fetches the latest release or the oldest if OLDEST_RELEASE=1 # $1 - GitHub repository (string) -get_latest_release() -{ - local repo=$1 +get_release_version() { + local repo="$1" + local include_prereleases="${INCLUDE_PRERELEASES:-0}" + local oldest_release="${OLDEST_RELEASE:-0}" + local api_url="${GITHUB_API_URL}/${repo}/releases" + + local auth_header=() + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + auth_header=(-H "Authorization: token $GITHUB_TOKEN") + fi + + msg "Fetching release data from: $api_url (Include prereleases: $include_prereleases, Oldest: $oldest_release)" + + local json_response + json_response=$(curl -sSL "${auth_header[@]}" "$api_url") + + # Check for API errors + if echo "$json_response" | jq -e 'has("message")' >/dev/null; then + msg "GitHub API error: $(echo "$json_response" | jq -r '.message')" + exit 1 + fi + + local filter='.[] | select(.tag_name)' + [[ "$include_prereleases" -eq 0 ]] && filter+='.prerelease == false' local version - version=$(curl -s "https://api.github.com/repos/${repo}/releases/latest" \ - | grep "tag_name" \ - | awk -F '"' '{print $4}') + if [[ "$oldest_release" -eq 1 ]]; then + version=$(echo "$json_response" | jq -r "[${filter}] | last.tag_name // empty") + else + version=$(echo "$json_response" | jq -r "[${filter}] | first.tag_name // empty") + fi - if [ -z "$version" ]; then - msg "Failed to fetch the latest release version for repository: $repo" - echo "" + if [[ -z "$version" ]]; then + msg "Failed to fetch release version for repository: $repo" exit 1 fi echo "$version" - return 0 +} + +# Fetches the latest tag from the specified branch +get_latest_branch_tag() { + local repo="$1" + local branch="${BRANCH:-main}" + local api_url="${GITHUB_API_URL}/${repo}/git/refs/tags" + + msg "Fetching latest tag for branch '$branch' from: $api_url" + + local json_response + json_response=$(curl -sSL "$api_url") + + local version + version=$(echo "$json_response" | jq -r "[.[] | select(.ref | contains(\"refs/tags/$branch\"))] | last.ref | sub(\"refs/tags/\"; \"\") // empty") + + if [[ -z "$version" ]]; then + msg "Failed to fetch latest tag for branch: $branch" + exit 1 + fi + + echo "$version" +} + +# Fetches the latest commit SHA from the specified branch +get_latest_commit() { + local repo="$1" + local branch="${BRANCH:-main}" + local api_url="${GITHUB_API_URL}/${repo}/commits/$branch" + + msg "Fetching latest commit SHA from: $api_url" + + local json_response + json_response=$(curl -sSL "$api_url") + + local sha + sha=$(echo "$json_response" | jq -r '.sha // empty') + + if [[ -z "$sha" ]]; then + msg "Failed to fetch latest commit SHA for branch: $branch" + exit 1 + fi + + echo "$sha" } # Main function -main() -{ - if [ "$#" -ne 1 ]; then +# $1 - GitHub repository (string) +main() { + if [[ $# -ne 1 ]]; then usage fi - local repo=$1 + check_dependencies - msg "Fetching the latest release version for repository: $repo" + local repo="$1" + local result - local version - version=$(get_latest_release "$repo") + if [[ "${LATEST_COMMIT:-0}" -eq 1 ]]; then + result=$(get_latest_commit "$repo") + elif [[ -n "${BRANCH:-}" ]]; then + result=$(get_latest_branch_tag "$repo") + else + result=$(get_release_version "$repo") + fi - echo "$version" - return 0 + if [[ "${OUTPUT:-text}" == "json" ]]; then + echo "{\"repository\": \"$repo\", \"result\": \"$result\"}" + else + echo "$result" + fi } main "$@" diff --git a/local/bin/x-localip b/local/bin/x-localip index 2e5fc7b..9f3d299 100755 --- a/local/bin/x-localip +++ b/local/bin/x-localip @@ -6,11 +6,12 @@ # License: MIT VERSION="1.0.0" +SCRIPT_NAME="$(basename "$0")" # Function to display usage usage() { - echo "Usage: x-localip [options] [interface]" + echo "Usage: $SCRIPT_NAME [options] [interface]" echo "Options:" echo " --help Show this help message" echo " --version Show version information" @@ -31,7 +32,7 @@ while [[ $# -gt 0 ]]; do exit 0 ;; --version) - echo "x-localip version $VERSION" + echo "$SCRIPT_NAME version $VERSION" exit 0 ;; --ipv4) diff --git a/local/bin/x-multi-ping b/local/bin/x-multi-ping new file mode 100755 index 0000000..4f4b638 --- /dev/null +++ b/local/bin/x-multi-ping @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +# x-multi-ping: Multi-protocol ping wrapper in Bash +# +# Description: +# This script pings a list of hostnames using both IPv4 and IPv6 protocols. +# It uses the 'dig' command to resolve the hostnames and then pings each IP +# address found. The script can run once or loop indefinitely with a sleep +# interval between iterations. +# +# This script is based on the original work by Steve Kemp. +# Original work Copyright (c) 2014 by Steve Kemp. +# +# The code in the original repository may be modified and distributed under your choice of: +# * The Perl Artistic License (http://dev.perl.org/licenses/artistic.html) or +# * The GNU General Public License, version 2 or later (http://www.gnu.org/licenses/gpl2.txt). +# +# Modifications and enhancements by Ismo Vuorinen on 2025. +# +# Usage: +# x-multi-ping [--loop|--forever] [--sleep=N] hostname1 hostname2 ... +# +# Options: +# --help Display this help message. +# --verbose Enable verbose output. +# --loop, --forever Loop indefinitely. +# --sleep=N Sleep N seconds between iterations (default: 1). +# +# Examples: +# x-multi-ping example.com +# x-multi-ping --loop --sleep=5 example.com +# x-multi-ping --forever example.com example.org +# +# Dependencies: +# - dig (DNS lookup utility) +# - ping (ICMP ping utility) +# - ping6 (IPv6 ping utility) or ping -6 (alternative) +# + +# Defaults +LOOP=0 +SLEEP=1 +VERBOSE=0 +TIMEOUT=5 + +usage() +{ + echo "Usage: $0 [--loop|--forever] [--sleep=N] hostname1 hostname2 ..." + echo "Options:" + echo " --help Display this help message." + echo " --verbose Enable verbose output." + echo " --loop, --forever Loop indefinitely." + echo " --sleep=N Sleep N seconds between iterations (default: 1)." +} + +# Parse command-line options +while [[ $# -gt 0 ]]; do + case "$1" in + --help) + usage + exit 0 + ;; + --verbose) + # shellcheck disable=SC2034 + VERBOSE=1 + shift + ;; + --loop | --forever) + LOOP=1 + shift + ;; + --sleep=*) + SLEEP="${1#*=}" + shift + ;; + --sleep) + if [[ -n "$2" ]]; then + SLEEP="$2" + shift 2 + else + echo "Error: --sleep requires a numeric value." + exit 1 + fi + ;; + --*) + echo "Unknown option: $1" + usage + exit 1 + ;; + *) + break + ;; + esac +done + +# Check for required hostnames +if [[ $# -lt 1 ]]; then + usage + exit 1 +fi + +# Dependency check for dig and ping +if ! command -v dig > /dev/null 2>&1; then + echo "The required 'dig' command is missing. Aborting." + exit 1 +fi + +if ! command -v ping > /dev/null 2>&1; then + echo "The required 'ping' command is missing. Aborting." + exit 1 +fi + +# Determine how to invoke IPv6 ping +if command -v ping6 > /dev/null 2>&1; then + PING6="ping6" +elif ping -6 -c1 ::1 > /dev/null 2>&1; then + PING6="ping -6" +else + echo "The required IPv6 ping command (ping6 or ping -6) is missing. Aborting." + exit 1 +fi + +# Function to remove any URI scheme and port from the hostname. +strip_hostname() +{ + local host="$1" + # Remove leading scheme (e.g., http://) if present. + if [[ "$host" =~ ^[a-z]+://([^/]+)/? ]]; then + host="${BASH_REMATCH[1]}" + fi + # Remove a port if specified (e.g., example.com:80). + if [[ "$host" =~ ^([^:]+):[0-9]+$ ]]; then + host="${BASH_REMATCH[1]}" + fi + echo "$host" +} + +# Function to ping a given host based on DNS lookups. +pingHost() +{ + local original_host="$1" + local host + host=$(strip_hostname "$original_host") + + for type in A AAAA; do + # Look up the DNS records for the host. + ips=$(dig +short "$host" "$type") + if [[ -z "$ips" ]]; then + echo "WARNING: Failed to resolve $host [$type]" + else + # For each IP address found, perform the appropriate ping. + while IFS= read -r ip; do + if [[ "$type" == "A" ]]; then + ping_binary="ping" + else + ping_binary="$PING6" + fi + + # Execute ping with one packet and a timeout. + $ping_binary -c1 -w"$TIMEOUT" -W"$TIMEOUT" "$host" > /dev/null 2>&1 + # shellcheck disable=SC2181 + if [[ $? -eq 0 ]]; then + echo "Host $host - $ip - alive" + else + echo "Host $host - $ip - FAILED" + fi + done <<< "$ips" + fi + done +} + +# Main loop: run once or forever based on the options. +if [[ $LOOP -eq 1 ]]; then + while true; do + for host in "$@"; do + pingHost "$host" + done + sleep "$SLEEP" + done +else + for host in "$@"; do + pingHost "$host" + done +fi diff --git a/local/bin/x-open-ports b/local/bin/x-open-ports index 7bfc868..bed4377 100755 --- a/local/bin/x-open-ports +++ b/local/bin/x-open-ports @@ -1,37 +1,141 @@ #!/usr/bin/env bash # -# List open (listened) ports, without the crud that -# usually comes with `lsof -i` +# List open (listened) ports in Markdown or JSON format. # -# Modified by: Ismo Vuorinen 2020 +# Modified by: Ismo Vuorinen 2020, 2025 # Originally from: https://www.commandlinefu.com/commands/view/8951 # Original author: https://www.commandlinefu.com/commands/by/wickedcpj set -euo pipefail -# Function to print the header -print_header() +FORMAT="markdown" + +# Function to print help message +print_help() { - echo 'User: Command: PID: Port:' - echo '=========================================================' + cat << EOF +Usage: $(basename "$0") [OPTIONS] + +List open (listened) ports in a formatted table (Markdown) or JSON. + +Options: + --json Output results in JSON format instead of Markdown + --help Show this help message + +Examples: + $(basename "$0") # List open ports as a Markdown table + $(basename "$0") --json # List open ports in JSON format + +EOF + exit 0 } -# Function to list open ports +# Function to print the Markdown table header +print_header() +{ + echo "| User | Command | PID | Port |" + echo "|------------------|----------------------------|----------|---------|" +} + +# Function to list open ports using lsof +list_open_ports_lsof() +{ + lsof -i -P -n -sTCP:LISTEN +c 0 2> /dev/null | awk ' + NR > 1 { + port = $9 + sub(/.*:/, "", port) # Extract port number + printf "| %-16s | %-26s | %-8s | %-7s |\n", substr($3, 1, 16), substr($1, 1, 26), substr($2, 1, 8), port + } + ' | sort -k3,3n | uniq +} + +# Function to list open ports using ss (alternative) +list_open_ports_ss() +{ + ss -ltpn 2> /dev/null | awk ' + NR > 1 { + split($5, addr, ":") + port = addr[length(addr)] + user = $1 + cmd = $7 + sub(/users:\(\(/, "", cmd) # Cleanup command + sub(/\)\)/, "", cmd) + pid = "-" + match(cmd, /pid=([0-9]+)/, m) + if (m[1] != "") pid = m[1] + printf "| %-16s | %-26s | %-8s | %-7s |\n", substr(user, 1, 16), substr(cmd, 1, 26), substr(pid, 1, 8), port + } + ' | sort -k3,3n | uniq +} + +# Function to print JSON output +list_open_ports_json() +{ + if command -v lsof &> /dev/null; then + lsof -i -P -n -sTCP:LISTEN +c 0 2> /dev/null | awk ' + NR > 1 { + port = $9 + sub(/.*:/, "", port) # Extract port number + printf "{\"user\": \"%s\", \"command\": \"%s\", \"pid\": \"%s\", \"port\": \"%s\"},\n", $3, $1, $2, port + } + ' | sort -k3,3n | uniq | sed '$ s/,$//' + elif command -v ss &> /dev/null; then + ss -ltpn 2> /dev/null | awk ' + NR > 1 { + split($5, addr, ":") + port = addr[length(addr)] + user = $1 + cmd = $7 + sub(/users:\(\(/, "", cmd) + sub(/\)\)/, "", cmd) + pid = "-" + match(cmd, /pid=([0-9]+)/, m) + if (m[1] != "") pid = m[1] + printf "{\"user\": \"%s\", \"command\": \"%s\", \"pid\": \"%s\", \"port\": \"%s\"},\n", user, cmd, pid, port + } + ' | sort -k3,3n | uniq | sed '$ s/,$//' + else + echo "[]" + fi +} + +# Function to determine available command list_open_ports() { - lsof -i 4 -P -n +c 0 \ - | grep -i 'listen' \ - | awk '{print $3, $1, $2, $9}' \ - | sed 's/ [a-z0-9\.\*]*:/ /' \ - | sort -k 3 -n \ - | xargs printf '%-15s %-25s %-8s %-5s\n' \ - | uniq + if [[ "$FORMAT" == "json" ]]; then + echo "[" + list_open_ports_json + echo "]" + else + print_header + if command -v lsof &> /dev/null; then + list_open_ports_lsof + elif command -v ss &> /dev/null; then + list_open_ports_ss + else + echo "**Error:** Neither 'lsof' nor 'ss' is available." + exit 1 + fi + fi } # Main function main() { - print_header + case "${1:-}" in + --json) + FORMAT="json" + ;; + --help) + print_help + ;; + "") ;; + *) + echo "Unknown option: $1" + print_help + ;; + esac + list_open_ports echo "" } diff --git a/local/bin/x-path b/local/bin/x-path new file mode 100755 index 0000000..9b6bb6e --- /dev/null +++ b/local/bin/x-path @@ -0,0 +1,281 @@ +#!/usr/bin/env bash +# +# x-path: A unified script to manipulate the PATH variable. +# +# This script supports four subcommands: +# - append (or a): Remove duplicates and append one or more directories. +# - prepend (or p): Remove duplicates and prepend one or more directories. +# - remove: Remove one or more directories from PATH. +# - check: Check if the directories (or all directories in PATH if none provided) are valid. +# +# All directory arguments are normalized (trailing slashes removed, except for "/"), +# and the current PATH is normalized before any operations. +# +# Usage: +# x-path [ ...] +# +# Examples: +# x-path append /usr/local/bin /opt/bin +# x-path p /home/user/bin +# x-path remove /usr/local/bin +# x-path check # Check all directories in PATH +# x-path check /usr/local/bin /bin +# +# Enable verbose output by setting: +# export VERBOSE=1 + +VERBOSE="${VERBOSE:-0}" + +####################################### +# Normalize a directory by removing a trailing slash (unless the directory is "/"). +# Globals: +# None +# Arguments: +# $1 - Directory path to normalize +# Returns: +# Echoes the normalized directory. +####################################### +normalize_dir() +{ + local d="$1" + if [ "$d" != "/" ]; then + d="${d%/}" + fi + echo "$d" +} + +####################################### +# Normalize the PATH variable by normalizing each of its components. +# Globals: +# PATH +# Arguments: +# None +# Returns: +# Updates and exports PATH. +####################################### +normalize_path_var() +{ + local new_path="" + local d + IFS=':' read -r -a arr <<< "$PATH" + for d in "${arr[@]}"; do + d=$(normalize_dir "$d") + if [ -z "$new_path" ]; then + new_path="$d" + else + new_path="$new_path:$d" + fi + done + PATH="$new_path" + export PATH +} + +####################################### +# Remove all occurrences of a normalized directory from PATH. +# Globals: +# PATH +# Arguments: +# $1 - Normalized directory to remove from PATH. +# Returns: +# Updates PATH. +####################################### +remove_from_path() +{ + local d="$1" + PATH=":${PATH}:" + PATH="${PATH//:$d:/:}" + PATH="${PATH#:}" + PATH="${PATH%:}" +} + +####################################### +# Append one or more directories to PATH. +# Globals: +# PATH, VERBOSE +# Arguments: +# One or more directory paths. +# Returns: +# Updates PATH. +####################################### +do_append() +{ + local processed="" + local d + for arg in "$@"; do + d=$(normalize_dir "$arg") + if [[ " $processed " == *" $d "* ]]; then + continue + else + processed="$processed $d" + fi + + if [ ! -d "$d" ]; then + [ "$VERBOSE" -eq 1 ] && echo "(?) Directory '$d' does not exist. Skipping." + continue + fi + + remove_from_path "$d" + PATH="${PATH:+"$PATH:"}$d" + [ "$VERBOSE" -eq 1 ] && echo "Appended '$d' to PATH." + done + export PATH +} + +####################################### +# Prepend one or more directories to PATH. +# Directories are processed in reverse order so that the first argument ends up leftmost. +# Globals: +# PATH, VERBOSE +# Arguments: +# One or more directory paths. +# Returns: +# Updates PATH. +####################################### +do_prepend() +{ + local processed="" + local d + local -a arr=("$@") + local i + for ((i = ${#arr[@]} - 1; i >= 0; i--)); do + d=$(normalize_dir "${arr[i]}") + if [[ " $processed " == *" $d "* ]]; then + continue + else + processed="$processed $d" + fi + + if [ ! -d "$d" ]; then + [ "$VERBOSE" -eq 1 ] && echo "(?) Directory '$d' does not exist. Skipping." + continue + fi + + remove_from_path "$d" + PATH="$d${PATH:+":$PATH"}" + [ "$VERBOSE" -eq 1 ] && echo "Prepended '$d' to PATH." + done + export PATH +} + +####################################### +# Remove one or more directories from PATH. +# Globals: +# PATH, VERBOSE +# Arguments: +# One or more directory paths. +# Returns: +# Updates PATH. +####################################### +do_remove() +{ + local processed="" + local d + for arg in "$@"; do + d=$(normalize_dir "$arg") + if [[ " $processed " == *" $d "* ]]; then + continue + else + processed="$processed $d" + fi + + case ":$PATH:" in + *":$d:"*) + remove_from_path "$d" + [ "$VERBOSE" -eq 1 ] && echo "Removed '$d' from PATH." + ;; + *) + [ "$VERBOSE" -eq 1 ] && echo "(?) '$d' is not in PATH." + ;; + esac + done + export PATH +} + +####################################### +# Check the validity of directories. +# If arguments are provided, check those directories; otherwise, check all directories in PATH. +# Globals: +# PATH +# Arguments: +# Zero or more directory paths. +# Returns: +# Outputs the validity status of each directory. +####################################### +do_check() +{ + local d + if [ "$#" -eq 0 ]; then + echo "Checking all directories in PATH:" + IFS=':' read -r -a arr <<< "$PATH" + for d in "${arr[@]}"; do + d=$(normalize_dir "$d") + if [ -d "$d" ]; then + echo "Valid: $d" + else + echo "Invalid: $d" + fi + done + else + for arg in "$@"; do + d=$(normalize_dir "$arg") + if [ -d "$d" ]; then + echo "Valid: $d" + else + echo "Invalid: $d" + fi + done + fi +} + +####################################### +# Main routine: Parse subcommand and arguments, normalize PATH, +# and dispatch to the appropriate functionality. +####################################### +if [ "$#" -lt 1 ]; then + echo "Usage: $0 [ ...]" + echo "Commands:" + echo " append (or a) - Append directories to PATH" + echo " prepend (or p) - Prepend directories to PATH" + echo " remove - Remove directories from PATH" + echo " check - Check validity of directories (or all in PATH if none given)" + exit 1 +fi + +cmd="$1" +shift + +# Normalize the current PATH variable. +normalize_path_var + +case "$cmd" in + append | a) + [ "$#" -ge 1 ] || { + echo "Usage: $0 append [ ...]" + exit 1 + } + do_append "$@" + ;; + prepend | p) + [ "$#" -ge 1 ] || { + echo "Usage: $0 prepend [ ...]" + exit 1 + } + do_prepend "$@" + ;; + remove) + [ "$#" -ge 1 ] || { + echo "Usage: $0 remove [ ...]" + exit 1 + } + do_remove "$@" + ;; + check) + # If no directories are provided, check all directories in PATH. + do_check "$@" + ;; + *) + echo "Unknown command: $cmd" + echo "Usage: $0 [ ...]" + exit 1 + ;; +esac diff --git a/local/bin/x-path-append b/local/bin/x-path-append index 57a8dc9..9204fa5 100755 --- a/local/bin/x-path-append +++ b/local/bin/x-path-append @@ -1,40 +1,44 @@ #!/usr/bin/env bash # -# Add a directory to the beginning of the PATH if it's not already there. -# Usage: x-path-append +# Optimized script to append directories to PATH. +# For each given directory, it removes all duplicate occurrences from PATH +# and then appends it if the directory exists. +# +# Usage: x-path-append [ ...] +# +# Enable verbose output by setting the environment variable VERBOSE=1. +# +# Author: Ismo Vuorinen 2024 +# License: MIT -# Set verbosity with VERBOSE=1 VERBOSE="${VERBOSE:-0}" -# Function to print messages if VERBOSE is enabled -# $1 - message (string) -msg() -{ - [[ "$VERBOSE" -eq 1 ]] && echo "$1" +# Ensure that at least one directory is provided. +[ "$#" -lt 1 ] && { + echo "Usage: $0 [ ...]" + exit 1 } -if [ "$#" -ne 1 ]; then - echo "Usage: $0 " - exit 1 -fi +for dir in "$@"; do + # Check if the specified directory exists. + if [ ! -d "$dir" ]; then + [ "$VERBOSE" -eq 1 ] && echo "(?) Directory '$dir' does not exist. Skipping." + continue + fi -dir="$1" + # Remove all duplicate occurrences of the directory from PATH. + case ":$PATH:" in + *":$dir:"*) + PATH=":${PATH}:" + PATH="${PATH//:$dir:/:}" + PATH="${PATH#:}" + PATH="${PATH%:}" + [ "$VERBOSE" -eq 1 ] && echo "Removed previous occurrences of '$dir' from PATH." + ;; + *) ;; + esac -if echo "$PATH" | grep -qE "(^|:)$dir($|:)"; then - export PATH=$(echo -n "$PATH" | awk -v RS=: -v ORS=: "\$0 != \"$dir\"" | sed 's/:$//') - msg "Directory $dir has been removed from PATH" -else - msg "Directory $dir is not in PATH" -fi - -if [ ! -d "$dir" ]; then - msg "(?) Directory $dir does not exist" - exit 0 -fi - -if echo "$PATH" | grep -qE "(^|:)$dir($|:)"; then - msg "(!) Directory $dir is already in PATH" -else + # Append the directory to PATH. export PATH="${PATH:+"$PATH:"}$dir" - msg "(!) Directory $dir has been added to the end of PATH" -fi + [ "$VERBOSE" -eq 1 ] && echo "Appended '$dir' to PATH." +done diff --git a/local/bin/x-path-prepend b/local/bin/x-path-prepend index 8797b7a..3dd0b4f 100755 --- a/local/bin/x-path-prepend +++ b/local/bin/x-path-prepend @@ -1,33 +1,50 @@ #!/usr/bin/env bash # -# Add a directory to the front of the PATH if it exists and is not already there -# Usage: x-path-prepend +# Optimized script to batch prepend directories to PATH. +# For each given directory, it removes all duplicate occurrences from PATH +# and then prepends it. Directories that do not exist are skipped. +# +# Usage: x-path-prepend [ ...] +# +# Enable verbose output by setting the environment variable VERBOSE=1. +# +# Author: Ismo Vuorinen 2024 +# License: MIT -# Set verbosity with VERBOSE=1 VERBOSE="${VERBOSE:-0}" -# Function to print messages if VERBOSE is enabled -# $1 - message (string) -msg() -{ - [[ "$VERBOSE" -eq 1 ]] && echo "$1" +# Ensure that at least one argument is provided. +[ "$#" -lt 1 ] && { + echo "Usage: $0 [ ...]" + exit 1 } -if [ "$#" -ne 1 ]; then - echo "Usage: $0 " - exit 1 -fi +# Save the arguments in an array. +dirs=("$@") -dir="$1" +# Process the directories in reverse order so that the first argument ends up leftmost in PATH. +for ((idx = ${#dirs[@]} - 1; idx >= 0; idx--)); do + dir="${dirs[idx]}" -if [ ! -d "$dir" ]; then - msg "(?) Directory $dir does not exist" - exit 0 -fi + # Check if the specified directory exists. + if [ ! -d "$dir" ]; then + [ "$VERBOSE" -eq 1 ] && echo "(?) Directory '$dir' does not exist. Skipping." + continue + fi -if echo "$PATH" | grep -qE "(^|:)$dir($|:)"; then - msg "(!) Directory $dir is already in PATH" -else + # Remove all duplicate occurrences of the directory from PATH using built-in string operations. + case ":$PATH:" in + *":$dir:"*) + PATH=":${PATH}:" + PATH="${PATH//:$dir:/:}" + PATH="${PATH#:}" + PATH="${PATH%:}" + [ "$VERBOSE" -eq 1 ] && echo "Removed duplicate occurrences of '$dir' from PATH." + ;; + *) ;; + esac + + # Prepend the directory to PATH. export PATH="$dir${PATH:+":$PATH"}" - msg "(!) Directory $dir has been added to the front of PATH" -fi + [ "$VERBOSE" -eq 1 ] && echo "Prepended '$dir' to PATH." +done diff --git a/local/bin/x-path-remove b/local/bin/x-path-remove index fc17fa0..fca0390 100755 --- a/local/bin/x-path-remove +++ b/local/bin/x-path-remove @@ -1,29 +1,41 @@ #!/usr/bin/env bash # -# Remove a directory from the PATH -# Usage: x-path-remove +# Optimized script to remove directories from PATH. +# For each specified directory, all occurrences are removed from PATH. +# +# Usage: x-path-remove [ ...] +# +# Enable verbose output by setting the environment variable VERBOSE=1. +# +# Author: Ismo Vuorinen 2024 +# License: MIT -# Set verbosity with VERBOSE=1 VERBOSE="${VERBOSE:-0}" -# Function to print messages if VERBOSE is enabled -# $1 - message (string) -msg() -{ - [[ "$VERBOSE" -eq 1 ]] && echo "$1" +# Ensure that at least one directory is provided. +[ "$#" -lt 1 ] && { + echo "Usage: $0 [ ...]" + exit 1 } -if [ "$#" -ne 1 ]; then - echo "Usage: $0 " - exit 1 -fi +for dir in "$@"; do + # Remove trailing slash if present, unless the directory is "/" + [ "$dir" != "/" ] && dir="${dir%/}" -dir="$1" + # Check if the directory is present in PATH. + case ":$PATH:" in + *":$dir:"*) + # Remove all occurrences of the directory from PATH using parameter expansion. + PATH=":${PATH}:" + PATH="${PATH//:$dir:/:}" + PATH="${PATH#:}" + PATH="${PATH%:}" + [ "$VERBOSE" -eq 1 ] && echo "Removed '$dir' from PATH." + ;; + *) + [ "$VERBOSE" -eq 1 ] && echo "(?) '$dir' is not in PATH." + ;; + esac +done -if ! echo "$PATH" | grep -qE "(^|:)$dir($|:)"; then - msg "(?) Directory $dir is not in PATH" - exit 0 -fi - -export PATH=$(echo -n "$PATH" | awk -v RS=: -v ORS=: "\$0 != \"$dir\"" | sed 's/:$//') -msg "(!) Directory $dir has been removed from PATH" +export PATH diff --git a/local/bin/x-quota-usage.php b/local/bin/x-quota-usage.php index 5ef77d5..07a7739 100755 --- a/local/bin/x-quota-usage.php +++ b/local/bin/x-quota-usage.php @@ -1,5 +1,6 @@ #!/usr/bin/env php */ + error_reporting(E_ALL); $debug = false; diff --git a/local/bin/x-record b/local/bin/x-record index 3cab45c..ad0f8b6 100755 --- a/local/bin/x-record +++ b/local/bin/x-record @@ -1,4 +1,4 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash # # DESCRIPTION: # Simple recording tool and wrapper around giph (ffmpeg). diff --git a/local/bin/x-set-php-aliases b/local/bin/x-set-php-aliases index c34150d..66bada0 100755 --- a/local/bin/x-set-php-aliases +++ b/local/bin/x-set-php-aliases @@ -1,71 +1,137 @@ #!/usr/bin/env bash -# Check which PHP versions are installed with brew, and create aliases for each installation. -# Copyright (c) 2023 Ismo Vuorinen. All Rights Reserved. +# ----------------------------------------------------------------------------- +# This script caches the list of PHP installations via Homebrew and generates +# shell aliases for each installation. Both the brew list and the generated +# alias definitions are stored in the XDG cache directory. +# +# If the brew list cache is invalid (older than CACHE_TTL), then both caches are +# regenerated. Otherwise, if only the alias cache is stale, it is regenerated +# from the brew list cache. +# +# Usage: +# source x-set-php-aliases.sh +# +# (C) 2023, 2025 Ismo Vuorinen. All Rights Reserved. +# ----------------------------------------------------------------------------- set -euo pipefail -# Set verbosity with VERBOSE=1 x-set-php-aliases +# Set verbosity level (0 by default; set to 1 or 2 for more detail) VERBOSE="${VERBOSE:-0}" - -# Enable debugging if verbosity is set to 2 [ "$VERBOSE" = "2" ] && set -x -# Check if brew is installed, if not exit. +# Exit early if Homebrew is not installed. if ! command -v brew &> /dev/null; then + echo "Homebrew is not installed. Exiting." exit 0 fi -# Function to read installed PHP versions using brew -get_php_versions() +# Determine Homebrew's prefix. +HOMEBREW_PREFIX="${HOMEBREW_PREFIX:-$(brew --prefix)}" + +# Determine the XDG cache directory (default to ~/.cache). +XDG_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}" +CACHE_DIR="${XDG_CACHE}/x-set-php-aliases" +mkdir -p "$CACHE_DIR" + +# Define cache file paths. +BREW_LIST_CACHE="${CACHE_DIR}/brew_list.cache" +ALIASES_CACHE="${CACHE_DIR}/aliases.cache" + +# Cache time-to-live in seconds (here 300 seconds = 5 minutes). +CACHE_TTL=300 + +# ----------------------------------------------------------------------------- +# Function: cache_is_valid +# Returns 0 if the file exists and its modification time is within TTL. +# ----------------------------------------------------------------------------- +cache_is_valid() { - local versions=() - while IFS="" read -r line; do - versions+=("$line") - done < <(bkt -- brew list | grep '^php') - echo "${versions[@]}" + local file="$1" + local ttl="$2" + if [[ -f "$file" ]]; then + local mod_time + if stat --version &> /dev/null; then + mod_time=$(stat -c %Y "$file") + else + mod_time=$(stat -f %m "$file") + fi + local current_time + current_time=$(date +%s) + if ((current_time - mod_time < ttl)); then + return 0 + fi + fi + return 1 } -# Function to create aliases for each PHP version -create_aliases() +# ----------------------------------------------------------------------------- +# Function: generate_aliases +# Reads PHP formulas (one per line) from the specified file and prints out +# alias definitions for each valid PHP installation. +# +# The following aliases are created (assuming the formula is "php@80"): +# +# p80r : Raw PHP (executable only) +# p80 : PHP with an error reporting flag enabled +# p80s : Launches a PHP local server at localhost:9000 +# p80c : Runs composer (if found) using this PHP and error reporting flag +# ----------------------------------------------------------------------------- +generate_aliases() { - local php_versions=("$@") + local brew_file="$1" local php_error_reporting='-d error_reporting=22527' + local composer_path + composer_path=$(command -v composer 2> /dev/null || true) - for version in "${php_versions[@]}"; do - [ "$VERBOSE" = "1" ] && echo "Setting aliases for $version" + while IFS= read -r version || [[ -n "$version" ]]; do + # Remove any leading/trailing whitespace. + version=$(echo "$version" | xargs) + [[ -z "$version" ]] && continue - # Drop the dot from version (e.g., 8.0 -> 80) + # Compute an alias name: remove dots and replace "php@" with "p" local php_abbr="${version//\./}" - # Replace "php@" with "p" so "php@80" becomes "p80" local php_alias="${php_abbr//php@/p}" - # Fetch the exec path once - local php_exec="$HOMEBREW_PREFIX/opt/$version/bin/php" - - if [ -f "$php_exec" ]; then - [ "$VERBOSE" = "1" ] && echo "-> php_exec $php_exec" - - # Raw PHP without error_reporting flag. - alias "${php_alias}r"="$php_exec" - - # PHP with error_reporting flag. - alias "$php_alias"="$php_exec $php_error_reporting" - - # Local PHP Server. - alias "${php_alias}s"="$php_exec -S localhost:9000" - - # Use composer with specific PHP and error_reporting flag on. - alias "${php_alias}c"="$php_exec $php_error_reporting $(which composer)" + local php_exec="${HOMEBREW_PREFIX}/opt/${version}/bin/php" + if [[ -x "$php_exec" ]]; then + echo "alias ${php_alias}r='$php_exec'" + echo "alias $php_alias='$php_exec $php_error_reporting'" + echo "alias ${php_alias}s='$php_exec -S localhost:9000'" + if [[ -n "$composer_path" ]]; then + echo "alias ${php_alias}c='$php_exec $php_error_reporting $composer_path'" + fi + else + [[ "$VERBOSE" -ge 1 ]] && echo "Executable not found: $php_exec (skipping alias for $version)" fi - done + done < "$brew_file" } -# Main function -main() -{ - local php_versions - php_versions=($(get_php_versions)) - create_aliases "${php_versions[@]}" -} +# ----------------------------------------------------------------------------- +# Main Cache Update Logic +# +# If the brew list cache is stale (or missing), regenerate it and the aliases. +# If only the alias cache is stale, regenerate just the alias cache. +# ----------------------------------------------------------------------------- +if ! cache_is_valid "$BREW_LIST_CACHE" "$CACHE_TTL"; then + [[ "$VERBOSE" -ge 1 ]] && echo "Brew list cache is stale or missing. Regenerating brew list and aliases." + # Regenerate the brew list cache (filtering only PHP formulas). + brew list | grep '^php' > "$BREW_LIST_CACHE" + # Generate the aliases cache from the new brew list. + generate_aliases "$BREW_LIST_CACHE" > "$ALIASES_CACHE" +else + [[ "$VERBOSE" -ge 1 ]] && echo "Using cached brew list from $BREW_LIST_CACHE." + if ! cache_is_valid "$ALIASES_CACHE" "$CACHE_TTL"; then + [[ "$VERBOSE" -ge 1 ]] && echo "Alias cache is stale or missing. Regenerating aliases." + generate_aliases "$BREW_LIST_CACHE" > "$ALIASES_CACHE" + fi +fi -main "$@" +# Source the cached alias definitions. +if [[ -f "$ALIASES_CACHE" ]]; then + # shellcheck source=/dev/null + source "$ALIASES_CACHE" + [[ "$VERBOSE" -ge 1 ]] && echo "Aliases loaded from cache." +else + [[ "$VERBOSE" -ge 1 ]] && echo "No alias cache found; no aliases were loaded." +fi diff --git a/local/bin/x-sha256sum-matcher b/local/bin/x-sha256sum-matcher index 62a47fa..cc4fa8d 100755 --- a/local/bin/x-sha256sum-matcher +++ b/local/bin/x-sha256sum-matcher @@ -1,78 +1,112 @@ #!/usr/bin/env bash +# # x-sha256sum-matcher # -# Check if two files are the same +# Compare two files by computing their SHA256 hashes. # # Ismo Vuorinen 2023 # MIT License set -euo pipefail -# ENV Variables -: "${VERBOSE:=0}" # VERBOSE=1 x-sha256sum-matcher file1 file2 +# Default settings +VERBOSE=0 -# Return sha256sum for file -# $1 - filename (string) -get_sha256sum() +# Print usage/help message +usage() { - sha256sum "$1" | head -c 64 + cat << EOF +Usage: $0 [options] file1 file2 + +Compare two files by computing their SHA256 hashes. + +Options: + -v Enable verbose output. + -h, --help Display this help message and exit. +EOF } -# Print message if VERBOSE is enabled -# $1 - message (string) -msg() +# Check if a command exists +command_exists() { - [[ "$VERBOSE" -eq 1 ]] && echo "$1" + command -v "$1" > /dev/null 2>&1 } -# Print error message and exit -# $1 - error message (string) -error() -{ - msg "(!) ERROR: $1" +# Ensure sha256sum is available +if ! command_exists sha256sum; then + echo "Error: sha256sum command not found. Please install it." >&2 exit 1 -} +fi -# Validate input arguments -validate_inputs() -{ - if [ "$#" -ne 2 ]; then - echo "Usage: $0 file1 file2" +# Process command-line options +while [[ $# -gt 0 ]]; do + case "$1" in + -h | --help) + usage + exit 0 + ;; + -v) + VERBOSE=1 + shift + ;; + -*) + echo "Error: Unknown option: $1" >&2 + usage + exit 1 + ;; + *) + break + ;; + esac +done + +# Validate input arguments: expect exactly 2 files +if [[ $# -ne 2 ]]; then + echo "Error: Two file arguments required." >&2 + usage + exit 1 +fi + +file1="$1" +file2="$2" + +# Check if files exist and are readable +for file in "$file1" "$file2"; do + if [[ ! -f "$file" ]]; then + echo "Error: File does not exist: $file" >&2 + exit 1 + elif [[ ! -r "$file" ]]; then + echo "Error: File is not readable: $file" >&2 exit 1 fi -} +done -# Check if file exists -# $1 - filename (string) -check_file_exists() +# Print verbose messages if enabled +msg() { - local filename=$1 - if [ ! -f "$filename" ]; then - error "File does not exist: $filename" + if [[ "$VERBOSE" -eq 1 ]]; then + echo "$1" fi } -# Main function -main() +# Compute SHA256 hash for a file using awk to extract the first field +get_sha256sum() { - local file_1=$1 - local file_2=$2 - - validate_inputs "$file_1" "$file_2" - check_file_exists "$file_1" - check_file_exists "$file_2" - - local file_1_hash - local file_2_hash - - file_1_hash=$(get_sha256sum "$file_1") - file_2_hash=$(get_sha256sum "$file_2") - - if [ "$file_1_hash" != "$file_2_hash" ]; then - error "Files do not match" - else - msg "(*) Success: Files do match" - fi + sha256sum "$1" | awk '{print $1}' } -main "$@" +msg "Computing SHA256 for '$file1'..." +hash1=$(get_sha256sum "$file1") +msg "SHA256 for '$file1': $hash1" + +msg "Computing SHA256 for '$file2'..." +hash2=$(get_sha256sum "$file2") +msg "SHA256 for '$file2': $hash2" + +if [[ "$hash1" != "$hash2" ]]; then + echo "Files do not match." >&2 + exit 1 +else + msg "Success: Files match." + exit 0 +fi diff --git a/local/bin/x-thumbgen b/local/bin/x-thumbgen index 677b570..edc5f2a 100755 --- a/local/bin/x-thumbgen +++ b/local/bin/x-thumbgen @@ -1,69 +1,192 @@ #!/usr/bin/env bash -# Generate thumbnails using ImageMagick (magick) +# +# Generate thumbnails using ImageMagick (magick) with MIME type filtering. # https://imagemagick.org/script/download.php # -# Defaults to current directory creating thumbnails with 1000x1000 -# dimensions and 200px white borders around the original image. +# This script recursively processes images in a given directory (and its subdirectories) +# by using the `mimetype` command to detect file types. Files with MIME types that are not +# supported by ImageMagick (as defined in the ALLOWED_MIMETYPES array) are skipped. # -# Defaults can be overridden with ENV variables like this: -# $ THMB_BACKGROUND=black x-thumbgen ~/images/ +# Defaults (can be overridden by environment variables or command-line options): +# THUMB_SOURCE: Directory with images (provided as a positional argument) +# THUMB_OUTPUT: Directory to store thumbnails (default: same as THUMB_SOURCE) +# THUMB_BACKGROUND: Background color (default: white) +# THUMB_RESIZE: Resize dimensions (default: 800x800) +# THUMB_EXTENT: Canvas dimensions (default: 1000x1000) +# THUMB_SUFFIX: Suffix appended to filename (default: _thumb) # -# Created by: Ismo Vuorinen 2015 +# Options: +# -o output_directory Specify the output directory for thumbnails (default: same as source). +# -s suffix Specify a custom suffix for thumbnail filenames (default: _thumb). +# -h, --help Display this help message and exit. +# +# Example: +# THUMB_BACKGROUND=black x-thumbgen.sh -o ~/thumbnails ~/images/ +# +# Author: Ismo Vuorinen 2015 +# Improved in 2025 set -euo pipefail -# Default values -: "${THMB_SOURCE:=${1:-}}" -: "${THMB_BACKGROUND:=white}" -: "${THMB_RESIZE:=800x800}" -: "${THMB_EXTENT:=1000x1000}" - -# Print usage information usage() { - echo "Usage: $0 /full/path/to/image/folder" + cat << EOF +Usage: $0 [options] source_directory + +Options: + -o output_directory Specify the output directory for thumbnails (default: same as source). + -s suffix Specify a custom suffix for thumbnail filenames (default: _thumb). + -h, --help Display this help message and exit. +EOF exit 1 } -# Check if ImageMagick is installed +# Default values (can be overridden by ENV variables) +THUMB_SOURCE="" +THUMB_OUTPUT="" +THUMB_BACKGROUND="${THUMB_BACKGROUND:-white}" +THUMB_RESIZE="${THUMB_RESIZE:-800x800}" +THUMB_EXTENT="${THUMB_EXTENT:-1000x1000}" +THUMB_SUFFIX="${THUMB_SUFFIX:-_thumb}" + +# List of MIME types supported by ImageMagick (adjust as needed) +ALLOWED_MIMETYPES=("image/jpeg" "image/png" "image/gif" "image/bmp" "image/tiff" "image/webp") + check_magick_installed() { if ! command -v magick &> /dev/null; then - echo "magick not found in PATH, https://imagemagick.org/script/download.php" + echo "Error: 'magick' command not found. Please install ImageMagick from https://imagemagick.org/script/download.php" >&2 exit 1 fi } -# Generate thumbnails -generate_thumbnails() +check_mimetype_installed() { - local source=$1 - - magick \ - "${source}/*" \ - -resize "$THMB_RESIZE" \ - -background "$THMB_BACKGROUND" \ - -gravity center \ - -extent "$THMB_EXTENT" \ - -set filename:fname '%t_thumb.%e' +adjoin '%[filename:fname]' + if ! command -v mimetype &> /dev/null; then + echo "Error: 'mimetype' command not found. Please install it (e.g. via 'sudo apt install libfile-mimeinfo-perl' on Debian/Ubuntu)." >&2 + exit 1 + fi } -# Main function -main() +# Helper function to check if a given MIME type is allowed +is_supported_mimetype() { - # Validate input - if [ -z "$THMB_SOURCE" ]; then + local mt=$1 + for allowed in "${ALLOWED_MIMETYPES[@]}"; do + if [[ "$mt" == "$allowed" ]]; then + return 0 + fi + done + return 1 +} + +# Parse command-line options using getopts +parse_options() +{ + while getopts ":o:s:h-:" opt; do + case $opt in + o) + THUMB_OUTPUT="$OPTARG" + ;; + s) + THUMB_SUFFIX="$OPTARG" + ;; + h) + usage + ;; + -) + if [[ "$OPTARG" == "help" ]]; then + usage + else + echo "Error: Unknown option --$OPTARG" >&2 + usage + fi + ;; + \?) + echo "Error: Invalid option -$OPTARG" >&2 + usage + ;; + :) + echo "Error: Option -$OPTARG requires an argument." >&2 + usage + ;; + esac + done + shift $((OPTIND - 1)) + + # The remaining argument should be the source directory. + if [ $# -lt 1 ]; then + echo "Error: Source directory is required." >&2 usage fi - # Check if the source directory is valid - if [ ! -d "$THMB_SOURCE" ]; then - echo "Invalid directory: $THMB_SOURCE" + THUMB_SOURCE="$1" +} + +# Generate thumbnails recursively using find and filtering by MIME type +generate_thumbnails() +{ + local source_dir=$1 + local output_dir=$2 + + # Ensure the output directory exists (create if necessary) + if [ ! -d "$output_dir" ]; then + mkdir -p "$output_dir" + fi + + # Recursively find all files. + while IFS= read -r -d '' file; do + # Use mimetype to determine the file's MIME type. + file_mimetype=$(mimetype -b "$file") + if ! is_supported_mimetype "$file_mimetype"; then + echo "Skipping unsupported MIME type '$file_mimetype' for file: $file" >&2 + continue + fi + + # Determine the relative path with respect to the source directory. + rel_path="${file#$source_dir/}" + dir="$(dirname "$rel_path")" + base="$(basename "$rel_path")" + filename="${base%.*}" + ext="${base##*.}" + + # Create corresponding output subdirectory + out_dir="${output_dir}/${dir}" + mkdir -p "$out_dir" + outfile="${out_dir}/${filename}${THUMB_SUFFIX}.${ext}" + + echo "Processing '$file' -> '$outfile'..." + magick "$file" \ + -resize "$THUMB_RESIZE" \ + -background "$THUMB_BACKGROUND" \ + -gravity center \ + -extent "$THUMB_EXTENT" \ + "$outfile" + done < <(find "$source_dir" -type f -print0) +} + +main() +{ + parse_options "$@" + + if [ -z "$THUMB_SOURCE" ]; then + echo "Error: Source directory not specified." >&2 + usage + fi + + if [ ! -d "$THUMB_SOURCE" ]; then + echo "Error: Source directory '$THUMB_SOURCE' does not exist or is not accessible." >&2 exit 1 fi + # If output directory is not specified, default to the source directory. + if [ -z "$THUMB_OUTPUT" ]; then + THUMB_OUTPUT="$THUMB_SOURCE" + fi + check_magick_installed - generate_thumbnails "$THMB_SOURCE" + check_mimetype_installed + generate_thumbnails "$THUMB_SOURCE" "$THUMB_OUTPUT" } main "$@" diff --git a/local/bin/x-until-error b/local/bin/x-until-error index d934723..87211c0 100755 --- a/local/bin/x-until-error +++ b/local/bin/x-until-error @@ -1,12 +1,92 @@ #!/bin/sh # -# About -# ----- -# Repeat the command until it fails - always run at least once. +# x-until-error: Repeatedly execute a command until it fails (non-zero exit status) +# +# Description: +# This script executes the given command repeatedly until it returns a non-zero +# exit status. It always runs the command at least once. +# +# This script is based on the original work by Steve Kemp. +# Original work Copyright (c) 2013 by Steve Kemp. +# +# The code in the original repository may be modified and distributed under your choice of: +# * The Perl Artistic License (http://dev.perl.org/licenses/artistic.html) or +# * The GNU General Public License, version 2 or later (http://www.gnu.org/licenses/gpl2.txt). +# +# Modifications and enhancements by Ismo Vuorinen on 2025. +# +# Usage: +# x-until-error [--sleep SECONDS] command [arguments...] +# +# Options: +# --sleep SECONDS Wait SECONDS (default: 1) between command executions. +# -h, --help Display this help message. +# +# Example: +# x-until-error --sleep 2 ls -l -"$@" +# Default sleep interval between executions. +SLEEP_INTERVAL=1 -# If the status code was zero then repeat. -while [ $? -eq 0 ]; do - "$@" +# Function to display usage information. +usage() +{ + cat << EOF +Usage: $0 [--sleep SECONDS] command [arguments...] + +Repeats the given command until it fails (returns a non-zero exit status). + +Options: + --sleep SECONDS Wait SECONDS (default: 1) between command executions. + -h, --help Display this help message. + +Example: + $0 --sleep 2 ls -l +EOF + exit 1 +} + +# Parse command-line options. +while [ $# -gt 0 ]; do + case "$1" in + --sleep) + shift + if [ $# -eq 0 ]; then + echo "Error: --sleep requires a numeric argument." >&2 + exit 1 + fi + SLEEP_INTERVAL="$1" + shift + ;; + -h | --help) + usage + ;; + --) # End of options marker. + shift + break + ;; + -*) + echo "Error: Unknown option: $1" >&2 + usage + ;; + *) + break + ;; + esac +done + +# Ensure a command is provided. +if [ $# -eq 0 ]; then + echo "Error: No command specified." >&2 + usage +fi + +# Execute the command repeatedly until it fails. +while true; do + "$@" + status=$? + if [ $status -ne 0 ]; then + exit $status + fi + sleep "$SLEEP_INTERVAL" done diff --git a/local/bin/x-until-success b/local/bin/x-until-success index 00ac45c..067d826 100755 --- a/local/bin/x-until-success +++ b/local/bin/x-until-success @@ -1,24 +1,92 @@ #!/bin/sh # -# About -# ----- -# Repeat the command until it succeeds - always run at least once. +# x-until-success: Repeat the command until it succeeds - always run at least once. # +# This script is based on the original work by Steve Kemp. +# Original work Copyright (c) 2013 by Steve Kemp. # -# License -# ------- +# The code in the original repository may be modified and distributed under your choice of: +# * The Perl Artistic License (http://dev.perl.org/licenses/artistic.html) or +# * The GNU General Public License, version 2 or later (http://www.gnu.org/licenses/gpl2.txt). # -# Copyright (c) 2013 by Steve Kemp. All rights reserved. +# Modifications and enhancements by Ismo Vuorinen on 2025. # -# This script is free software; you can redistribute it and/or modify it under -# the same terms as Perl itself. +# Usage: +# x-until-success [--sleep SECONDS] command [arguments...] # -# The LICENSE file contains the full text of the license. +# Options: +# --sleep SECONDS Wait SECONDS (default: 1) between command executions. +# -h, --help Display this help message. +# +# Example: +# x-until-success --sleep 2 ls -l -# Run the first time. -"$@" +# Default sleep interval between command executions. +SLEEP_INTERVAL=1 -# If the status code was not zero then repeat. -while [ $? -ne 0 ]; do - "$@" +# Display usage information. +usage() +{ + cat << EOF +Usage: $0 [--sleep SECONDS] command [arguments...] + +Repeats the given command until it succeeds (returns a zero exit status). +The command is always executed at least once. + +Options: + --sleep SECONDS Wait SECONDS (default: 1) between command executions. + -h, --help Display this help message. + +Example: + $0 --sleep 2 ping -c 1 google.com +EOF + exit 1 +} + +# Parse command-line options. +while [ $# -gt 0 ]; do + case "$1" in + --sleep) + shift + if [ $# -eq 0 ]; then + echo "Error: --sleep requires a numeric argument." >&2 + usage + fi + SLEEP_INTERVAL="$1" + shift + ;; + -h | --help) + usage + ;; + --) + shift + break + ;; + -*) + echo "Error: Unknown option: $1" >&2 + usage + ;; + *) + break + ;; + esac done + +# Ensure that a command is provided. +if [ $# -eq 0 ]; then + echo "Error: No command specified." >&2 + usage +fi + +# Execute the command at least once. +"$@" +status=$? + +# If the command did not succeed, repeat until it does. +while [ $status -ne 0 ]; do + sleep "$SLEEP_INTERVAL" + "$@" + status=$? +done + +exit $status diff --git a/local/bin/x-welcome-banner b/local/bin/x-welcome-banner index ae032f4..43d36ba 100755 --- a/local/bin/x-welcome-banner +++ b/local/bin/x-welcome-banner @@ -15,7 +15,7 @@ COLOR_P='\033[1;36m' COLOR_S='\033[0;36m' RESET='\033[0m' -# Print time-based personalized message, using figlet & lolcat if availible +# Print time-based personalized message, using figlet & lolcat if available function welcome_greeting() { h=$(date +%H) @@ -51,7 +51,7 @@ function welcome_sysinfo() fi } -# Print todays info: Date, IP, weather, etc +# Print today's info: Date, IP, weather, etc function welcome_today() { timeout=1 diff --git a/local/bin/x-when-down b/local/bin/x-when-down index e9ede42..6eb2dfd 100755 --- a/local/bin/x-when-down +++ b/local/bin/x-when-down @@ -1,8 +1,17 @@ -#!/bin/sh +#!/usr/bin/env bash # # Wait until a given host is down (determined by ping) then execute the # given command # +# This script is based on the original work by Steve Kemp. +# Original work Copyright (c) 2013 by Steve Kemp. +# +# The code in the original repository may be modified and distributed under your choice of: +# * The Perl Artistic License (http://dev.perl.org/licenses/artistic.html) or +# * The GNU General Public License, version 2 or later (http://www.gnu.org/licenses/gpl2.txt). +# +# Modifications and enhancements by Ismo Vuorinen on 2025. +# # Usage: # ./when-down HOST COMMAND... # diff --git a/local/bin/x-when-up b/local/bin/x-when-up index e0abfdb..5bdb562 100755 --- a/local/bin/x-when-up +++ b/local/bin/x-when-up @@ -1,8 +1,17 @@ -#!/bin/sh +#!/usr/bin/env bash # # Wait until a given host is online (determined by ping) then execute the # given command # +# This script is based on the original work by Steve Kemp. +# Original work Copyright (c) 2013 by Steve Kemp. +# +# The code in the original repository may be modified and distributed under your choice of: +# * The Perl Artistic License (http://dev.perl.org/licenses/artistic.html) or +# * The GNU General Public License, version 2 or later (http://www.gnu.org/licenses/gpl2.txt). +# +# Modifications and enhancements by Ismo Vuorinen on 2025. +# # Usage: # ./when-up HOST COMMAND... # diff --git a/scripts/create-aerospace-keymaps.php b/scripts/create-aerospace-keymaps.php index ee7591f..cec75d6 100755 --- a/scripts/create-aerospace-keymaps.php +++ b/scripts/create-aerospace-keymaps.php @@ -9,13 +9,13 @@ $dotfiles_env = getenv("DOTFILES") ?? '~/.dotfiles'; $dest = "$dotfiles_env/docs/aerospace-keybindings.md"; exec("aerospace config --get mode --json", $output); -$output = join(' ', $output); +$output = implode(' ', $output); $config = json_decode($output, true); $main = $config['main']; unset($config['main']); -function process_section(string $title, array $array) +function process_section(string $title, array $array): string { $bindings = $array['binding'] ?? []; ksort($bindings);