# Validate CLI input

> Validate command-line values with Zod and handle missing input.

Give an option or positional argument a Zod schema to check the value a user supplies. The schema can also convert text, such as turning `--count 3` into a number.

## Validate a supplied value

In `src/cli.ts`, a Zod schema can convert an option's string into a number and reject invalid input. The `detected` check distinguishes an omitted option from a supplied value.

```ts
import { option, parser } from '@hulla/args'
import { argv } from 'node:process'
import { z } from 'zod'

const cli = parser({
  name: 'serve',
  arguments: [option({ name: 'port', schema: z.coerce.number().int().min(1) })],
})

const portArg = cli.parse(argv.slice(2)).arguments.port
const port = portArg.detected ? portArg.value : 3000
console.log(port)
```

Try the command with three inputs:

| Command                      | Result                                      |
| ---------------------------- | ------------------------------------------- |
| `bun src/cli.ts --port 8080` | Prints `8080`.                              |
| `bun src/cli.ts`             | Prints the fallback, `3000`.                |
| `bun src/cli.ts --port nope` | Throws a `ParserError` from Zod validation. |

> **Warning: Equals-form values can lose their casing**
>
> With the default `caseSensitive: false`, `--label=Sam` becomes `sam` before validation. Space-separated values and positionals keep their spelling, and `raw` keeps the original token. Set `caseSensitive: true` to preserve equals-form values too; argument names will then be case-sensitive.

## Require a positional value

> **Important: Schemas only check supplied input**
>
> An omitted option or positional has `detected: false` and `value: undefined`; its schema isn't called. Check `detected` to require it. Flags always validate a boolean, including `false` when absent.

Use `detected` to require that the user supplied a positional. A schema such as `.min(1)` validates a present string, but it cannot reject an omitted positional because the schema is skipped in that case.

```ts
import { ParserError, parser, positional } from '@hulla/args'
import { argv } from 'node:process'
import { z } from 'zod'

const cli = parser({
  name: 'notes',
  arguments: [
    positional({ name: 'title', schema: z.string().min(1, 'Enter a title') }),
  ],
})
```

Parse at the application's entrypoint. Check for a missing title explicitly, and catch validation errors from a title that was supplied:

```ts
try {
  const title = cli.parse(argv.slice(2)).arguments.title
  if (title.detected) {
    console.log(title.value)
  } else {
    console.error('A title is required')
    process.exitCode = 1
  }
} catch (error) {
  if (error instanceof ParserError) {
    console.error(error.message)
    process.exitCode = 1
  } else {
    throw error
  }
}
```

When a title is absent, the CLI's explicit check reports `A title is required`. An empty title is present, so Zod rejects it with `Enter a title`. Catch parser errors at the CLI boundary to choose the message and exit policy; `@hulla/args` does not print help or set the process exit code.

Zod validates values during parsing; it does not execute command handlers. See [arguments](https://hulla.dev/docs/args/arguments.md) for the available input shapes, and [commands](https://hulla.dev/docs/args/commands.md) to route a validated result.
