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():
Aspect | fs.move() | fs.rename() |
---|---|---|
Purpose | Move files/directories between locations | Rename files/directories |
Directory Creation | Yes (mkdir -p ) | No |
Typical Use | Cross-directory moves | Same-directory renames |
Parameter Names | sourcePath , targetPath | oldName , newName |
Complexity | Higher (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/execute644
=rw-r--r--
- Owner: read/write, Group/Others: read only600
=rw-------
- Owner: read/write, Group/Others: no access400
=r--------
- Owner: read only, Group/Others: no access444
=r--r--r--
- All: read only
Symbolic Permissions:
u+x
- Add execute permission for user (owner)g+w
- Add write permission for groupo-r
- Remove read permission for othersa+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
or600
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 .txtfile?.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 changedcreate
- File or directory was createddelete
- File or directory was deletedmove
- 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
Function | Purpose | Return Type | Example |
---|---|---|---|
fs.exists(path) | Check existence | boolean | fs.exists("file.txt") |
fs.readFile(path) | Read file content | string | fs.readFile("config.json") |
fs.writeFile(path, content) | Write to file | void | fs.writeFile("out.txt", data) |
fs.copy(source, target) | Copy file/directory with directory creation | boolean | fs.copy("src.txt", "dst.txt") |
fs.move(source, target) | Move/rename file/directory with directory creation | boolean | fs.move("old.txt", "new.txt") |
fs.rename(oldName, newName) | Rename file/directory in place | boolean | fs.rename("old.txt", "new.txt") |
fs.delete(path) | Delete file/directory recursively | boolean | fs.delete("temp.txt") |
fs.find(path, name?) | Search for files/directories with wildcard patterns | string[] | fs.find(".", "*.md") |
fs.appendFile(path, content) | Append to file | void | fs.appendFile("log.txt", entry) |
fs.filename(path) | Extract filename | string | fs.filename("/path/file.txt") |
fs.dirname(path) | Extract directory | string | fs.dirname("/path/file.txt") |
fs.extension(path) | Get file extension | string | fs.extension("file.txt") |
fs.parentDirName(path) | Parent dir name | string | fs.parentDirName("/a/b/c") |
fs.path(...) | Join path components | string | fs.path(dir, subdir, file) |
fs.createTempFolder(prefix?, baseDir?) | Create secure temporary directory | string | fs.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.