Broad claims about language syntax often become vague. A better way to compare languages is to write the same small program in each one and look at the amount of ceremony required.
This note compares Ruby, Go, Rust, and Zig using a minimal CLI app. The program accepts a name, prints a greeting, and supports an optional --shout flag that uppercases the output.
The CLI behavior
greet Alice
# Hello, Alice
greet --shout Alice
# HELLO, ALICE
Ruby
Ruby is the baseline. It is dynamic, compact, and has a strong scripting feel. For a small CLI, the standard library already gives a pleasant option parser.
#!/usr/bin/env ruby
require "optparse"
options = { shout: false }
parser = OptionParser.new do |opts|
opts.banner = "Usage: greet [options] NAME"
opts.on("--shout", "Uppercase the greeting") do
options[:shout] = true
end
end
parser.parse!
name = ARGV.first or abort parser.to_s
message = "Hello, #{name}"
message = message.upcase if options[:shout]
puts message
How it feels
Ruby keeps the program close to the problem statement. Parse options, read the positional argument, build a string, optionally transform it, and print it. There is little type ceremony and little structural overhead.
Go
Go is simple and readable, but it tends to be plain rather than expressive. Its standard flag package is enough for this example.
package main
import (
"flag"
"fmt"
"os"
"strings"
)
func main() {
shout := flag.Bool("shout", false, "Uppercase the greeting")
flag.Parse()
if flag.NArg() < 1 {
fmt.Fprintln(os.Stderr, "Usage: greet [--shout] NAME")
os.Exit(1)
}
message := fmt.Sprintf("Hello, %s", flag.Arg(0))
if *shout {
message = strings.ToUpper(message)
}
fmt.Println(message)
}
How it feels
Go is clear, but not especially terse. For this example, error handling is not the main issue, but the language still has visible boilerplate: package declaration, imports, explicit main, pointer flag values, and manual usage handling.
Rust
Rust can be verbose in systems code, but for CLI apps the ecosystem helps a lot. With clap, argument parsing becomes declarative and compact.
use clap::Parser;
#[derive(Parser)]
struct Cli {
name: String,
#[arg(long)]
shout: bool,
}
fn main() {
let cli = Cli::parse();
let mut message = format!("Hello, {}", cli.name);
if cli.shout {
message = message.to_uppercase();
}
println!("{message}");
}
Cargo.toml:
[dependencies]
clap = { version = "4", features = ["derive"] }
How it feels
Rust is surprisingly clean here. The derive macro hides a lot of parsing logic, and the resulting program is readable. It is still more formal than Ruby, but less noisy than Go for this particular CLI example.
Zig
Zig is explicit and low-level. It gives direct control, but small app-glue tasks usually expose more manual detail than Ruby, Go, or Rust.
const std = @import("std");
pub fn main() !void {
var args = try std.process.argsWithAllocator(std.heap.page_allocator);
defer args.deinit();
_ = args.next(); // program name
var shout = false;
var name: ?[]const u8 = null;
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--shout")) {
shout = true;
} else {
name = arg;
}
}
const actual_name = name orelse {
std.debug.print("Usage: greet [--shout] NAME\n", .{});
std.process.exit(1);
};
if (shout) {
var buffer: [256]u8 = undefined;
const upper = std.ascii.upperString(&buffer, actual_name);
std.debug.print("HELLO, {s}\n", .{upper});
} else {
std.debug.print("Hello, {s}\n", .{actual_name});
}
}
How it feels
Zig has clean error propagation with try, but the rest of the program is visibly more manual. The code exposes allocation, argument iteration, optional handling, fixed buffers, and explicit formatting. This is not bad; it is just not dynamic-language-like.
Comparison
| Language | Strength in this example | Main source of ceremony |
|---|---|---|
| Ruby | Shortest and closest to the task description | Minimal; mostly option parser setup |
| Go | Simple, explicit, easy to read | Package/import structure, flag pointer, manual checks |
| Rust | Very clean with clap |
Structs, derive macros, dependency setup |
| Zig | Precise and low-level | Manual argument parsing, allocation, buffers |
Ruby > Rust > Go > Zig
Why Rust ranks above Go here
Go often feels close to a dynamic language because its syntax is simple. But once error handling or repetitive checks enter the program, the code can become noisy. For CLI apps specifically, Rust's ecosystem can offset some of Rust's inherent explicitness.
With clap, Rust allows the CLI shape to be described as data: a struct with fields and attributes. That can feel more concise than manually coordinating flags, arguments, validation, and usage text.
Final take
If the question is "which language feels closest to Ruby for small scripts and CLI tools," Ruby remains the clear winner. Among the static languages, Rust can be more ergonomic than expected when backed by good libraries. Go is readable but often mechanically repetitive. Zig is a strong systems language, but it is the least suited to this kind of high-level app glue if terseness is the priority.
Sources
- Ruby OptionParser documentation, standard-library option parsing for command-line programs.
- Go package flag documentation, standard-library command-line flag parsing.
- clap derive tutorial, declarative Rust CLI parsing through derive macros.
- Zig standard library documentation for
std.process.argsWithAllocator. - Zig standard library documentation for
std.ascii.upperString.