#!/usr/bin/env bash # Shared Path Utilities # Sprint: CI/CD Enhancement - Script Consolidation # # Purpose: Path manipulation and file operations for CI/CD scripts # Usage: source "$(dirname "${BASH_SOURCE[0]}")/lib/path-utils.sh" # Prevent multiple sourcing if [[ -n "${__STELLAOPS_PATH_UTILS_LOADED:-}" ]]; then return 0 fi export __STELLAOPS_PATH_UTILS_LOADED=1 # Source dependencies SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/logging.sh" 2>/dev/null || true source "${SCRIPT_DIR}/exit-codes.sh" 2>/dev/null || true # ============================================================================ # Path Normalization # ============================================================================ # Normalize path (resolve .., ., symlinks) normalize_path() { local path="$1" # Handle empty path if [[ -z "$path" ]]; then echo "." return 0 fi # Try realpath first (most reliable) if command -v realpath >/dev/null 2>&1; then realpath -m "$path" 2>/dev/null && return 0 fi # Fallback to Python if command -v python3 >/dev/null 2>&1; then python3 -c "import os; print(os.path.normpath('$path'))" 2>/dev/null && return 0 fi # Manual normalization (basic) echo "$path" | sed 's|/\./|/|g' | sed 's|/[^/]*/\.\./|/|g' | sed 's|//|/|g' } # Get absolute path absolute_path() { local path="$1" if [[ "$path" == /* ]]; then normalize_path "$path" else normalize_path "$(pwd)/$path" fi } # Get relative path from one path to another relative_path() { local from="$1" local to="$2" if command -v realpath >/dev/null 2>&1; then realpath --relative-to="$from" "$to" 2>/dev/null && return 0 fi if command -v python3 >/dev/null 2>&1; then python3 -c "import os.path; print(os.path.relpath('$to', '$from'))" 2>/dev/null && return 0 fi # Fallback: just return absolute path absolute_path "$to" } # ============================================================================ # Path Components # ============================================================================ # Get directory name dir_name() { dirname "$1" } # Get base name base_name() { basename "$1" } # Get file extension file_extension() { local path="$1" local base base=$(basename "$path") if [[ "$base" == *.* ]]; then echo "${base##*.}" else echo "" fi } # Get file name without extension file_stem() { local path="$1" local base base=$(basename "$path") if [[ "$base" == *.* ]]; then echo "${base%.*}" else echo "$base" fi } # ============================================================================ # Directory Operations # ============================================================================ # Ensure directory exists ensure_directory() { local dir="$1" if [[ ! -d "$dir" ]]; then mkdir -p "$dir" fi } # Create temporary directory create_temp_dir() { local prefix="${1:-stellaops}" mktemp -d "${TMPDIR:-/tmp}/${prefix}.XXXXXX" } # Create temporary file create_temp_file() { local prefix="${1:-stellaops}" local suffix="${2:-}" mktemp "${TMPDIR:-/tmp}/${prefix}.XXXXXX${suffix}" } # Clean temporary directory clean_temp() { local path="$1" if [[ -d "$path" ]] && [[ "$path" == *stellaops* ]]; then rm -rf "$path" fi } # ============================================================================ # File Existence Checks # ============================================================================ # Check if file exists file_exists() { [[ -f "$1" ]] } # Check if directory exists dir_exists() { [[ -d "$1" ]] } # Check if path exists (file or directory) path_exists() { [[ -e "$1" ]] } # Check if file is readable file_readable() { [[ -r "$1" ]] } # Check if file is writable file_writable() { [[ -w "$1" ]] } # Check if file is executable file_executable() { [[ -x "$1" ]] } # ============================================================================ # File Discovery # ============================================================================ # Find files by pattern find_files() { local dir="${1:-.}" local pattern="${2:-*}" find "$dir" -type f -name "$pattern" 2>/dev/null } # Find files by extension find_by_extension() { local dir="${1:-.}" local ext="${2:-}" find "$dir" -type f -name "*.${ext}" 2>/dev/null } # Find project files (csproj, package.json, etc.) find_project_files() { local dir="${1:-.}" find "$dir" -type f \( \ -name "*.csproj" -o \ -name "*.fsproj" -o \ -name "package.json" -o \ -name "Cargo.toml" -o \ -name "go.mod" -o \ -name "pom.xml" -o \ -name "build.gradle" \ \) 2>/dev/null | grep -v node_modules | grep -v bin | grep -v obj } # Find test projects find_test_projects() { local dir="${1:-.}" find "$dir" -type f -name "*.Tests.csproj" 2>/dev/null | grep -v bin | grep -v obj } # ============================================================================ # Path Validation # ============================================================================ # Check if path is under directory path_under() { local path="$1" local dir="$2" local abs_path abs_dir abs_path=$(absolute_path "$path") abs_dir=$(absolute_path "$dir") [[ "$abs_path" == "$abs_dir"* ]] } # Validate path is safe (no directory traversal) path_is_safe() { local path="$1" local base="${2:-.}" # Check for obvious traversal attempts if [[ "$path" == *".."* ]] || [[ "$path" == "/*" ]]; then return 1 fi # Verify resolved path is under base path_under "$path" "$base" } # ============================================================================ # CI/CD Helpers # ============================================================================ # Get artifact output directory get_artifact_dir() { local name="${1:-artifacts}" local base="${GITHUB_WORKSPACE:-$(pwd)}" echo "${base}/out/${name}" } # Get test results directory get_test_results_dir() { local base="${GITHUB_WORKSPACE:-$(pwd)}" echo "${base}/TestResults" } # Ensure artifact directory exists and return path ensure_artifact_dir() { local name="${1:-artifacts}" local dir dir=$(get_artifact_dir "$name") ensure_directory "$dir" echo "$dir" }