What is Hackers' Pub?

Hackers' Pub is a place for software engineers to share their knowledge and experience with each other. It's also an ActivityPub-enabled social network, so you can follow your favorite hackers in the fediverse and get their latest posts in your feed.

0
1
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0
0

잇창명 EatChangmyeong💕🐱 shared the below article:

Stop writing if statements for your CLI flags

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

If you've built CLI tools, you've written code like this:

if (opts.reporter === "junit" && !opts.outputFile) {
  throw new Error("--output-file is required for junit reporter");
}
if (opts.reporter === "html" && !opts.outputFile) {
  throw new Error("--output-file is required for html reporter");
}
if (opts.reporter === "console" && opts.outputFile) {
  console.warn("--output-file is ignored for console reporter");
}

A few months ago, I wrote Stop writing CLI validation. Parse it right the first time. about parsing individual option values correctly. But it didn't cover the relationships between options.

In the code above, --output-file only makes sense when --reporter is junit or html. When it's console, the option shouldn't exist at all.

We're using TypeScript. We have a powerful type system. And yet, here we are, writing runtime checks that the compiler can't help with. Every time we add a new reporter type, we need to remember to update these checks. Every time we refactor, we hope we didn't miss one.

The state of TypeScript CLI parsers

The old guard—Commander, yargs, minimist—were built before TypeScript became mainstream. They give you bags of strings and leave type safety as an exercise for the reader.

But we've made progress. Modern TypeScript-first libraries like cmd-ts and Clipanion (the library powering Yarn Berry) take types seriously:

// cmd-ts
const app = command({
  args: {
    reporter: option({ type: string, long: 'reporter' }),
    outputFile: option({ type: string, long: 'output-file' }),
  },
  handler: (args) => {
    // args.reporter: string
    // args.outputFile: string
  },
});
// Clipanion
class TestCommand extends Command {
  reporter = Option.String('--reporter');
  outputFile = Option.String('--output-file');
}

These libraries infer types for individual options. --port is a number. --verbose is a boolean. That's real progress.

But here's what they can't do: express that --output-file is required when --reporter is junit, and forbidden when --reporter is console. The relationship between options isn't captured in the type system.

So you end up writing validation code anyway:

handler: (args) => {
  // Both cmd-ts and Clipanion need this
  if (args.reporter === "junit" && !args.outputFile) {
    throw new Error("--output-file required for junit");
  }
  // args.outputFile is still string | undefined
  // TypeScript doesn't know it's definitely string when reporter is "junit"
}

Rust's clap and Python's Click have requires and conflicts_with attributes, but those are runtime checks too. They don't change the result type.

If the parser configuration knows about option relationships, why doesn't that knowledge show up in the result type?

Modeling relationships with conditional()

Optique treats option relationships as a first-class concept. Here's the test reporter scenario:

import { conditional, object } from "@optique/core/constructs";
import { option } from "@optique/core/primitives";
import { choice, string } from "@optique/core/valueparser";
import { run } from "@optique/run";

const parser = conditional(
  option("--reporter", choice(["console", "junit", "html"])),
  {
    console: object({}),
    junit: object({
      outputFile: option("--output-file", string()),
    }),
    html: object({
      outputFile: option("--output-file", string()),
      openBrowser: option("--open-browser"),
    }),
  }
);

const [reporter, config] = run(parser);

The conditional() combinator takes a discriminator option (--reporter) and a map of branches. Each branch defines what other options are valid for that discriminator value.

TypeScript infers the result type automatically:

type Result =
  | ["console", {}]
  | ["junit", { outputFile: string }]
  | ["html", { outputFile: string; openBrowser: boolean }];

When reporter is "junit", outputFile is string—not string | undefined. The relationship is encoded in the type.

Now your business logic gets real type safety:

const [reporter, config] = run(parser);

switch (reporter) {
  case "console":
    runWithConsoleOutput();
    break;
  case "junit":
    // TypeScript knows config.outputFile is string
    writeJUnitReport(config.outputFile);
    break;
  case "html":
    // TypeScript knows config.outputFile and config.openBrowser exist
    writeHtmlReport(config.outputFile);
    if (config.openBrowser) openInBrowser(config.outputFile);
    break;
}

No validation code. No runtime checks. If you add a new reporter type and forget to handle it in the switch, the compiler tells you.

A more complex example: database connections

Test reporters are a nice example, but let's try something with more variation. Database connection strings:

myapp --db=sqlite --file=./data.db
myapp --db=postgres --host=localhost --port=5432 --user=admin
myapp --db=mysql --host=localhost --port=3306 --user=root --ssl

Each database type needs completely different options:

  • SQLite just needs a file path
  • PostgreSQL needs host, port, user, and optionally password
  • MySQL needs host, port, user, and has an SSL flag

Here's how you model this:

import { conditional, object } from "@optique/core/constructs";
import { withDefault, optional } from "@optique/core/modifiers";
import { option } from "@optique/core/primitives";
import { choice, string, integer } from "@optique/core/valueparser";

const dbParser = conditional(
  option("--db", choice(["sqlite", "postgres", "mysql"])),
  {
    sqlite: object({
      file: option("--file", string()),
    }),
    postgres: object({
      host: option("--host", string()),
      port: withDefault(option("--port", integer()), 5432),
      user: option("--user", string()),
      password: optional(option("--password", string())),
    }),
    mysql: object({
      host: option("--host", string()),
      port: withDefault(option("--port", integer()), 3306),
      user: option("--user", string()),
      ssl: option("--ssl"),
    }),
  }
);

The inferred type:

type DbConfig =
  | ["sqlite", { file: string }]
  | ["postgres", { host: string; port: number; user: string; password?: string }]
  | ["mysql", { host: string; port: number; user: string; ssl: boolean }];

Notice the details: PostgreSQL defaults to port 5432, MySQL to 3306. PostgreSQL has an optional password, MySQL has an SSL flag. Each database type has exactly the options it needs—no more, no less.

With this structure, writing dbConfig.ssl when the mode is sqlite isn't a runtime error—it's a compile-time impossibility.

Try expressing this with requires_if attributes. You can't. The relationships are too rich.

The pattern is everywhere

Once you see it, you find this pattern in many CLI tools:

Authentication modes:

const authParser = conditional(
  option("--auth", choice(["none", "basic", "token", "oauth"])),
  {
    none: object({}),
    basic: object({
      username: option("--username", string()),
      password: option("--password", string()),
    }),
    token: object({
      token: option("--token", string()),
    }),
    oauth: object({
      clientId: option("--client-id", string()),
      clientSecret: option("--client-secret", string()),
      tokenUrl: option("--token-url", url()),
    }),
  }
);

Deployment targets, output formats, connection protocols—anywhere you have a mode selector that determines what other options are valid.

Why conditional() exists

Optique already has an or() combinator for mutually exclusive alternatives. Why do we need conditional()?

The or() combinator distinguishes branches based on structure—which options are present. It works well for subcommands like git commit vs git push, where the arguments differ completely.

But in the reporter example, the structure is identical: every branch has a --reporter flag. The difference lies in the flag's value, not its presence.

// This won't work as intended
const parser = or(
  object({ reporter: option("--reporter", choice(["console"])) }),
  object({ 
    reporter: option("--reporter", choice(["junit", "html"])),
    outputFile: option("--output-file", string())
  }),
);

When you pass --reporter junit, or() tries to pick a branch based on what options are present. Both branches have --reporter, so it can't distinguish them structurally.

conditional() solves this by reading the discriminator's value first, then selecting the appropriate branch. It bridges the gap between structural parsing and value-based decisions.

The structure is the constraint

Instead of parsing options into a loose type and then validating relationships, define a parser whose structure is the constraint.

Traditional approach Optique approach
Parse → Validate → Use Parse (with constraints) → Use
Types and validation logic maintained separately Types reflect the constraints
Mismatches found at runtime Mismatches found at compile time

The parser definition becomes the single source of truth. Add a new reporter type? The parser definition changes, the inferred type changes, and the compiler shows you everywhere that needs updating.

Try it

If this resonates with a CLI you're building:

  • Documentation
  • Tutorial
  • conditional() reference
  • GitHub

Next time you're about to write an if statement checking option relationships, ask: could the parser express this constraint instead?

The structure of your parser is the constraint. You might not need that validation code at all.

Read more →
3
0
0
0
0

Hope: The Autobiography by Pope Francis & Jorge Mario Bergoglio, 2025

Hope is the first autobiography in history ever to be published by a Pope. Written over six years, this complete autobiography starts in the early years of the twentieth century, with Pope Francis’s Italian roots and his ancestors’ courageous migration to Latin America, continuing through his childhood, the enthusiasms and preoccupations of his youth, his vocation, adult life




and the whole of his papacy up to the present day. 

In recounting his memories with intimate narrative force (not forgetting his own personal passions), Pope Francis deals unsparingly with some of the crucial moments of his papacy and writes candidly, fearlessly, and prophetically about some of the most important and controversial questions of our present times.
0
0
0

블스(AT프로토콜)는 오픈 SNS를 지향하고 있고 제한된 글을 공유하고 싶으면 액펍 서비스를 사용하는게 더 좋은 선택이며 액펍으로 블친들이랑 소통하고 싶으면 브릿지 쓰시면 되고 블스에서 네이티브 비밀글을 만들려면 E2EE 가 필요한데 E2EE 가 구현이 여간 쉬운게 아니라 시간이 꽤 걸릴텐데도 굳이 블스를 비계처럼 쓰고 싶으시다길래 시크릿 스카이 secret-sky.vercel.app 도 제가 직접 만들어 드렸는데 굳이 블스에 글 쓰면서 동시에 알티마저 금지해달라고 바라는 건 욕심입니다...

Secret Sky

0

다행히 블스에는 인용에 대해 많이 관대해진 것 같은데 트위터를 막 시작했을 때 인용에 대해 너무 적대적으로 반응해서 좀 힘들었음 그래서 그거 관련해서 글 썼더니 "인용으로 무례하게 글 쓰는 사람이 많아서 그렇다" 라고 무례하게들 반응하더라고 인용으로 무례하게 굴면 무례한 게 잘못이고 인용으호 욕을 하면 욕을 한 게 잘못 아냐?? 왜 그게 남이 서비스에 있는 멀쩡한 기능을 사적으로 제재하는 근거가 되는지 이해가 안 됐음 블스도 초창기엔 그런 얘기 나오다가 그냥 인용 싫은 사람은 인용 금지 걸어놓을 수 있어서 별 얘기 안 나오는 듯

0

ちゃっとで相槌をうつのに「ふむふむ」くらいの意味合いで「ふんふん」と書き込んだらなんというかちょっと間抜けというか変な感じになってしまった(かもしれない、だいじょぶかな)

0
1

ちょっとこちらの声も入ってしまってるからなあ。分が悪い。

(引用

中国側が出した音声、メディアが出す翻訳が微妙なので、自分で訳しました。
ちなみに空母(ここから飛行機が飛ぶ)と、その周りに様々な役割の船を配置して演習をします。今回、連絡してきたのは、周りの船からです。
日本側は、明らかに日本語話者の英語の発音、中国側は中国語話者の発音。(典型的なアジア人の発音)
この違いは、さすがに今のAIでも作れないので、リアルな音声です。
このやり取りからわかることは、
①いきなりの連絡ではなく、事前から計画されていた軍事演習
②日本側が「了解した」と返事をしている
③防衛大臣が言う「聞いてなかった」とは整合しない
threads.com/@tsugumi_penstorie

0
0
0
0
0
0

ちょっとこちらの声も入ってしまってるからなあ。分が悪い。

(引用

中国側が出した音声、メディアが出す翻訳が微妙なので、自分で訳しました。
ちなみに空母(ここから飛行機が飛ぶ)と、その周りに様々な役割の船を配置して演習をします。今回、連絡してきたのは、周りの船からです。
日本側は、明らかに日本語話者の英語の発音、中国側は中国語話者の発音。(典型的なアジア人の発音)
この違いは、さすがに今のAIでも作れないので、リアルな音声です。
このやり取りからわかることは、
①いきなりの連絡ではなく、事前から計画されていた軍事演習
②日本側が「了解した」と返事をしている
③防衛大臣が言う「聞いてなかった」とは整合しない
threads.com/@tsugumi_penstorie

0
0
0
0
0

I fear this joke will only be funny for like one year or two, and then nobody will get it anymore because it will truly be considered crazy stuff to write, draw, code, or read just using your own brain 🫠

I hate this AI-dystopian timeline :blobcat_thisisfine:

0

I fear this joke will only be funny for like one year or two, and then nobody will get it anymore because it will truly be considered crazy stuff to write, draw, code, or read just using your own brain 🫠

I hate this AI-dystopian timeline :blobcat_thisisfine:

0
0

この問題についても話したいことはたくさんありますが、まず、MastodonやMisskeyなどの既存の主流な実装が「ActivityPubを改善するため」という名目で、FEPのような標準化プロセスを経ずに拡張機能を実装してしまうのは、コミュニティとしては困る点があると考えています。こうした批判を意識してか、Mastodonは最近、独自の引用仕様を実装する前に「FEP-044f: Consent-respecting quote posts」を提案したりもしました。ただ、FEPが依然として英語圏中心で動いていることは、非英語圏の開発者にとっては残念な状況だと思います。

5
1
0
0
0

この問題についても話したいことはたくさんありますが、まず、MastodonやMisskeyなどの既存の主流な実装が「ActivityPubを改善するため」という名目で、FEPのような標準化プロセスを経ずに拡張機能を実装してしまうのは、コミュニティとしては困る点があると考えています。こうした批判を意識してか、Mastodonは最近、独自の引用仕様を実装する前に「FEP-044f: Consent-respecting quote posts」を提案したりもしました。ただ、FEPが依然として英語圏中心で動いていることは、非英語圏の開発者にとっては残念な状況だと思います。

5
1
0
0
0

그래도 일론의 공학적 지식은 부정하고 싶진 않지만 이 인간은 자기가 모르는 분야도 아는척해서 문제라고 생각. 트위터 인수 시작하면서 계속 기술에 대해서 헛소리하는 것도 그렇고 나중가선 맞지도 않는 사실 퍼뜨리는거 보고 어이가 없었다.

0
0
0

노동이 노동자의 본질에 속하지 않는 이유 때문에 노동자는 노동 속에서 행복을 느끼지 못하며, 외부에서야 비로소 자기 곁에 있다고 느낀다. 노동하지 않을 때에는 그의 집에 있는 것처럼 편안하고, 노동할 때에는 편안하지 않다. 그런 까닭에 노동은 어떤 욕구의 만족이 아니라 노동 바깥에 있는 욕구를 만족시키기 위한 수단일 뿐이며, 강요된 강제노동이다. - 카를 마르크스 <경제학-철학 수고> 中

RE: https://bsky.app/profile/did:plc:ai6aov3o6xql2rohuapzl4x7/post/3m7lvpuyjls2n

0

스티브 잡스와 일론 머스크

스티브잡스가 정말,,, 리더십계에서 대단히 불미스러운 리더인데 조직을 이끌면서 리더로서 이런거좀 하지말아요 하는거 다 해놓고 대박 성공하는 바람에
얘 이전에는 리더는 조직을 잘 관리하고 총괄하고 목표를 제시하고... 이런 롤이었는데 (이것도 올드한 리더상이라 요즘에 들어맞진 않습니다만)
스티브잡스가 괴짜스타일의 리더로 엄청 유명해지면서 대충 인성은 터졌지만 일에대한 미학이 있고 인간을 갈아넣어서 아무튼 성과를 낸 어쩌구천재~ 류의 리더상이 핫해졋어요 스티브잡스는 죽엇지만 이런 리더상에 대한 수요는 그 후에도 계속 폭발적으로 증가햇고요 이런 흐름을 타서 돈과 백양남혈통으로 이미지메이킹을 잘 해서 테슬라빠들을 만들어낸게 일론임
그래서 걔네한텐 그 많은 사람들의 성과가 다 일론의 성과인거고 일론의 기술력 이런말이 나오는거

0