# Cliffy 1.1.0: responsive help, positional arguments, and error context

*Published on 2026-05-05 by c4spar*

## Command

### Responsive help output

Generated help used to assume a wide terminal. Long descriptions ran off the
edge in a narrow one, and there was no way to tell Cliffy how much room it had.
Help is now laid out from the terminal width, and descriptions wrap into their
column instead of overflowing.

Two help options give you direct control. `width` sets the width to render at
instead of the detected terminal width, and `maxWidth` caps it so help stays
readable on a very wide terminal:

```ts
import { Command } from "@cliffy/command";

await new Command()
  .name("deploy")
  .help({ maxWidth: 100 })
  .parse();
```

The layout itself comes from the table module, which gained
[flexible column widths](#flexible-column-widths) in the same release for this
purpose.

### `ErrorContext`

Error handlers used to receive the error and the failed command, so recovering
what the user actually typed meant parsing it again yourself. Handlers now get a
third argument, an `ErrorContext` carrying the options and arguments that were
parsed before the failure:

```ts
import { Command } from "@cliffy/command";

await new Command()
  .globalOption("-v, --verbose", "Print stack traces.")
  .error((error, _cmd, { options }) => {
    console.error(options.verbose ? error.stack : error.message);
  })
  .parse();
```

### Empty option and argument values

An empty string is now skipped for optional options and positional arguments, as
if nothing had been passed, instead of being taken as the value. A declared
default then applies. Without one the option is left out of the options object.
The behaviour comes from the parser, see [empty values](#empty-values).

```ts
import { Command } from "@cliffy/command";

await new Command()
  .name("greet")
  .option("--name [name:string]", "Who to greet.", { default: "world" })
  .argument("[times:number]", "How many times to repeat the greeting.", {
    default: 1,
  })
  .action(({ name }, times) => {
    for (let i = 0; i < times; i++) console.log(`Hello, ${name}!`);
  })
  .parse();
```

```sh
$ greet --name "" ""
Hello, world!
```

It makes no difference whether the value is written `[name:string]` or
`<name:string>`. What decides it is whether the option or argument itself is
required, not how its value is declared.

Positional arguments keep their place. An empty value becomes `undefined` rather
than dropping out, so the arguments after it do not shift. Position carries no
meaning inside a rest argument, so there the empty values are dropped from the
list instead:

```ts
import { Command } from "@cliffy/command";

await new Command()
  .name("example")
  .arguments("[foo] [bar] [baz] [...beep]")
  .action((_options, ...args) => console.log(args))
  .parse();
```

```sh
$ example "" bar-value "" "" "" beep-value-3 "" beep-value-5
[ undefined, "bar-value", undefined, "beep-value-3", "beep-value-5" ]
```

`foo` and `baz` hold their positions as `undefined`, while the three empty
values passed to `beep` are gone.

### Help for container commands

A command that only groups subcommands and has no action of its own now prints
its help when called with no arguments, instead of doing nothing. It saves
calling `this.showHelp()` from an action handler that exists for nothing else.

It is on by default and `auto` in the help options turns it off:

```ts
import { Command } from "@cliffy/command";

await new Command()
  .name("tool")
  .help({ auto: false })
  .command("sub", "A subcommand.")
  .parse();
```

## Table

### Flexible column widths

`@cliffy/table` gains `flexGrow`, `flexShrink` and the `flex` shorthand, both
per column and as table-level defaults. Columns can absorb or give up space,
which is what lets the help description column take whatever is left over:

```ts
import { Table } from "@cliffy/table";

new Table()
  .body([["Name", "A description that gives up space when the table is tight"]])
  .maxWidth(60)
  .flexShrink([0, 1])
  .render();
```

The same three are available on `Column`, as methods and as options, so a single
column can be configured on its own instead of through a table-level array:

```ts
import { Column, Table } from "@cliffy/table";

new Table()
  .body([["Name", "A description that gives up space when the table is tight"]])
  .maxWidth(60)
  .columns([Column.from({ flexShrink: 0 }), Column.from({ flexShrink: 1 })])
  .render();
```

Flex needs a finite `maxWidth` on the table to have anything to lay out against.
Without one it is a no-op.

## Flags

### Positional arguments

`@cliffy/flags` parses positional arguments rather than leaving everything that
isn't a flag in `unknown`. Declare them with `args` and each value is validated
and converted with its declared type, the same way option values are:

```ts
import { parseFlags } from "@cliffy/flags";

const { flags, args } = parseFlags(["--count", "3", "file.txt"], {
  flags: [{ name: "count", type: "number" }],
  args: [{ name: "file", type: "string" }],
});

console.log(flags); // { count: 3 }
console.log(args); // [ "file.txt" ]
```

This is runtime parsing only. `args` comes back as `Array<unknown> | undefined`.

### Parsed flags

The parse context now carries `parsedFlags`, the raw flag tokens in the order
they were typed. That is how Cliffy knows whether a value came from the command
line or from a default.

```ts
import { parseFlags } from "@cliffy/flags";

const ctx = parseFlags(["--count", "3", "file.txt"], {
  flags: [{ name: "count", type: "number" }],
  args: [{ name: "file", type: "string" }],
});

console.log(ctx.parsedFlags); // [ "--count", "3" ]
```

Positional arguments are left out, so what remains is exactly what the user
typed as flags.

### Empty values

An empty string passed to an optional option or argument is now skipped rather
than taken as the value, so a declared default applies instead of being
overwritten with `""`. Positional arguments become `undefined` so the arguments
after them keep their places, and inside a rest argument they are dropped. The
parsing lives here, and commands inherit it, see
[empty option and argument values](#empty-option-and-argument-values).

## Testing

### Environment variables per test and step

Snapshot tests take an `env` option, and each step can set its own, so one test
file can cover the same code under different environments:

```ts
import { snapshotTest } from "@cliffy/testing";

await snapshotTest({
  name: "should read the log level from the environment",
  meta: import.meta,
  steps: {
    "quiet": { env: { LOG_LEVEL: "error" } },
    "loud": { env: { LOG_LEVEL: "debug" } },
  },
  fn() {
    console.log(Deno.env.get("LOG_LEVEL"));
  },
});
```

### Filtering steps with `only`

`only` works on individual steps, not just whole tests, so you can narrow a
failing suite to one step while you work on it:

```ts
import { snapshotTest } from "@cliffy/testing";

await snapshotTest({
  name: "should render the prompt",
  meta: import.meta,
  steps: {
    "with a default": {},
    "with a hint": { only: true },
  },
  fn() {
    // ...
  },
});
```

## Quality of life improvements

- `snapshotTest` is faster.
- Leading dashes are allowed in positional arguments.
- `parseFlags` throws `TooManyArgumentsError` when it receives more positional
  arguments than were declared, rather than collecting the extras in
  `ctx.unknown`.
- Special characters in option descriptions are escaped for zsh completions.
- Error messages are suppressed while generating completions, so a broken
  command doesn't corrupt the generated script.

## Fixed in 1.0.1

Shipped in the patch since 1.0:

- Argument type inference for mapped arguments.
- Default command handling moved into `parseCommand`.
- `--flag` parsing after a variadic option.
- Bracketed paste and escape sequence parsing in `keycode`.
- Correct return types for action handlers and `generateShellCompletions`.
- Error classes in `flags` get their prototypes assigned correctly, so
  `instanceof` works.

## Acknowledgements

Responsive help came out of [#761](https://github.com/c4spar/cliffy/pull/761) by
Chris Markiewicz, which laid the terminal-width layout and the table's
`flexShrink` that the rest of this release builds on. Thank you.

Contributions are welcome. The
[contributing guide](https://github.com/c4spar/cliffy/blob/main/CONTRIBUTING.md)
covers how to get started.
