# Build a CLI Calculator

### Build a CLI Calculator

In this project, we will build a small command-line calculator.

The program will read an expression from the command line, parse it, compute the result, and print the answer.

We will start with simple expressions like this:

```bash
zig build run -- "1 + 2"
```

Output:

```text
3
```

Then:

```bash
zig build run -- "10 - 4"
```

Output:

```text
6
```

And:

```bash
zig build run -- "6 * 7"
```

Output:

```text
42
```

This is a small project, but it teaches several important Zig ideas:

You will read command-line arguments.

You will work with slices.

You will parse text.

You will return errors.

You will use `try`.

You will separate code into small functions.

You will build a real executable with `build.zig`.

#### The Goal

Our calculator will support four operators:

```text
+
-
*
/
```

It will accept expressions with this simple shape:

```text
number operator number
```

Examples:

```text
1 + 2
10 - 3
4 * 5
20 / 4
```

For now, we will not support parentheses, operator precedence, or long expressions like:

```text
1 + 2 * 3
```

That will come later. A good first project should have a small shape. We want to finish a complete working program before adding more features.

#### Create the Project

Create a new folder:

```bash
mkdir cli-calculator
cd cli-calculator
```

Then initialize a Zig executable project:

```bash
zig init
```

You should now have files similar to:

```text
build.zig
src/main.zig
```

The file `src/main.zig` is where our program starts.

#### First Version

Open `src/main.zig` and replace its contents with this:

```zig
const std = @import("std");

pub fn main() !void {
    var stdout_buffer: [1024]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_writer.interface;

    try stdout.print("CLI Calculator\n", .{});
    try stdout.flush();
}
```

Run it:

```bash
zig build run
```

You should see:

```text
CLI Calculator
```

This does not calculate anything yet. It only proves that the project builds and runs.

#### Reading Command-Line Arguments

A command-line program receives arguments from the shell.

When you run:

```bash
zig build run -- "1 + 2"
```

The string `"1 + 2"` is passed to your program as an argument.

Update `src/main.zig`:

```zig
const std = @import("std");

pub fn main() !void {
    var stdout_buffer: [1024]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_writer.interface;

    var stderr_buffer: [1024]u8 = undefined;
    var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
    const stderr = &stderr_writer.interface;

    var args = try std.process.argsWithAllocator(std.heap.page_allocator);
    defer args.deinit();

    _ = args.next();

    const expression = args.next() orelse {
        try stderr.print("usage: calc \"number operator number\"\n", .{});
        try stderr.flush();
        return;
    };

    try stdout.print("expression: {s}\n", .{expression});
    try stdout.flush();
}
```

Run:

```bash
zig build run -- "1 + 2"
```

Output:

```text
expression: 1 + 2
```

Now the program can read input.

This line skips the program name:

```zig
_ = args.next();
```

The first command-line argument is usually the program path. We do not need it, so we discard it.

This line reads the actual expression:

```zig
const expression = args.next() orelse {
    try stderr.print("usage: calc \"number operator number\"\n", .{});
    try stderr.flush();
    return;
};
```

`args.next()` returns an optional value. It may return an argument, or it may return `null` if there is no argument.

The `orelse` block handles the missing-argument case.

#### Splitting the Expression

Our expression has three parts:

```text
1 + 2
```

That means:

```text
left number: 1
operator: +
right number: 2
```

We can split the string by spaces.

Add this helper function above `main`:

```zig
fn parseExpression(expression: []const u8) !void {
    var parts = std.mem.splitScalar(u8, expression, ' ');

    const left_text = parts.next() orelse return error.InvalidExpression;
    const op_text = parts.next() orelse return error.InvalidExpression;
    const right_text = parts.next() orelse return error.InvalidExpression;

    if (parts.next() != null) {
        return error.InvalidExpression;
    }

    _ = left_text;
    _ = op_text;
    _ = right_text;
}
```

This function does not calculate yet. It only checks that the expression has exactly three pieces.

Now update `main` to call it:

```zig
try parseExpression(expression);
try stdout.print("valid expression\n", .{});
```

Full file:

```zig
const std = @import("std");

fn parseExpression(expression: []const u8) !void {
    var parts = std.mem.splitScalar(u8, expression, ' ');

    const left_text = parts.next() orelse return error.InvalidExpression;
    const op_text = parts.next() orelse return error.InvalidExpression;
    const right_text = parts.next() orelse return error.InvalidExpression;

    if (parts.next() != null) {
        return error.InvalidExpression;
    }

    _ = left_text;
    _ = op_text;
    _ = right_text;
}

pub fn main() !void {
    var stdout_buffer: [1024]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_writer.interface;

    var stderr_buffer: [1024]u8 = undefined;
    var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
    const stderr = &stderr_writer.interface;

    var args = try std.process.argsWithAllocator(std.heap.page_allocator);
    defer args.deinit();

    _ = args.next();

    const expression = args.next() orelse {
        try stderr.print("usage: calc \"number operator number\"\n", .{});
        try stderr.flush();
        return;
    };

    parseExpression(expression) catch {
        try stderr.print("invalid expression\n", .{});
        try stderr.flush();
        return;
    };

    try stdout.print("valid expression\n", .{});
    try stdout.flush();
}
```

Run:

```bash
zig build run -- "1 + 2"
```

Output:

```text
valid expression
```

Run:

```bash
zig build run -- "1 +"
```

Output:

```text
invalid expression
```

Now we have basic validation.

#### Returning a Parsed Expression

Instead of returning `void`, `parseExpression` should return useful data.

Create a struct:

```zig
const Expression = struct {
    left: i64,
    operator: u8,
    right: i64,
};
```

This struct stores the parsed form of the expression.

Now change `parseExpression`:

```zig
fn parseExpression(expression: []const u8) !Expression {
    var parts = std.mem.splitScalar(u8, expression, ' ');

    const left_text = parts.next() orelse return error.InvalidExpression;
    const op_text = parts.next() orelse return error.InvalidExpression;
    const right_text = parts.next() orelse return error.InvalidExpression;

    if (parts.next() != null) {
        return error.InvalidExpression;
    }

    if (op_text.len != 1) {
        return error.InvalidOperator;
    }

    const left = try std.fmt.parseInt(i64, left_text, 10);
    const right = try std.fmt.parseInt(i64, right_text, 10);

    return Expression{
        .left = left,
        .operator = op_text[0],
        .right = right,
    };
}
```

This line parses the left number:

```zig
const left = try std.fmt.parseInt(i64, left_text, 10);
```

The type is `i64`, which is a signed 64-bit integer.

The final argument, `10`, means base 10.

So this parses normal decimal numbers like:

```text
123
-50
0
```

#### Evaluating the Expression

Now we need a function that computes the result.

Add this:

```zig
fn evaluate(expr: Expression) !i64 {
    return switch (expr.operator) {
        '+' => expr.left + expr.right,
        '-' => expr.left - expr.right,
        '*' => expr.left * expr.right,
        '/' => {
            if (expr.right == 0) {
                return error.DivisionByZero;
            }

            return @divTrunc(expr.left, expr.right);
        },
        else => error.InvalidOperator,
    };
}
```

This function takes an `Expression` and returns either an `i64` result or an error.

The division case is special because division by zero is invalid.

```zig
if (expr.right == 0) {
    return error.DivisionByZero;
}
```

For integer division, we use:

```zig
@divTrunc(expr.left, expr.right)
```

This divides two integers and truncates the result toward zero.

So:

```text
7 / 2 = 3
-7 / 2 = -3
```

This is integer arithmetic, not floating-point arithmetic.

#### The Complete Program

Here is the complete version:

```zig
const std = @import("std");

const Expression = struct {
    left: i64,
    operator: u8,
    right: i64,
};

fn parseExpression(expression: []const u8) !Expression {
    var parts = std.mem.splitScalar(u8, expression, ' ');

    const left_text = parts.next() orelse return error.InvalidExpression;
    const op_text = parts.next() orelse return error.InvalidExpression;
    const right_text = parts.next() orelse return error.InvalidExpression;

    if (parts.next() != null) {
        return error.InvalidExpression;
    }

    if (op_text.len != 1) {
        return error.InvalidOperator;
    }

    const left = try std.fmt.parseInt(i64, left_text, 10);
    const right = try std.fmt.parseInt(i64, right_text, 10);

    return Expression{
        .left = left,
        .operator = op_text[0],
        .right = right,
    };
}

fn evaluate(expr: Expression) !i64 {
    return switch (expr.operator) {
        '+' => expr.left + expr.right,
        '-' => expr.left - expr.right,
        '*' => expr.left * expr.right,
        '/' => {
            if (expr.right == 0) {
                return error.DivisionByZero;
            }

            return @divTrunc(expr.left, expr.right);
        },
        else => error.InvalidOperator,
    };
}

pub fn main() !void {
    var stdout_buffer: [1024]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_writer.interface;

    var stderr_buffer: [1024]u8 = undefined;
    var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
    const stderr = &stderr_writer.interface;

    var args = try std.process.argsWithAllocator(std.heap.page_allocator);
    defer args.deinit();

    _ = args.next();

    const expression_text = args.next() orelse {
        try stderr.print("usage: calc \"number operator number\"\n", .{});
        try stderr.flush();
        return;
    };

    const expression = parseExpression(expression_text) catch |err| {
        try stderr.print("parse error: {s}\n", .{@errorName(err)});
        try stderr.flush();
        return;
    };

    const result = evaluate(expression) catch |err| {
        try stderr.print("calculation error: {s}\n", .{@errorName(err)});
        try stderr.flush();
        return;
    };

    try stdout.print("{d}\n", .{result});
    try stdout.flush();
}
```

Run it:

```bash
zig build run -- "1 + 2"
```

Output:

```text
3
```

Run:

```bash
zig build run -- "10 - 4"
```

Output:

```text
6
```

Run:

```bash
zig build run -- "6 * 7"
```

Output:

```text
42
```

Run:

```bash
zig build run -- "20 / 4"
```

Output:

```text
5
```

Run:

```bash
zig build run -- "1 / 0"
```

Output:

```text
calculation error: DivisionByZero
```

#### Why We Used a Struct

The `Expression` struct gives a name to the shape of our data.

Without a struct, we would pass around three separate values:

```zig
left
operator
right
```

That works for very small code, but it becomes messy quickly.

With a struct, we can pass one value:

```zig
const result = try evaluate(expression);
```

This makes the code easier to read.

The struct says:

```zig
const Expression = struct {
    left: i64,
    operator: u8,
    right: i64,
};
```

That means every expression has exactly these three fields.

#### Why We Used Errors

Parsing can fail.

Evaluation can fail.

So the function types should say that clearly.

This function may fail:

```zig
fn parseExpression(expression: []const u8) !Expression
```

This function may also fail:

```zig
fn evaluate(expr: Expression) !i64
```

The `!` is important. It tells the reader that the result is either a value or an error.

This is one of Zig’s strongest habits: failure is visible in the type.

#### Why Spaces Are Required

Our parser expects spaces:

```text
1 + 2
```

This works.

But this does not:

```text
1+2
```

Why?

Because we used:

```zig
std.mem.splitScalar(u8, expression, ' ')
```

That splits only on spaces. The string `"1+2"` has no spaces, so the parser sees one piece instead of three.

This limitation is acceptable for the first version. A more advanced calculator would scan the input character by character.

#### Add Tests

Zig has built-in tests. Add these tests at the bottom of `src/main.zig`:

```zig
test "parse addition expression" {
    const expr = try parseExpression("1 + 2");

    try std.testing.expectEqual(@as(i64, 1), expr.left);
    try std.testing.expectEqual(@as(u8, '+'), expr.operator);
    try std.testing.expectEqual(@as(i64, 2), expr.right);
}

test "evaluate addition" {
    const expr = Expression{
        .left = 1,
        .operator = '+',
        .right = 2,
    };

    const result = try evaluate(expr);

    try std.testing.expectEqual(@as(i64, 3), result);
}

test "division by zero fails" {
    const expr = Expression{
        .left = 1,
        .operator = '/',
        .right = 0,
    };

    try std.testing.expectError(error.DivisionByZero, evaluate(expr));
}
```

Run:

```bash
zig build test
```

Tests are useful because they let us check small pieces of the program without running the whole command-line interface manually.

#### Improving the Program

This calculator is intentionally simple. Here are natural next steps:

Support expressions without spaces:

```text
1+2
```

Support floating-point numbers:

```text
1.5 + 2.25
```

Support more than one operator:

```text
1 + 2 + 3
```

Support precedence:

```text
1 + 2 * 3
```

Support parentheses:

```text
(1 + 2) * 3
```

Support an interactive mode:

```text
calc> 1 + 2
3
calc> 10 / 2
5
```

But each of those features needs a better parser. The version in this section is a good first step because it has the same basic structure that larger programs use:

Read input.

Parse input.

Represent the parsed data.

Evaluate the data.

Print the result.

Handle errors clearly.

#### What You Learned

You built a complete command-line calculator.

You used command-line arguments with `std.process.argsWithAllocator`.

You parsed text with `std.mem.splitScalar`.

You converted text to integers with `std.fmt.parseInt`.

You modeled data with a struct.

You used `switch` to select behavior by operator.

You returned errors for invalid input and division by zero.

You wrote tests for the parser and evaluator.

This is the basic shape of many real command-line tools. The details change, but the structure stays the same: input, parse, process, output.

