Skip to main content

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.