Skip to main content

Control Flow

Utah provides modern control flow structures that compile to efficient bash code. All control structures use familiar TypeScript-like syntax with proper type checking.

Conditional Statements

If Statements

Basic if statements with comparison operators:

let temperature: number = 25;

if (temperature > 30) {
console.log("It's hot outside");
}

Generated Bash:

temperature=25
if [ "${temperature}" -gt 30 ]; then
echo "It's hot outside"
fi

If-Else Statements

Full conditional branching:

let score: number = 85;

if (score >= 90) {
console.log("Excellent!");
} else if (score >= 80) {
console.log("Good job!");
} else if (score >= 70) {
console.log("Not bad");
} else {
console.log("Keep trying");
}

String Comparisons

String equality and pattern matching:

let environment: string = "production";

if (environment == "production") {
console.log("Running in production mode");
} else if (environment == "development") {
console.log("Running in development mode");
} else {
console.log("Unknown environment");
}

Boolean Logic

Combine conditions with logical operators:

let hasPermission: boolean = true;
let isLoggedIn: boolean = true;
let attempts: number = 3;

if (isLoggedIn && hasPermission) {
console.log("Access granted");
} else if (!isLoggedIn) {
console.log("Please log in first");
} else if (attempts < 5) {
console.log("Permission denied, but you can try again");
} else {
console.log("Too many attempts");
}

Switch Statements

Switch statements provide clean multi-way branching:

Basic Switch

let action: string = "create";

switch (action) {
case "create":
console.log("Creating new file");
break;
case "delete":
console.log("Deleting file");
break;
case "update":
console.log("Updating file");
break;
default:
console.log("Unknown action");
}

Generated Bash:

action="create"
case "${action}" in
"create")
echo "Creating new file"
;;
"delete")
echo "Deleting file"
;;
"update")
echo "Updating file"
;;
*)
echo "Unknown action"
;;
esac

Switch with Fall-through

Omit break statements for fall-through behavior:

let fileType: string = "jpg";

switch (fileType) {
case "jpg":
case "jpeg":
console.log("JPEG image format");
break;
case "png":
case "gif":
console.log("Lossless image format");
break;
default:
console.log("Unknown image format");
}

Loop Statements

For Loops

Traditional C-style for loops:

for (let i: number = 0; i < 10; i++) {
console.log("Iteration ${i}");
}

Generated Bash:

for ((i=0; i<10; i++)); do
echo "Iteration ${i}"
done

For-In Loops

Iterate over arrays and strings:

let fruits: string[] = ["apple", "banana", "orange"];

for (let fruit: string in fruits) {
console.log("Processing: ${fruit}");
}

Generated Bash:

fruits=("apple" "banana" "orange")
for fruit in "${fruits[@]}"; do
echo "Processing: ${fruit}"
done

While Loops

Conditional iteration:

let count: number = 0;

while (count < 5) {
console.log("Count is: ${count}");
count++;
}

Generated Bash:

count=0
while [ "${count}" -lt 5 ]; do
echo "Count is: ${count}"
((count++))
done

Loop Control

Use break to exit loops early:

let numbers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

for (let num: number in numbers) {
if (num > 5) {
break;
}
console.log("Number: ${num}");
}

Comparison Operators

Utah supports various comparison operators:

Numeric Comparisons

let a: number = 10;
let b: number = 20;

if (a == b) { console.log("Equal"); }
if (a != b) { console.log("Not equal"); }
if (a < b) { console.log("Less than"); }
if (a <= b) { console.log("Less than or equal"); }
if (a > b) { console.log("Greater than"); }
if (a >= b) { console.log("Greater than or equal"); }

String Equality Examples

let name1: string = "Alice";
let name2: string = "Bob";

if (name1 == name2) { console.log("Same name"); }
if (name1 != name2) { console.log("Different names"); }

Boolean Operations

let isActive: boolean = true;
let isVerified: boolean = false;

if (isActive) { console.log("Active"); }
if (!isVerified) { console.log("Not verified"); }
if (isActive && isVerified) { console.log("Active and verified"); }
if (isActive || isVerified) { console.log("Active or verified"); }

Ternary Operator

Conditional expressions for simple branching:

let age: number = 20;
let status: string = (age >= 18) ? "adult" : "minor";
console.log("Status: ${status}");

Generated Bash:

age=20
if [ "${age}" -ge 18 ]; then
status="adult"
else
status="minor"
fi
echo "Status: ${status}"

Nested Conditions

Complex conditional logic:

let userType: string = "admin";
let permissions: string[] = ["read", "write", "delete"];

if (userType == "admin") {
if (array.contains(permissions, "delete")) {
console.log("Admin with delete permission");
} else {
console.log("Admin without delete permission");
}
} else if (userType == "user") {
if (array.contains(permissions, "write")) {
console.log("User with write permission");
} else {
console.log("Read-only user");
}
} else {
console.log("Unknown user type");
}

Practical Examples

File Processing

let files: string[] = ["config.json", "data.txt", "backup.sql"];

for (let file: string in files) {
if (fs.exists(file)) {
let extension: string = fs.extension(file);

switch (extension) {
case "json":
console.log("Processing JSON file: ${file}");
// Process JSON
break;
case "txt":
console.log("Processing text file: ${file}");
// Process text
break;
case "sql":
console.log("Processing SQL file: ${file}");
// Process SQL
break;
default:
console.log("Unknown file type: ${file}");
}
} else {
console.log("File not found: ${file}");
}
}

User Input Validation

args.define("--port", "-p", "Server port", "number", false, 8080);
args.define("--environment", "-e", "Environment", "string", false, "development");

let port: number = args.get("--port");
let env: string = args.get("--environment");

// Validate port
if (port < 1 || port > 65535) {
console.log("Error: Port must be between 1 and 65535");
exit(1);
}

// Validate environment
let validEnvs: string[] = ["development", "staging", "production"];
if (!array.contains(validEnvs, env)) {
console.log("Error: Invalid environment. Must be one of: development, staging, production");
exit(1);
}

console.log("Starting server on port ${port} in ${env} mode");

Service Health Check

let services: string[] = ["nginx", "mysql", "redis"];
let healthyServices: string[] = [];
let unhealthyServices: string[] = [];

for (let service: string in services) {
console.log("Checking ${service}...");

if (os.isInstalled("systemctl")) {
// Use systemctl for service status
let status: string = "$(systemctl is-active ${service})";

if (status == "active") {
healthyServices[healthyServices.length] = service;
console.log("✓ ${service} is running");
} else {
unhealthyServices[unhealthyServices.length] = service;
console.log("✗ ${service} is not running");
}
} else {
console.log("Cannot check ${service}: systemctl not available");
}
}

console.log(`\nHealth Check Summary:`);
console.log("Healthy services: ${healthyServices.length}");
console.log("Unhealthy services: ${unhealthyServices.length}");

if (unhealthyServices.length > 0) {
console.log("Services need attention:");
for (let service: string in unhealthyServices) {
console.log(" - ${service}");
}
exit(1);
} else {
console.log("All services are healthy!");
}

Configuration Management

let configFile: string = "app.json";
let defaultConfig: string = "{\"debug\": false, \"port\": 3000, \"timeout\": 30}";

// Load configuration
let config: object;
if (fs.exists(configFile)) {
try {
let configData: string = fs.readFile(configFile);
if (json.isValid(configData)) {
config = json.parse(configData);
console.log("Configuration loaded from file");
} else {
console.log("Invalid JSON in config file, using defaults");
config = json.parse(defaultConfig);
}
}
catch {
console.log("Error reading config file, using defaults");
config = json.parse(defaultConfig);
}
} else {
console.log("Config file not found, using defaults");
config = json.parse(defaultConfig);
}

// Extract and validate configuration values
let debug: boolean = json.getBool(config, ".debug");
let port: number = json.getNumber(config, ".port");
let timeout: number = json.getNumber(config, ".timeout");

// Validate values
if (port < 1 || port > 65535) {
console.log("Warning: Invalid port in config, using default 3000");
port = 3000;
}

if (timeout < 1 || timeout > 300) {
console.log("Warning: Invalid timeout in config, using default 30");
timeout = 30;
}

console.log("Starting with config: debug=${debug}, port=${port}, timeout=${timeout}");

Best Practices

1. Use Explicit Type Comparisons

// Good - explicit type checking
if (count == 0) {
console.log("No items");
}

// Good - boolean variables
if (isEnabled) {
console.log("Feature enabled");
}

// Avoid - implicit truthiness
if (count) { // This might not work as expected
console.log("Has items");
}

2. Validate Input Early

function processFile(filename: string): void {
// Validate input first
if (filename == "") {
console.log("Error: Filename cannot be empty");
return;
}

if (!fs.exists(filename)) {
console.log("Error: File not found: ${filename}");
return;
}

// Process the file
console.log("Processing: ${filename}");
}

3. Use Switch for Multiple Conditions

// Good - clear and maintainable
switch (operation) {
case "create":
createFile();
break;
case "delete":
deleteFile();
break;
case "update":
updateFile();
break;
default:
console.log("Unknown operation");
}

// Avoid - nested if-else for many conditions
if (operation == "create") {
createFile();
} else if (operation == "delete") {
deleteFile();
} else if (operation == "update") {
updateFile();
} else {
console.log("Unknown operation");
}

4. Handle Edge Cases

let items: string[] = ["a", "b", "c"];

// Check array is not empty before processing
if (items.length > 0) {
for (let item: string in items) {
console.log("Processing: ${item}");
}
} else {
console.log("No items to process");
}

Common Patterns

Early Return Pattern

function validateConfig(config: object): boolean {
if (!json.has(config, ".version")) {
console.log("Missing version in config");
return false;
}

if (!json.has(config, ".database")) {
console.log("Missing database config");
return false;
}

let version: string = json.get(config, ".version");
if (version == "") {
console.log("Empty version in config");
return false;
}

console.log("Config validation passed");
return true;
}

Guard Clauses

function backupDatabase(dbName: string): void {
// Guard clauses at the top
if (dbName == "") {
console.log("Database name required");
return;
}

if (!os.isInstalled("mysqldump")) {
console.log("mysqldump not available");
return;
}

if (!fs.exists("/var/backups")) {
console.log("Backup directory not found");
return;
}

// Main logic here
console.log("Backing up database: ${dbName}");
}

State Machine Pattern

let state: string = "init";
let running: boolean = true;

while (running) {
switch (state) {
case "init":
console.log("Initializing...");
state = "loading";
break;

case "loading":
console.log("Loading data...");
state = "processing";
break;

case "processing":
console.log("Processing data...");
state = "complete";
break;

case "complete":
console.log("Processing complete");
running = false;
break;

default:
console.log("Unknown state");
running = false;
}
}

Control flow is the foundation of program logic in Utah. These structures provide the building blocks for creating sophisticated automation scripts while maintaining the readability and type safety that Utah is designed to provide.