Functions
Utah provides first-class function support with TypeScript-inspired syntax, strong typing, and proper scoping. Functions compile to efficient bash function definitions.
Function Declaration
Basic Syntax
Functions are declared with the function
keyword and require type annotations:
function greet(name: string): void {
console.log("Hello, ${name}!");
}
greet("World");
Generated Bash:
greet() {
local name="$1"
echo "Hello, ${name}!"
}
greet "World"
Return Values
Functions can return typed values:
function add(a: number, b: number): number {
return a + b;
}
let result: number = add(5, 3);
console.log("Result: ${result}");
Generated Bash:
add() {
local a="$1"
local b="$2"
echo $((a + b))
}
result=$(add 5 3)
echo "Result: ${result}"
String Return Values
function getGreeting(name: string): string {
return "Hello, ${name}!";
}
let message: string = getGreeting("Alice");
console.log(message);
Generated Bash:
getGreeting() {
local name="$1"
echo "Hello, ${name}!"
}
message=$(getGreeting "Alice")
echo "${message}"
Parameter Types
Multiple Parameters
Functions can accept multiple typed parameters:
function createUser(name: string, age: number, active: boolean): string {
let status: string = active ? "active" : "inactive";
return "User: ${name}, Age: ${age}, Status: ${status}";
}
let userInfo: string = createUser("Bob", 25, true);
console.log(userInfo);
Array Parameters
Pass arrays as function parameters:
function processItems(items: string[]): number {
let count: number = 0;
for (let item: string in items) {
console.log("Processing: ${item}");
count++;
}
return count;
}
let files: string[] = ["file1.txt", "file2.txt", "file3.txt"];
let processed: number = processItems(files);
console.log("Processed ${processed} items");
Generated Bash:
processItems() {
local items=("$@")
local count=0
for item in "${items[@]}"; do
echo "Processing: ${item}"
((count++))
done
echo "${count}"
}
files=("file1.txt" "file2.txt" "file3.txt")
processed=$(processItems "${files[@]}")
echo "Processed ${processed} items"
Local Variables and Scoping
Local Variable Scope
Variables declared inside functions are automatically scoped locally:
let globalVar: string = "global";
function testScope(): void {
let localVar: string = "local";
globalVar = "modified global";
console.log("Local: ${localVar}");
console.log("Global: ${globalVar}");
}
testScope();
console.log("Outside function: ${globalVar}");
Generated Bash:
globalVar="global"
testScope() {
local localVar="local"
globalVar="modified global"
echo "Local: ${localVar}"
echo "Global: ${globalVar}"
}
testScope
echo "Outside function: ${globalVar}"
Variable Shadowing
Local variables can shadow global ones:
let name: string = "Global";
function showName(): void {
let name: string = "Local";
console.log("Inside function: ${name}");
}
showName();
console.log("Outside function: ${name}");
Function Composition
Calling Functions from Functions
Functions can call other functions:
function formatName(first: string, last: string): string {
return "${first} ${last}";
}
function createWelcome(first: string, last: string): string {
let fullName: string = formatName(first, last);
return "Welcome, ${fullName}!";
}
let welcome: string = createWelcome("John", "Doe");
console.log(welcome);
Helper Functions
Break complex logic into smaller functions:
function isValidEmail(email: string): boolean {
return string.contains(email, "@") && string.contains(email, ".");
}
function isValidAge(age: number): boolean {
return age >= 0 && age <= 150;
}
function validateUser(email: string, age: number): boolean {
if (!isValidEmail(email)) {
console.log("Invalid email format");
return false;
}
if (!isValidAge(age)) {
console.log("Invalid age");
return false;
}
return true;
}
if (validateUser("user@example.com", 25)) {
console.log("User is valid");
}
Advanced Function Patterns
Early Return Pattern
Use early returns to reduce nesting:
function processFile(filename: string): boolean {
if (filename == "") {
console.log("Filename cannot be empty");
return false;
}
if (!fs.exists(filename)) {
console.log("File not found: ${filename}");
return false;
}
let content: string = fs.readFile(filename);
if (content == "") {
console.log("File is empty");
return false;
}
console.log("Processing file: ${filename}");
return true;
}
Factory Functions
Functions that create and return data:
function createConfig(env: string): object {
let baseConfig: object = json.parse('{"version": "1.0"}');
if (env == "development") {
baseConfig = json.set(baseConfig, ".debug", true);
baseConfig = json.set(baseConfig, ".port", 3000);
} else if (env == "production") {
baseConfig = json.set(baseConfig, ".debug", false);
baseConfig = json.set(baseConfig, ".port", 80);
}
return baseConfig;
}
let devConfig: object = createConfig("development");
let prodConfig: object = createConfig("production");
Transform Functions
Functions that process and transform data:
function normalizeFilename(filename: string): string {
// Remove invalid characters
filename = filename.replace(" ", "_");
filename = filename.replace("(", "");
filename = filename.replace(")", "");
// Convert to lowercase
filename = filename.toLowerCase();
return filename;
}
function processFiles(files: string[]): string[] {
let normalized: string[] = [];
for (let file: string in files) {
let clean: string = normalizeFilename(file);
normalized[normalized.length] = clean;
}
return normalized;
}
let originalFiles: string[] = ["My Document (1).txt", "Photo Album.jpg"];
let cleanFiles: string[] = processFiles(originalFiles);
Error Handling in Functions
Return Error Codes
Use return values to indicate success or failure:
function backupFile(source: string, dest: string): number {
if (!fs.exists(source)) {
console.log("Source file not found: ${source}");
return 1;
}
if (fs.exists(dest)) {
console.log("Destination already exists: ${dest}");
return 2;
}
console.log("Backing up ${source} to ${dest}");
// Copy logic would go here
return 0;
}
let result: number = backupFile("data.txt", "backup/data.txt");
if (result != 0) {
console.log("Backup failed with error code: ${result}");
exit(result);
}
Void Functions with Side Effects
Functions that perform actions without returning values:
function logMessage(level: string, message: string): void {
let timestamp: string = timer.current();
let logEntry: string = "[${timestamp}] ${level}: ${message}";
console.log(logEntry);
// Also write to log file
fs.appendFile("app.log", logEntry + "\n");
}
function logError(message: string): void {
logMessage("ERROR", message);
}
function logInfo(message: string): void {
logMessage("INFO", message);
}
logInfo("Application started");
logError("Database connection failed");
Practical Examples
Configuration Parser
function parseConfigFile(filename: string): object {
if (!fs.exists(filename)) {
console.log("Config file not found: ${filename}, using defaults");
return json.parse('{"port": 8080, "debug": false}');
}
let content: string = fs.readFile(filename);
if (!json.isValid(content)) {
console.log("Invalid JSON in config file, using defaults");
return json.parse('{"port": 8080, "debug": false}');
}
return json.parse(content);
}
function getConfigValue(config: object, key: string, defaultValue: string): string {
if (json.has(config, key)) {
return json.get(config, key);
}
return defaultValue;
}
// Usage
let config: object = parseConfigFile("app.json");
let port: string = getConfigValue(config, ".port", "8080");
let debug: string = getConfigValue(config, ".debug", "false");
console.log("Server will run on port ${port} with debug=${debug}");
File Processing Pipeline
function validateFile(filename: string): boolean {
if (!fs.exists(filename)) {
console.log("File not found: ${filename}");
return false;
}
let size: number = "$(stat -c%s "${filename}")";
if (size == 0) {
console.log("File is empty: ${filename}");
return false;
}
return true;
}
function processTextFile(filename: string): string {
let content: string = fs.readFile(filename);
// Clean up the content
content = content.trim();
content = content.replace("\r\n", "\n");
return content;
}
function saveProcessedFile(content: string, outputFile: string): boolean {
try {
fs.writeFile(outputFile, content);
console.log("Saved processed content to: ${outputFile}");
return true;
}
catch {
console.log("Failed to save file: ${outputFile}");
return false;
}
}
function processFilesPipeline(inputFiles: string[]): void {
for (let file: string in inputFiles) {
console.log("Processing: ${file}");
if (!validateFile(file)) {
continue;
}
let content: string = processTextFile(file);
let outputFile: string = "processed_${file}";
if (saveProcessedFile(content, outputFile)) {
console.log("Successfully processed: ${file}");
} else {
console.log("Failed to process: ${file}");
}
}
}
// Usage
let files: string[] = ["data1.txt", "data2.txt", "data3.txt"];
processFilesPipeline(files);
System Information Collector
function getSystemInfo(): object {
let info: object = json.parse('{}');
// Operating system
if (os.isInstalled("uname")) {
let osName: string = `$(uname -s)`;
info = json.set(info, ".os", osName);
}
// Memory info
if (fs.exists("/proc/meminfo")) {
let memTotal: string = `$(grep MemTotal /proc/meminfo | awk '{print $2}')`;
info = json.set(info, ".memory_kb", memTotal);
}
// Disk space
if (os.isInstalled("df")) {
let diskUsage: string = `$(df -h / | tail -1 | awk '{print $5}')`;
info = json.set(info, ".disk_usage", diskUsage);
}
return info;
}
function formatSystemInfo(info: object): void {
console.log("=== System Information ===");
if (json.has(info, ".os")) {
let os: string = json.get(info, ".os");
console.log("Operating System: ${os}");
}
if (json.has(info, ".memory_kb")) {
let memKb: string = json.get(info, ".memory_kb");
let memMb: number = memKb / 1024;
console.log("Total Memory: ${memMb} MB");
}
if (json.has(info, ".disk_usage")) {
let usage: string = json.get(info, ".disk_usage");
console.log("Disk Usage: ${usage}");
}
}
// Usage
let systemInfo: object = getSystemInfo();
formatSystemInfo(systemInfo);
Deployment Helper
function checkPrerequisites(): boolean {
let required: string[] = ["git", "docker", "curl"];
let missing: string[] = [];
for (let tool: string in required) {
if (!os.isInstalled(tool)) {
missing[missing.length] = tool;
}
}
if (missing.length > 0) {
console.log("Missing required tools:");
for (let tool: string in missing) {
console.log(" - ${tool}");
}
return false;
}
return true;
}
function deployApplication(version: string, environment: string): boolean {
console.log("Starting deployment of version ${version} to ${environment}");
if (!checkPrerequisites()) {
console.log("Prerequisites check failed");
return false;
}
// Validate environment
let validEnvs: string[] = ["dev", "staging", "prod"];
if (!array.contains(validEnvs, environment)) {
console.log("Invalid environment: ${environment}");
return false;
}
console.log("Building Docker image...");
let buildResult: number = "$(docker build -t myapp:${version} .)";
if (buildResult != 0) {
console.log("Docker build failed");
return false;
}
console.log("Deploying to environment...");
// Deployment logic would go here
console.log(`Deployment completed successfully`);
return true;
}
// Usage
args.define("--version", "-v", "Version to deploy", "string", true);
args.define("--env", "-e", "Target environment", "string", true);
let version: string = args.get("--version");
let environment: string = args.get("--env");
if (!deployApplication(version, environment)) {
console.log("Deployment failed");
exit(1);
}
Best Practices
1. Use Clear Function Names
// Good - descriptive names
function validateEmailAddress(email: string): boolean { }
function calculateTotalPrice(items: object[]): number { }
// Avoid - unclear names
function check(data: string): boolean { }
function calc(items: object[]): number { }
2. Keep Functions Focused
// Good - single responsibility
function readConfigFile(filename: string): string {
return fs.readFile(filename);
}
function parseJsonConfig(content: string): object {
return json.parse(content);
}
function validateConfig(config: object): boolean {
return json.has(config, ".version") && json.has(config, ".database");
}
// Avoid - doing too much
function loadAndValidateConfig(filename: string): object {
let content: string = fs.readFile(filename);
let config: object = json.parse(content);
if (!json.has(config, ".version")) {
exit(1);
}
return config;
}
3. Handle Edge Cases
function divideNumbers(a: number, b: number): number {
if (b == 0) {
console.log("Error: Division by zero");
return 0;
}
return a / b;
}
function getArrayElement(arr: string[], index: number): string {
if (index < 0 || index >= arr.length) {
console.log("Error: Array index out of bounds");
return "";
}
return arr[index];
}
4. Use Type Annotations
// Good - explicit types
function processUserData(name: string, age: number, active: boolean): object {
return json.parse("{"name": "${name}", "age": ${age}, "active": ${active}}");
}
// Required - Utah enforces type annotations
function calculateScore(points: number[], multiplier: number): number {
let total: number = 0;
for (let point: number in points) {
total += point * multiplier;
}
return total;
}
Functions are essential building blocks in Utah that enable code reuse, better organization, and maintainable scripts. With proper typing and scoping, they provide a robust foundation for complex automation tasks.