Skip to main content

Filesystem Functions

The fs namespace provides comprehensive file and directory operations. These functions handle file I/O, path manipulation, and filesystem queries with proper error handling.

File Operations

fs.exists()

Check if a file or directory exists:

if (fs.exists("config.json")) {
console.log("Configuration file found");
} else {
console.log("Configuration file missing");
}

if (fs.exists("/etc/passwd")) {
console.log("System password file exists");
}

Generated Bash:

if [ -e "config.json" ]; then
echo "Configuration file found"
else
echo "Configuration file missing"
fi

if [ -e "/etc/passwd" ]; then
echo "System password file exists"
fi

Test Coverage:

  • File: tests/positive_fixtures/fs_exists.shx
  • Tests file existence checking with -e test

fs.readFile()

Read the contents of a file:

let config: string = fs.readFile("app.json");
console.log("Config content: ${config}");

try {
let data: string = fs.readFile("important.txt");
console.log("File loaded successfully");
}
catch {
console.log("Failed to read file");
}

Generated Bash:

config=$(cat "app.json")
echo "Config content: ${config}"

if data=$(cat "important.txt" 2>/dev/null); then
echo "File loaded successfully"
else
echo "Failed to read file"
fi

fs.writeFile()

Write content to a file (overwrites existing content):

let content: string = "Hello, World!";
fs.writeFile("output.txt", content);

let jsonData: string = "{\"name\": \"test\", \"value\": 123}";
fs.writeFile("data.json", jsonData);

Generated Bash:

content="Hello, World!"
echo "${content}" > "output.txt"

jsonData='{"name": "test", "value": 123}'
echo "${jsonData}" > "data.json"

fs.copy()

Copy a file or directory from source to target path, automatically creating directories if needed. Uses recursive copying for directories:

// Simple file copy
fs.copy("config.json", "backup/config.json");

// Copy with variables
let sourceFile: string = "important.doc";
let targetDir: string = "archive";
fs.copy(sourceFile, targetDir + "/" + sourceFile);

// Copy and check success
let success: boolean = fs.copy("data.csv", "reports/data.csv");
if (success) {
console.log("File copied successfully");
} else {
console.log("File copy failed");
}

// Copy entire directories
fs.copy("source_dir", "backup/source_dir");

Generated Bash:

# Statement usage
mkdir -p $(dirname "backup/config.json")
cp "config.json" "backup/config.json"

# With variables
sourceFile="important.doc"
targetDir="archive"
mkdir -p $(dirname "${targetDir}/${sourceFile}")
cp "${sourceFile}" "${targetDir}/${sourceFile}"

# Expression usage with return value
success=$(mkdir -p $(dirname "reports/data.csv") && cp "data.csv" "reports/data.csv" && echo "true" || echo "false")
if [ "${success}" = "true" ]; then
echo "File copied successfully"
else
echo "File copy failed"
fi

Test Coverage:

  • File: tests/positive_fixtures/fs_copy.shx
  • Tests file and directory copying with directory creation and boolean return values

fs.move()

Move or rename a file or directory from source to target path, automatically creating directories if needed. This function uses atomic move operations when possible:

// Simple file move/rename
fs.move("temp.txt", "final.txt");

// Move to different directory
fs.move("draft.md", "archive/document.md");

// Move with variables
let oldFile: string = "report_draft.pdf";
let newPath: string = "reports/final/report.pdf";
fs.move(oldFile, newPath);

// Move and check success
let success: boolean = fs.move("data.csv", "processed/data_final.csv");
if (success) {
console.log("File moved successfully");
} else {
console.log("File move failed");
}

// Conditional move based on file existence
if (fs.exists("temp_file.txt")) {
let moved: boolean = fs.move("temp_file.txt", "archive/temp_file.txt");
if (!moved) {
console.log("Warning: File move failed");
}
}

// Move entire directories
fs.move("old_project", "archive/old_project");
}

Generated Bash:

# Statement usage
mkdir -p $(dirname "final.txt")
mv "temp.txt" "final.txt"

# Move to different directory
mkdir -p $(dirname "archive/document.md")
mv "draft.md" "archive/document.md"

# With variables
oldFile="report_draft.pdf"
newPath="reports/final/report.pdf"
mkdir -p $(dirname "${newPath}")
mv "${oldFile}" "${newPath}"

# Expression usage with return value
success=$(mkdir -p $(dirname "processed/data_final.csv") && mv "data.csv" "processed/data_final.csv" && echo "true" || echo "false")
if [ "${success}" = "true" ]; then
echo "File moved successfully"
else
echo "File move failed"
fi

# Conditional move
if [ -e "temp_file.txt" ]; then
moved=$(mkdir -p $(dirname "archive/temp_file.txt") && mv "temp_file.txt" "archive/temp_file.txt" && echo "true" || echo "false")
if [ "${moved}" != "true" ]; then
echo "Warning: File move failed"
fi
fi

Key Features:

  • Atomic Operations: Uses mv command which is atomic within the same filesystem
  • Directory Creation: Automatically creates target directories using mkdir -p
  • Return Values: Returns boolean indicating success/failure when used as expression
  • Cross-filesystem: Works across different filesystems (though may not be atomic)
  • Rename Support: Can be used for simple file renaming in the same directory

Test Coverage:

  • File: tests/positive_fixtures/fs_move.shx
  • Tests file and directory moving, renaming, directory creation, and boolean return values

fs.rename()

Rename a file or directory within the same location. This function is simpler than fs.move() as it focuses on renaming operations without automatic directory creation:

// Simple file rename
fs.rename("old-file.txt", "new-file.txt");

// Directory rename
fs.rename("old-folder", "new-folder");

// Rename with variables
let oldFileName: string = "report_draft.pdf";
let newFileName: string = "report_final.pdf";
fs.rename(oldFileName, newFileName);

// Rename and check success
let success: boolean = fs.rename("temp.log", "archive.log");
if (success) {
console.log("File renamed successfully");
} else {
console.log("File rename failed");
}

// Conditional rename
if (fs.exists("temporary_file.txt")) {
let renamed: boolean = fs.rename("temporary_file.txt", "permanent_file.txt");
if (renamed) {
console.log("Temporary file renamed to permanent");
}
}

// Rename with string operations
let prefix: string = "processed_";
let originalName: string = "document.pdf";
fs.rename(originalName, prefix + originalName);

Generated Bash:

# Statement usage
mv "old-file.txt" "new-file.txt"

# Directory rename
mv "old-folder" "new-folder"

# With variables
oldFileName="report_draft.pdf"
newFileName="report_final.pdf"
mv "${oldFileName}" "${newFileName}"

# Expression usage with return value
success=$(mv "temp.log" "archive.log" && echo "true" || echo "false")
if [ "${success}" = "true" ]; then
echo "File renamed successfully"
else
echo "File rename failed"
fi

# Conditional rename
if [ -e "temporary_file.txt" ]; then
renamed=$(mv "temporary_file.txt" "permanent_file.txt" && echo "true" || echo "false")
if [ "${renamed}" = "true" ]; then
echo "Temporary file renamed to permanent"
fi
fi

# With string concatenation
prefix="processed_"
originalName="document.pdf"
mv "${originalName}" "${prefix}${originalName}"

Key Features:

  • Simple Renaming: Focused on renaming files and directories in place
  • No Directory Creation: Unlike fs.move(), does not create target directories
  • Atomic Operations: Uses mv command which is atomic within the same filesystem
  • Return Values: Returns boolean indicating success/failure when used as expression
  • Same-Directory Focus: Optimized for renaming within the same location
  • Directory Support: Works with both files and directories

Comparison with fs.move():

Aspectfs.move()fs.rename()
PurposeMove files/directories between locationsRename files/directories
Directory CreationYes (mkdir -p)No
Typical UseCross-directory movesSame-directory renames
Parameter NamessourcePath, targetPatholdName, newName
ComplexityHigher (directory handling)Lower (direct rename)

Test Coverage:

  • File: tests/positive_fixtures/fs_rename.shx
  • Tests file renaming, directory renaming, return values, and conditional logic

fs.delete()

Delete a file or directory recursively. This function uses rm -rf to remove files and directories, making it suitable for both files and entire directory trees:

// Simple file deletion
fs.delete("temp.txt");

// Delete directory and all contents
fs.delete("old-folder");

// Delete with variables
let tempFile: string = "processing.tmp";
fs.delete(tempFile);

// Delete and check success
let success: boolean = fs.delete("config.json");
if (success) {
console.log("File deleted successfully");
} else {
console.log("File delete failed");
}

// Conditional deletion based on file existence
if (fs.exists("temp_data.csv")) {
let deleted: boolean = fs.delete("temp_data.csv");
if (!deleted) {
console.log("Warning: File deletion failed");
}
}

// Delete with dynamic paths
let timestamp: string = "2024-01-15";
fs.delete("backups/backup-" + timestamp + ".tar.gz");

// Delete entire directory tree
fs.delete("cache/user-sessions");

Generated Bash:

# Statement usage
rm -rf "temp.txt"

# Delete directory
rm -rf "old-folder"

# With variables
tempFile="processing.tmp"
rm -rf "${tempFile}"

# Expression usage with return value
success=$(rm -rf "config.json" && echo "true" || echo "false")
if [ "${success}" = "true" ]; then
echo "File deleted successfully"
else
echo "File delete failed"
fi

Key Features:

  • Recursive Deletion: Uses rm -rf to delete files and directories recursively
  • Return Values: Returns boolean indicating success/failure when used as expression
  • Safety: No confirmation prompts - deletion is immediate and permanent
  • Cross-platform: Works consistently across different Unix-like systems
  • Directory Support: Can delete both individual files and entire directory trees

Test Coverage:

  • File: tests/positive_fixtures/fs_delete.shx
  • Tests file deletion, directory deletion, return values, and dynamic path expressions

fs.chmod()

Change file permissions using numeric (octal) or symbolic notation. Returns a boolean indicating success or failure:

// Numeric permissions
fs.chmod("script.sh", "755"); // rwxr-xr-x (executable script)
fs.chmod("config.txt", "600"); // rw------- (owner read/write only)
fs.chmod("public.txt", "644"); // rw-r--r-- (owner read/write, others read)
fs.chmod("readonly.txt", "444"); // r--r--r-- (read-only for all)

// Symbolic permissions
fs.chmod("backup.sh", "u+x"); // Add execute permission for user
fs.chmod("data.log", "g+w"); // Add write permission for group
fs.chmod("secrets.txt", "o-r"); // Remove read permission for others
fs.chmod("public.txt", "a+r"); // Add read permission for all
fs.chmod("private.log", "go-rwx"); // Remove all permissions for group/others

// Expression usage - check if permission change was successful
let success: boolean = fs.chmod("important.sh", "700");
if (success) {
console.log("File permissions updated successfully");
} else {
console.log("Failed to update file permissions");
}

// Secure configuration files
if (fs.exists("database.conf")) {
fs.chmod("database.conf", "600"); // Owner read/write only
console.log("Database configuration secured");
}

// Make scripts executable
let scriptFiles: string[] = ["deploy.sh", "backup.sh", "cleanup.sh"];
for (let script in scriptFiles) {
let result: boolean = fs.chmod(script, "755");
console.log(`Made ${script} executable: ${result}`);
}

Generated Bash:

# Numeric permissions
$(chmod "755" "script.sh" && echo "true" || echo "false")
$(chmod "600" "config.txt" && echo "true" || echo "false")
$(chmod "644" "public.txt" && echo "true" || echo "false")
$(chmod "444" "readonly.txt" && echo "true" || echo "false")

# Symbolic permissions
$(chmod "u+x" "backup.sh" && echo "true" || echo "false")
$(chmod "g+w" "data.log" && echo "true" || echo "false")
$(chmod "o-r" "secrets.txt" && echo "true" || echo "false")
$(chmod "a+r" "public.txt" && echo "true" || echo "false")
$(chmod "go-rwx" "private.log" && echo "true" || echo "false")

# Expression usage with conditional logic
success=$(chmod "700" "important.sh" && echo "true" || echo "false")
if [ "${success}" = "true" ]; then
echo "File permissions updated successfully"
else
echo "Failed to update file permissions"
fi

# Secure configuration files
if [ -e "database.conf" ]; then
$(chmod "600" "database.conf" && echo "true" || echo "false")
echo "Database configuration secured"
fi

# Make scripts executable in loop
scriptFiles=("deploy.sh" "backup.sh" "cleanup.sh")
for script in "${scriptFiles[@]}"; do
result=$(chmod "755" "${script}" && echo "true" || echo "false")
echo "Made ${script} executable: ${result}"
done

Permission Reference:

Numeric Permissions (Octal):

  • 755 = rwxr-xr-x - Owner: read/write/execute, Group/Others: read/execute
  • 644 = rw-r--r-- - Owner: read/write, Group/Others: read only
  • 600 = rw------- - Owner: read/write, Group/Others: no access
  • 400 = r-------- - Owner: read only, Group/Others: no access
  • 444 = r--r--r-- - All: read only

Symbolic Permissions:

  • u+x - Add execute permission for user (owner)
  • g+w - Add write permission for group
  • o-r - Remove read permission for others
  • a+r - Add read permission for all (user, group, others)
  • go-rwx - Remove all permissions for group and others

Key Features:

  • Multiple Formats: Supports both numeric (755) and symbolic (u+x) permission notation
  • Return Values: Returns boolean indicating success/failure when used as expression
  • Comprehensive: Handles all standard Unix permission patterns
  • Cross-platform: Works on all Unix-like systems (Linux, macOS, BSD)
  • Security: Essential for securing configuration files, scripts, and sensitive data
  • Error Handling: Gracefully handles permission change failures

Use Cases:

  • Script Security: Make shell scripts executable while restricting access
  • Configuration Files: Secure config files with appropriate read/write permissions
  • Log Files: Set proper permissions for log file access and rotation
  • Backup Scripts: Ensure backup scripts are executable but secure
  • Database Security: Restrict access to database configuration and data files
  • DevOps Automation: Set permissions as part of deployment and configuration scripts

Best Practices:

  • Principle of Least Privilege: Grant only the minimum permissions necessary
  • Secure Defaults: Use 600 for configuration files, 644 for data files, 755 for executables
  • Check Results: Always check the return value when permission changes are critical
  • Script Security: Make scripts executable (755) but not world-writable
  • Sensitive Files: Use 400 or 600 for files containing passwords or keys

Test Coverage:

  • File: tests/positive_fixtures/fs_chmod.shx
  • Tests numeric and symbolic permissions, expression usage, conditionals, and batch operations

fs.chown()

Change file and directory ownership. Supports user-only ownership or user and group:

// Change owner only
fs.chown("/path/to/file", "newowner");

// Change owner and group
fs.chown("/path/to/file", "newowner", "newgroup");

// Using numeric IDs
fs.chown("/var/log/app.log", "1000", "1000");

// With variables
let user = "webserver";
let group = "www-data";
fs.chown("/var/www/html", user, group);

// In conditionals
if (fs.chown("/etc/app.conf", "app", "app")) {
console.log("Ownership changed successfully");
} else {
console.log("Failed to change ownership");
}

Generated Bash:

# Owner only
if chown newowner "/path/to/file" >/dev/null 2>&1; then echo "true"; else echo "false"; fi

# Owner and group
if chown newowner:newgroup "/path/to/file" >/dev/null 2>&1; then echo "true"; else echo "false"; fi

# With numeric IDs
if chown 1000:1000 "/var/log/app.log" >/dev/null 2>&1; then echo "true"; else echo "false"; fi

Common Use Cases:

  • Web Server Setup: Change ownership of web files to web server user
  • Log File Management: Set appropriate ownership for application logs
  • Security Hardening: Restrict file access to specific users/groups
  • Service Configuration: Set ownership for service-specific files

Best Practices:

  • Verify Users/Groups: Ensure target users and groups exist before changing ownership
  • Recursive Operations: Use with fs.find() for recursive ownership changes
  • Security: Be cautious when changing ownership of system files
  • Backup: Consider backing up important files before ownership changes

Test Coverage:

  • File: tests/positive_fixtures/fs_chown.shx
  • Tests owner-only, owner+group, numeric IDs, variables, and conditional usage

fs.find()

Search for files and directories recursively with optional wildcard pattern matching. Returns an array of matching paths:

// Find all files and directories
let allItems: string[] = fs.find(".");
console.log("Found ${array.length(allItems)} items");

// Find files by pattern
let markdownFiles: string[] = fs.find(".", "*.md");
let sourceFiles: string[] = fs.find("src", "*.shx");

// Find in specific directory
let testFiles: string[] = fs.find("tests", "*.shx");
let configFiles: string[] = fs.find(".", "*.json");

// Find with variables
let searchDir: string = "docs";
let pattern: string = "*.md";
let docFiles: string[] = fs.find(searchDir, pattern);

// Process results
for (let file: string in markdownFiles) {
if (file.trim() != "") {
console.log("Processing: ${file}");
let content: string = fs.readFile(file);
// Process file content...
}
}

// Use with other filesystem functions
let logFiles: string[] = fs.find("/var/log", "*.log");
for (let logFile: string in logFiles) {
if (fs.exists(logFile)) {
console.log("Log file: ${logFile}");
}
}

Generated Bash:

# Find all items
allItems=$(IFS=$'\n'; mapfile -t _utah_find_results < <(find "." 2>/dev/null); printf '%s\n' "${_utah_find_results[@]}")
echo "Found ${#allItems[@]} items"

# Find by pattern
markdownFiles=$(IFS=$'\n'; mapfile -t _utah_find_results < <(find "." -name "*.md" 2>/dev/null); printf '%s\n' "${_utah_find_results[@]}")
sourceFiles=$(IFS=$'\n'; mapfile -t _utah_find_results < <(find "src" -name "*.shx" 2>/dev/null); printf '%s\n' "${_utah_find_results[@]}")

# Find in specific directory
testFiles=$(IFS=$'\n'; mapfile -t _utah_find_results < <(find "tests" -name "*.shx" 2>/dev/null); printf '%s\n' "${_utah_find_results[@]}")
configFiles=$(IFS=$'\n'; mapfile -t _utah_find_results < <(find "." -name "*.json" 2>/dev/null); printf '%s\n' "${_utah_find_results[@]}")

# Process results
for file in "${markdownFiles[@]}"; do
if [ -n "$(echo "$file" | tr -d '[:space:]')" ]; then
echo "Processing: $file"
content=$(cat "$file")
# Process file content...
fi
done

Key Features:

  • Recursive Search: Searches all subdirectories by default
  • Wildcard Support: Supports shell wildcard patterns (*, ?, [abc], etc.)
  • Array Return: Returns string[] for easy iteration and processing
  • Error Resilience: Uses 2>/dev/null to handle permission errors gracefully
  • Empty Filtering: Automatically handles empty results
  • Variable Support: Both parameters accept variables and expressions

Wildcard Patterns:

  • *.txt - All files ending with .txt
  • file?.log - Files like file1.log, fileA.log, etc.
  • test[1-9].txt - Files like test1.txt through test9.txt
  • *.{js,ts} - Files ending with .js or .ts

Test Coverage:

  • File: tests/positive_fixtures/fs_find.shx
  • Tests basic finding, pattern matching, variable usage, and empty results

fs.watch()

Monitor files or directories for changes and execute a callback when events occur. Returns a process ID string for managing the background watcher:

// Basic file monitoring
let watchPid: string = fs.watch("/var/log/app.log", "echo 'Log file changed: $1, Event: $2'");
console.log("Watching file with PID: ${watchPid}");

// Directory monitoring with function callback
function handleFileChange(filePath: string, eventType: string): void {
console.log("File changed: ${filePath}");
console.log("Event type: ${eventType}");

if (eventType == "modify") {
console.log("File was modified");
} else if (eventType == "create") {
console.log("File was created");
} else if (eventType == "delete") {
console.log("File was deleted");
} else if (eventType == "move") {
console.log("File was moved or renamed");
}
}

let dirWatchPid: string = fs.watch("/tmp", "handleFileChange");

// Configuration file monitoring
function reloadConfig(filePath: string, eventType: string): void {
if (eventType == "modify" && fs.exists(filePath)) {
console.log("Configuration updated, reloading...");
// Reload application configuration here
}
}

let configWatchPid: string = fs.watch("/etc/myapp/config.yaml", "reloadConfig");

// Variable-based watching
let watchPath: string = "/home/user/documents";
let callbackCommand: string = "echo 'Document changed: $1 ($2)'";

let docWatchPid: string = fs.watch(watchPath, callbackCommand);

// Watch management
if (process.isRunning(watchPid)) {
console.log("Watcher is active");
} else {
console.log("Watcher has stopped");
}

// Stop watching
process.kill(watchPid);

Generated Bash:

# Basic file monitoring
watchPid=$(_utah_watch_pid_1=$(inotifywait -m -e modify,create,delete,move "/var/log/app.log" --format '%w%f %e' | while read file event; do
echo 'Log file changed: $1, Event: $2' "${file}" "${event}"
done & echo $!)
echo "${_utah_watch_pid_1}")
echo "Watching file with PID: ${watchPid}"

# Function-based callback
handleFileChange() {
local filePath="$1"
local eventType="$2"
echo "File changed: ${filePath}"
echo "Event type: ${eventType}"

if [ "${eventType}" = "modify" ]; then
echo "File was modified"
elif [ "${eventType}" = "create" ]; then
echo "File was created"
elif [ "${eventType}" = "delete" ]; then
echo "File was deleted"
elif [ "${eventType}" = "move" ]; then
echo "File was moved or renamed"
fi
}

dirWatchPid=$(_utah_watch_pid_2=$(inotifywait -m -e modify,create,delete,move "/tmp" --format '%w%f %e' | while read file event; do
handleFileChange "${file}" "${event}"
done & echo $!)
echo "${_utah_watch_pid_2}")

# Process management
isRunning=$(ps -p ${watchPid} -o pid= > /dev/null 2>&1 && echo "true" || echo "false")
if [ "${isRunning}" = "true" ]; then
echo "Watcher is active"
else
echo "Watcher has stopped"
fi

# Stop watching
kill ${watchPid} 2>/dev/null || true

Key Features:

  • Real-time Monitoring: Uses inotifywait for efficient filesystem event monitoring
  • Event Types: Supports modify, create, delete, and move events
  • Callback Flexibility: Accepts both simple commands and function names as callbacks
  • Background Process: Runs in background and returns process ID for management
  • Cross-platform: Works on Linux systems with inotify-tools installed
  • Integration: Works seamlessly with Utah's process management functions

Event Types:

  • modify - File or directory content was changed
  • create - File or directory was created
  • delete - File or directory was deleted
  • move - File or directory was moved or renamed

Dependencies:

  • Requires inotify-tools package (sudo apt install inotify-tools on Ubuntu/Debian)
  • Uses inotifywait command for efficient filesystem monitoring

Use Cases:

  • Configuration file monitoring for auto-reload functionality
  • Log file monitoring for real-time analysis
  • Development tools that need to detect source code changes
  • Backup systems that trigger on file modifications
  • Security monitoring for sensitive directories

Test Coverage:

  • File: tests/positive_fixtures/fs_watch.shx
  • Tests basic monitoring, function callbacks, variable usage, and process management

fs.appendFile()

Append content to a file:

let logEntry: string = "[${timer.current()}] Application started";
fs.appendFile("app.log", logEntry + "\n");

fs.appendFile("notes.txt", "Additional note\n");

Generated Bash:

logEntry="[$(date '+%Y-%m-%d %H:%M:%S')] Application started"
echo "${logEntry}" >> "app.log"

echo "Additional note" >> "notes.txt"

Path Manipulation

fs.filename()

Extract the filename from a path:

let fullPath: string = "/home/user/documents/report.pdf";
let filename: string = fs.filename(fullPath);
console.log("Filename: ${filename}"); // Output: report.pdf

Generated Bash:

fullPath="/home/user/documents/report.pdf"
filename=$(basename "${fullPath}")
echo "Filename: ${filename}"

Test Coverage:

  • File: tests/positive_fixtures/fs_filename.shx
  • Tests filename extraction using basename

fs.dirname()

Extract the directory path from a full path:

let fullPath: string = "/home/user/documents/report.pdf";
let directory: string = fs.dirname(fullPath);
console.log("Directory: ${directory}"); // Output: /home/user/documents

Generated Bash:

fullPath="/home/user/documents/report.pdf"
directory=$(dirname "${fullPath}")
echo "Directory: ${directory}"

Test Coverage:

  • File: tests/positive_fixtures/fs_dirname.shx
  • Tests directory extraction using dirname

fs.extension()

Get the file extension:

let filename: string = "document.pdf";
let ext: string = fs.extension(filename);
console.log("Extension: ${ext}"); // Output: pdf

let noExt: string = fs.extension("README");
console.log("Extension: ${noExt}"); // Output: (empty)

Generated Bash:

filename="document.pdf"
ext="${filename##*.}"
if [ "${ext}" = "${filename}" ]; then
ext=""
fi
echo "Extension: ${ext}"

noExt="${filename##*.}"
if [ "${noExt}" = "${filename}" ]; then
noExt=""
fi
echo "Extension: ${noExt}"

Test Coverage:

  • File: tests/positive_fixtures/fs_extension.shx
  • Tests file extension extraction with fallback for files without extensions

fs.parentDirName()

Get the name of the parent directory:

let path: string = "/home/user/projects/myapp/src";
let parentName: string = fs.parentDirName(path);
console.log("Parent directory: ${parentName}"); // Output: myapp

Generated Bash:

path="/home/user/projects/myapp/src"
parentName=$(basename "$(dirname "${path}")")
echo "Parent directory: ${parentName}"

Test Coverage:

  • File: tests/positive_fixtures/fs_parentdirname.shx
  • Tests parent directory name extraction

fs.path()

Construct a file path by joining components:

let dir: string = "/home/user";
let subdir: string = "documents";
let filename: string = "report.txt";

let fullPath: string = fs.path(dir, subdir, filename);
console.log("Full path: ${fullPath}"); // Output: /home/user/documents/report.txt

Generated Bash:

dir="/home/user"
subdir="documents"
filename="report.txt"

fullPath="${dir}/${subdir}/${filename}"
echo "Full path: ${fullPath}"

Test Coverage:

  • File: tests/positive_fixtures/fs_path.shx
  • Tests path construction with proper separator handling

Directory Operations

fs.createTempFolder()

Create a secure temporary directory and return its absolute path. Useful for scratch workspaces you’ll clean up later.

// Default: under $TMPDIR or /tmp, prefix "utah"
let tmpDir: string = fs.createTempFolder();
console.log("Temp dir: ${tmpDir}");

// Custom prefix
let jobTmp: string = fs.createTempFolder("job");

// Custom prefix and base directory
let buildTmp: string = fs.createTempFolder("build", "/var/tmp");

// Remember to clean up when done
if (fs.exists(tmpDir)) {
fs.delete(tmpDir);
}

Generated Bash (simplified):

# Defaults
_utah_tmp_base="${TMPDIR:-/tmp}"
_utah_prefix="utah"
_utah_prefix=$(echo "${_utah_prefix}" | tr -cd '[:alnum:]_.-')
[ -z "${_utah_prefix}" ] && _utah_prefix=utah

if command -v mktemp >/dev/null 2>&1; then
dir=$(mktemp -d -t "${_utah_prefix}.XXXXXXXX" 2>/dev/null) || \
dir=$(mktemp -d "${_utah_tmp_base%/}/${_utah_prefix}.XXXXXXXX" 2>/dev/null)
fi

if [ -z "${dir}" ]; then
# Fallback with secure mkdir retry
for _i in 1 2 3 4 5 6 7 8 9 10; do
_suf=$(LC_ALL=C tr -dc 'a-z0-9' </dev/urandom | head -c12)
[ -z "${_suf}" ] && _suf=$$
_cand="${_utah_tmp_base%/}/${_utah_prefix}-${_suf}"
if mkdir -m 700 "${_cand}" 2>/dev/null; then dir="${_cand}"; break; fi
done
fi

if [ -z "${dir}" ]; then echo "Error: Could not create temporary directory" >&2; exit 1; fi
echo "${dir}"

Notes:

  • Prefix is sanitized to safe characters [A-Za-z0-9_.-]; defaults to "utah" if empty
  • Directory permissions are 0700; you are responsible for removing it when done
  • When baseDir is supplied, creation happens under that directory

Test Coverage:

  • File: tests/positive_fixtures/fs_create_temp_folder_default.shx

Creating Directories

// Create a single directory
`$(mkdir -p "new_directory")`;

// Create nested directories
`$(mkdir -p "project/src/components")`;

// Check if directory was created
if (fs.exists("project/src")) {
console.log("Directory structure created successfully");
}

Listing Directory Contents

// List files in current directory
let files: string = `$(ls -la)`;
console.log("Directory contents:\n${files}");

// List only files (not directories)
let filesOnly: string = `$(find . -maxdepth 1 -type f)`;
console.log("Files only:\n${filesOnly}");

// List only directories
let dirsOnly: string = `$(find . -maxdepth 1 -type d)`;
console.log("Directories only:\n${dirsOnly}");

Practical Examples

File Backup System

script.description("File backup utility");

function backupFile(source: string, backupDir: string): boolean {
if (!fs.exists(source)) {
console.log("Source file not found: ${source}");
return false;
}

// Create backup directory if it doesn't exist
if (!fs.exists(backupDir)) {
"$(mkdir -p "${backupDir}")";
}

// Generate backup filename with timestamp
let filename: string = fs.filename(source);
let extension: string = fs.extension(filename);
let baseName: string = filename.replace(".${extension}", "");
let timestamp: string = `$(date +%Y%m%d_%H%M%S)`;

let backupName: string;
if (extension == "") {
backupName = "${baseName}_${timestamp}";
} else {
backupName = "${baseName}_${timestamp}.${extension}";
}

let backupPath: string = fs.path(backupDir, backupName);

// Copy file to backup location
let result: number = "$(cp "${source}" "${backupPath}")";
if (result == 0) {
console.log("✓ Backed up: ${source} → ${backupPath}");
return true;
} else {
console.log("✗ Failed to backup: ${source}");
return false;
}
}

// Usage
let sourceFiles: string[] = ["config.json", "database.db", "app.log"];
let backupDirectory: string = "backups";

for (let file: string in sourceFiles) {
backupFile(file, backupDirectory);
}

Log File Manager

script.description("Log file rotation and cleanup");

function rotateLogFile(logFile: string, maxSize: number): void {
if (!fs.exists(logFile)) {
console.log("Log file not found: ${logFile}");
return;
}

// Check file size (in bytes)
let size: number = "$(stat -c%s "${logFile}")";

if (size > maxSize) {
console.log("Log file ${logFile} is ${size} bytes, rotating...");

// Create rotated filename
let timestamp: string = `$(date +%Y%m%d_%H%M%S)`;
let dir: string = fs.dirname(logFile);
let filename: string = fs.filename(logFile);
let rotatedName: string = "${filename}.${timestamp}";
let rotatedPath: string = fs.path(dir, rotatedName);

// Move current log to rotated name
"$(mv "${logFile}" "${rotatedPath}")";

// Create new empty log file
fs.writeFile(logFile, "");

console.log("Log rotated: ${logFile} → ${rotatedPath}");
} else {
console.log("Log file ${logFile} is ${size} bytes, no rotation needed");
}
}

function cleanupOldLogs(logDir: string, daysToKeep: number): void {
if (!fs.exists(logDir)) {
console.log("Log directory not found: ${logDir}");
return;
}

console.log("Cleaning up logs older than ${daysToKeep} days in ${logDir}");

// Find and remove old log files
"$(find "${logDir}" -name "*.log.*" -type f -mtime +${daysToKeep} -delete)";

console.log("Log cleanup completed");
}

// Usage
rotateLogFile("app.log", 10485760); // 10MB
rotateLogFile("error.log", 5242880); // 5MB
cleanupOldLogs("logs", 30); // Keep 30 days

Configuration File Manager

script.description("Configuration file manager");

function loadConfig(configFile: string): object {
if (!fs.exists(configFile)) {
console.log("Config file not found: ${configFile}, creating default");
let defaultConfig: object = json.parse('{"version": "1.0", "debug": false}');
saveConfig(configFile, defaultConfig);
return defaultConfig;
}

try {
let content: string = fs.readFile(configFile);
if (!json.isValid(content)) {
console.log("Invalid JSON in ${configFile}, using defaults");
return json.parse('{"version": "1.0", "debug": false}');
}
return json.parse(content);
}
catch {
console.log("Error reading ${configFile}, using defaults");
return json.parse('{"version": "1.0", "debug": false}');
}
}

function saveConfig(configFile: string, config: object): void {
try {
let configJson: string = json.stringify(config, true);
fs.writeFile(configFile, configJson);
console.log("Configuration saved to ${configFile}");
}
catch {
console.log("Error saving configuration to ${configFile}");
}
}

function backupConfig(configFile: string): string {
if (!fs.exists(configFile)) {
return "";
}

let dir: string = fs.dirname(configFile);
let filename: string = fs.filename(configFile);
let timestamp: string = `$(date +%Y%m%d_%H%M%S)`;
let backupName: string = "${filename}.backup.${timestamp}";
let backupPath: string = fs.path(dir, backupName);

"$(cp "${configFile}" "${backupPath}")";
console.log("Config backed up to: ${backupPath}");
return backupPath;
}

// Usage
let configPath: string = "app.json";

// Backup existing config
let backupPath: string = backupConfig(configPath);

// Load current config
let config: object = loadConfig(configPath);

// Modify config
config = json.set(config, ".lastUpdate", timer.current());
config = json.set(config, ".version", "1.1");

// Save updated config
saveConfig(configPath, config);

Project File Organizer

script.description("Organize project files by type");

function organizeFiles(sourceDir: string): void {
if (!fs.exists(sourceDir)) {
console.log("Source directory not found: ${sourceDir}");
return;
}

// Define file type mappings
let typeMap: object = json.parse(`{
"images": ["jpg", "jpeg", "png", "gif", "bmp", "svg"],
"documents": ["pdf", "doc", "docx", "txt", "md"],
"code": ["js", "ts", "py", "java", "cpp", "c", "h"],
"data": ["json", "xml", "csv", "yaml", "yml"],
"archives": ["zip", "tar", "gz", "rar", "7z"]
}`);

// Get all files in source directory
let allFiles: string = "$(find "${sourceDir}" -maxdepth 1 -type f)";
let fileList: string[] = allFiles.split("\n");

for (let file: string in fileList) {
if (file == "") continue;

let filename: string = fs.filename(file);
let extension: string = fs.extension(filename);

if (extension == "") {
console.log("Skipping file without extension: ${filename}");
continue;
}

// Find appropriate category
let category: string = "misc";

if (json.getArray(typeMap, ".images").contains(extension)) {
category = "images";
} else if (json.getArray(typeMap, ".documents").contains(extension)) {
category = "documents";
} else if (json.getArray(typeMap, ".code").contains(extension)) {
category = "code";
} else if (json.getArray(typeMap, ".data").contains(extension)) {
category = "data";
} else if (json.getArray(typeMap, ".archives").contains(extension)) {
category = "archives";
}

// Create category directory
let categoryDir: string = fs.path(sourceDir, category);
if (!fs.exists(categoryDir)) {
"$(mkdir -p "${categoryDir}")";
console.log("Created directory: ${categoryDir}");
}

// Move file to category directory
let destination: string = fs.path(categoryDir, filename);
"$(mv "${file}" "${destination}")";
console.log("Moved ${filename} to ${category}/");
}

console.log("File organization completed");
}

// Usage
args.define("--dir", "-d", "Directory to organize", "string", false, ".");
let targetDir: string = args.get("--dir");

organizeFiles(targetDir);

Best Practices

1. Always Check File Existence

// Good - check before operations
if (fs.exists("config.json")) {
let config: string = fs.readFile("config.json");
// Process config...
} else {
console.log("Config file not found, using defaults");
}

// Avoid - assuming files exist
let config: string = fs.readFile("config.json"); // May fail

2. Handle Path Separators Properly

// Good - use fs.path() for cross-platform compatibility
let fullPath: string = fs.path(baseDir, subDir, filename);

// Avoid - hardcoding separators
let fullPath: string = baseDir + "/" + subDir + "/" + filename; // Unix-specific

3. Use Try-Catch for File Operations

function safeReadFile(filename: string): string {
try {
return fs.readFile(filename);
}
catch {
console.log("Failed to read file: ${filename}");
return "";
}
}

4. Validate File Extensions

function isImageFile(filename: string): boolean {
let ext: string = fs.extension(filename).toLowerCase();
let imageExts: string[] = ["jpg", "jpeg", "png", "gif", "bmp"];
return array.contains(imageExts, ext);
}

5. Create Directories When Needed

function ensureDirectory(dirPath: string): void {
if (!fs.exists(dirPath)) {
"$(mkdir -p "${dirPath}")";
console.log("Created directory: ${dirPath}");
}
}

Function Reference Summary

FunctionPurposeReturn TypeExample
fs.exists(path)Check existencebooleanfs.exists("file.txt")
fs.readFile(path)Read file contentstringfs.readFile("config.json")
fs.writeFile(path, content)Write to filevoidfs.writeFile("out.txt", data)
fs.copy(source, target)Copy file/directory with directory creationbooleanfs.copy("src.txt", "dst.txt")
fs.move(source, target)Move/rename file/directory with directory creationbooleanfs.move("old.txt", "new.txt")
fs.rename(oldName, newName)Rename file/directory in placebooleanfs.rename("old.txt", "new.txt")
fs.delete(path)Delete file/directory recursivelybooleanfs.delete("temp.txt")
fs.find(path, name?)Search for files/directories with wildcard patternsstring[]fs.find(".", "*.md")
fs.appendFile(path, content)Append to filevoidfs.appendFile("log.txt", entry)
fs.filename(path)Extract filenamestringfs.filename("/path/file.txt")
fs.dirname(path)Extract directorystringfs.dirname("/path/file.txt")
fs.extension(path)Get file extensionstringfs.extension("file.txt")
fs.parentDirName(path)Parent dir namestringfs.parentDirName("/a/b/c")
fs.path(...)Join path componentsstringfs.path(dir, subdir, file)
fs.createTempFolder(prefix?, baseDir?)Create secure temporary directorystringfs.createTempFolder("job", "/var/tmp")

Filesystem functions are essential for any script that works with files and directories. They provide a safe, cross-platform way to manipulate the filesystem while maintaining Utah's type safety guarantees.